aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTrumeet <yuuta@yuuta.moe>2021-04-02 22:59:55 -0700
committerTrumeet <yuuta@yuuta.moe>2021-04-02 22:59:55 -0700
commit8b655e6058feaa4a00c6824400d57b3969d5e4db (patch)
tree75d5d32465cedc419300c820394e91f9f56740ac
parentc056e7ffb3d0f4ecf62d9927c5761b034d740620 (diff)
downloaddn42peering-8b655e6058feaa4a00c6824400d57b3969d5e4db.tar
dn42peering-8b655e6058feaa4a00c6824400d57b3969d5e4db.tar.gz
dn42peering-8b655e6058feaa4a00c6824400d57b3969d5e4db.tar.bz2
dn42peering-8b655e6058feaa4a00c6824400d57b3969d5e4db.zip
feat(central): add ASN admin page
-rw-r--r--central/src/main/java/moe/yuuta/dn42peering/admin/AdminHandler.java2
-rw-r--r--central/src/main/java/moe/yuuta/dn42peering/admin/asn/ASNAdminUI.java57
-rw-r--r--central/src/main/java/moe/yuuta/dn42peering/admin/asn/ASNHandler.java111
-rw-r--r--central/src/main/java/moe/yuuta/dn42peering/asn/ASNServiceImpl.java17
-rw-r--r--central/src/main/java/moe/yuuta/dn42peering/asn/IASNService.java4
-rw-r--r--central/src/main/resources/admin/asn/changepw.ftlh38
-rw-r--r--central/src/main/resources/admin/asn/index.ftlh25
-rw-r--r--central/src/main/resources/admin/index.ftlh1
8 files changed, 255 insertions, 0 deletions
diff --git a/central/src/main/java/moe/yuuta/dn42peering/admin/AdminHandler.java b/central/src/main/java/moe/yuuta/dn42peering/admin/AdminHandler.java
index 766aae2..7495611 100644
--- a/central/src/main/java/moe/yuuta/dn42peering/admin/AdminHandler.java
+++ b/central/src/main/java/moe/yuuta/dn42peering/admin/AdminHandler.java
@@ -20,6 +20,7 @@ import io.vertx.json.schema.SchemaParser;
import io.vertx.json.schema.SchemaRouter;
import io.vertx.json.schema.SchemaRouterOptions;
import io.vertx.json.schema.common.dsl.ObjectSchemaBuilder;
+import moe.yuuta.dn42peering.admin.asn.ASNHandler;
import moe.yuuta.dn42peering.admin.nodes.NodeHandler;
import moe.yuuta.dn42peering.asn.IASNService;
import moe.yuuta.dn42peering.manage.AdminASNAuthProvider;
@@ -112,6 +113,7 @@ public class AdminHandler implements ISubRouter {
});
router.mountSubRouter("/nodes", new NodeHandler().mount(vertx));
+ router.mountSubRouter("/asn", new ASNHandler().mount(vertx));
return router;
}
}
diff --git a/central/src/main/java/moe/yuuta/dn42peering/admin/asn/ASNAdminUI.java b/central/src/main/java/moe/yuuta/dn42peering/admin/asn/ASNAdminUI.java
new file mode 100644
index 0000000..f940c2b
--- /dev/null
+++ b/central/src/main/java/moe/yuuta/dn42peering/admin/asn/ASNAdminUI.java
@@ -0,0 +1,57 @@
+package moe.yuuta.dn42peering.admin.asn;
+
+import io.vertx.core.Future;
+import io.vertx.core.json.JsonArray;
+import io.vertx.core.json.JsonObject;
+import io.vertx.ext.web.RoutingContext;
+import io.vertx.ext.web.common.template.TemplateEngine;
+import moe.yuuta.dn42peering.asn.ASN;
+import moe.yuuta.dn42peering.asn.IASNService;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static moe.yuuta.dn42peering.portal.RenderingUtils.getGeneralRenderingHandler;
+
+class ASNAdminUI {
+ public static void renderIndex(@Nonnull String asn,
+ @Nonnull TemplateEngine engine,
+ @Nonnull IASNService asnService,
+ @Nonnull RoutingContext ctx) {
+ Future.<List<ASN>>future(asnService::list)
+ .compose(asns -> {
+ final Map<String, Object> root = new HashMap<>();
+ root.put("asn", asn);
+ // Don't need to do that in async thread?
+ final JsonArray mapping = new JsonArray();
+ asns.stream().map(asnObj -> {
+ final JsonObject asnJson = new JsonObject();
+ asnJson.put("asn", asnObj.getAsn());
+ asnJson.put("activated", asnObj.isActivated());
+ return asnJson;
+ }).forEach(mapping::add);
+ root.put("asns", mapping);
+ return Future.succeededFuture(root);
+ })
+ .compose(json -> {
+ return engine.render(json, "admin/asn/index.ftlh");
+ })
+ .onComplete(getGeneralRenderingHandler(ctx));
+ }
+
+ public static void renderChangePassword(@Nonnull String asn,
+ @Nullable String targetASN,
+ @Nullable List<String> errors,
+ @Nonnull TemplateEngine engine,
+ @Nonnull RoutingContext ctx) {
+ final Map<String, Object> root = new HashMap<>();
+ root.put("asn", asn);
+ root.put("target_asn", targetASN);
+ root.put("errors", errors);
+ engine.render(root, "admin/asn/changepw.ftlh")
+ .onComplete(getGeneralRenderingHandler(ctx));
+ }
+}
diff --git a/central/src/main/java/moe/yuuta/dn42peering/admin/asn/ASNHandler.java b/central/src/main/java/moe/yuuta/dn42peering/admin/asn/ASNHandler.java
new file mode 100644
index 0000000..38c1403
--- /dev/null
+++ b/central/src/main/java/moe/yuuta/dn42peering/admin/asn/ASNHandler.java
@@ -0,0 +1,111 @@
+package moe.yuuta.dn42peering.admin.asn;
+
+import io.vertx.core.Vertx;
+import io.vertx.core.impl.logging.Logger;
+import io.vertx.core.impl.logging.LoggerFactory;
+import io.vertx.core.json.JsonObject;
+import io.vertx.ext.web.Router;
+import io.vertx.ext.web.common.template.TemplateEngine;
+import io.vertx.ext.web.handler.BodyHandler;
+import io.vertx.ext.web.templ.freemarker.FreeMarkerTemplateEngine;
+import io.vertx.ext.web.validation.RequestParameters;
+import io.vertx.ext.web.validation.RequestPredicate;
+import io.vertx.ext.web.validation.ValidationHandler;
+import io.vertx.ext.web.validation.builder.Bodies;
+import io.vertx.json.schema.SchemaParser;
+import io.vertx.json.schema.SchemaRouter;
+import io.vertx.json.schema.SchemaRouterOptions;
+import moe.yuuta.dn42peering.asn.IASNService;
+import moe.yuuta.dn42peering.portal.ISubRouter;
+
+import javax.annotation.Nonnull;
+import java.util.Collections;
+import java.util.List;
+
+import static io.vertx.json.schema.common.dsl.Schemas.objectSchema;
+import static io.vertx.json.schema.common.dsl.Schemas.stringSchema;
+
+public class ASNHandler implements ISubRouter {
+ private final Logger logger = LoggerFactory.getLogger(getClass().getSimpleName());
+ @Nonnull
+ @Override
+ public Router mount(@Nonnull Vertx vertx) {
+ final IASNService asnService = IASNService.createProxy(vertx, IASNService.ADDRESS);
+ final TemplateEngine engine = FreeMarkerTemplateEngine.create(vertx, "ftlh");
+ final SchemaParser parser = SchemaParser.createDraft7SchemaParser(
+ SchemaRouter.create(vertx, new SchemaRouterOptions()));
+
+ final Router router = Router.router(vertx);
+ router.post().handler(BodyHandler.create().setBodyLimit(100 * 1024));
+
+ router.get("/")
+ .produces("text/html")
+ .handler(ctx -> ASNAdminUI.renderIndex(
+ ctx.user().principal().getString("username"),
+ engine,
+ asnService,
+ ctx));
+ router.get("/change-password")
+ .produces("text/html")
+ .handler(ctx -> {
+ final List<String> predefinedASN =
+ ctx.queryParam("asn");
+ ASNAdminUI.renderChangePassword(
+ ctx.user().principal().getString("username"),
+ predefinedASN == null || predefinedASN.isEmpty() ? null :
+ predefinedASN.get(0),
+ null,
+ engine,
+ ctx);
+ });
+ router.post("/change-password")
+ .produces("text/html")
+ .handler(BodyHandler.create().setBodyLimit(100 * 1024))
+ .handler(ValidationHandler
+ .builder(parser)
+ .body(Bodies.formUrlEncoded(objectSchema()
+ .property("asn", stringSchema())
+ .property("passwd", stringSchema())
+ .property("confirm", stringSchema())
+ .allowAdditionalProperties(false)))
+ .predicate(RequestPredicate.BODY_REQUIRED)
+ .build())
+ .handler(ctx -> {
+ final String asn = ctx.user().principal().getString("username");
+ final JsonObject parameters = ctx.<RequestParameters>get(ValidationHandler.REQUEST_CONTEXT_KEY)
+ .body().getJsonObject();
+ final String targetASN = parameters.getString("asn");
+ final String passwd = parameters.getString("passwd");
+ final String confirm = parameters.getString("confirm");
+ if(targetASN == null || passwd == null || confirm == null ||
+ targetASN.isEmpty() || passwd.isEmpty() || confirm.isEmpty()) {
+ ASNAdminUI.renderChangePassword(asn,
+ targetASN,
+ Collections.singletonList("Some fields are not supplied."),
+ engine,
+ ctx);
+ return;
+ }
+ if(!passwd.equals(confirm)) {
+ ASNAdminUI.renderChangePassword(asn,
+ targetASN,
+ Collections.singletonList("Passwords mismatch."),
+ engine,
+ ctx);
+ return;
+ }
+ asnService.changePassword(targetASN, passwd, ar -> {
+ if(ar.succeeded()) {
+ // TODO: Destroy sessions?
+ ctx.response()
+ .setStatusCode(303)
+ .putHeader("Location", "/admin/asn")
+ .end();
+ } else {
+ logger.error("Cannot change password for " + targetASN, ar.cause());
+ }
+ });
+ });
+ return router;
+ }
+}
diff --git a/central/src/main/java/moe/yuuta/dn42peering/asn/ASNServiceImpl.java b/central/src/main/java/moe/yuuta/dn42peering/asn/ASNServiceImpl.java
index 2fce4f1..81bd946 100644
--- a/central/src/main/java/moe/yuuta/dn42peering/asn/ASNServiceImpl.java
+++ b/central/src/main/java/moe/yuuta/dn42peering/asn/ASNServiceImpl.java
@@ -162,4 +162,21 @@ class ASNServiceImpl implements IASNService {
.onComplete(handler);
return this;
}
+
+ @Nonnull
+ @Override
+ public IASNService list(@Nonnull Handler<AsyncResult<List<ASN>>> handler) {
+ SqlTemplate
+ .forQuery(pool, "SELECT asn, activated " +
+ "FROM asn")
+ .mapTo(ASNRowMapper.INSTANCE)
+ .execute(null)
+ .compose(asns -> {
+ final List<ASN> asnList = new ArrayList<>();
+ for (ASN asn : asns) asnList.add(asn);
+ return Future.succeededFuture(asnList);
+ })
+ .onComplete(handler);
+ return this;
+ }
}
diff --git a/central/src/main/java/moe/yuuta/dn42peering/asn/IASNService.java b/central/src/main/java/moe/yuuta/dn42peering/asn/IASNService.java
index a8f27da..d4b31ae 100644
--- a/central/src/main/java/moe/yuuta/dn42peering/asn/IASNService.java
+++ b/central/src/main/java/moe/yuuta/dn42peering/asn/IASNService.java
@@ -53,4 +53,8 @@ public interface IASNService {
@Fluent
@Nonnull
IASNService count(@Nonnull Handler<AsyncResult<Integer>> handler);
+
+ @Fluent
+ @Nonnull
+ IASNService list(@Nonnull Handler<AsyncResult<List<ASN>>> handler);
}
diff --git a/central/src/main/resources/admin/asn/changepw.ftlh b/central/src/main/resources/admin/asn/changepw.ftlh
new file mode 100644
index 0000000..a753801
--- /dev/null
+++ b/central/src/main/resources/admin/asn/changepw.ftlh
@@ -0,0 +1,38 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <#include "/style.ftlh">
+ <title>Change Password | Admin</title>
+</head>
+<body class="markdown-body">
+<h1>Change Password</h1>
+<p>You are logged in as: ${asn}.</p>
+<#if errors??>
+<div>
+ <p style="color:red">Errors in the previous form:</p>
+ <ul>
+ <#list errors as error>
+ <li>${error}</li>
+ </#list>
+ </ul>
+</div>
+</#if>
+<form action="/admin/asn/change-password" method="post">
+ <label for="asn">New Password:</label><br />
+ <input type="asn" id="asn" name="asn" required
+ placeholder="${asn}"
+ value="${(target_asn)!}"><br />
+ <br />
+ <label for="passwd">New Password:</label><br />
+ <input type="password" id="passwd" name="passwd" required
+ placeholder="p@ssw0rd!"><br />
+ <br />
+ <label for="confirm">Confirm Password:</label><br />
+ <input type="password" id="confirm" name="confirm" required
+ placeholder="p@ssw0rd!"><br />
+ <br />
+ <input type="submit" id="submit">
+</form>
+</body>
+</html> \ No newline at end of file
diff --git a/central/src/main/resources/admin/asn/index.ftlh b/central/src/main/resources/admin/asn/index.ftlh
new file mode 100644
index 0000000..af0e509
--- /dev/null
+++ b/central/src/main/resources/admin/asn/index.ftlh
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <#include "/style.ftlh">
+ <title>ASN | Admin</title>
+</head>
+<body class="markdown-body">
+<h1>ASN Management</h1>
+<table style="width: 100%">
+ <tr>
+ <th>ASN</th>
+ <th>Activated</th>
+ <th>Actions</th>
+ </tr>
+ <#list asns as asn>
+ <tr>
+ <td>${asn.asn}</td>
+ <td>${asn.activated?string('Yes', 'No')}</td>
+ <td><a href="/admin/asn/change-password?asn=${asn.asn}">Change Password</a></td>
+ </tr>
+ </#list>
+</table>
+</body>
+</html> \ No newline at end of file
diff --git a/central/src/main/resources/admin/index.ftlh b/central/src/main/resources/admin/index.ftlh
index d3e3fd3..8ef87af 100644
--- a/central/src/main/resources/admin/index.ftlh
+++ b/central/src/main/resources/admin/index.ftlh
@@ -10,6 +10,7 @@
<p>You are logged in as: ${asn}.</p>
<p>dn42peering administrative tasks.</p>
<ul>
+<li><a href="/admin/asn">ASN Management</a></li>
<li><a href="/admin/sudo">sudo</a></li>
</ul>
<h2>Registered ASNs and Peering</h2>