diff options
author | Trumeet <yuuta@yuuta.moe> | 2021-04-02 22:59:55 -0700 |
---|---|---|
committer | Trumeet <yuuta@yuuta.moe> | 2021-04-02 22:59:55 -0700 |
commit | 8b655e6058feaa4a00c6824400d57b3969d5e4db (patch) | |
tree | 75d5d32465cedc419300c820394e91f9f56740ac | |
parent | c056e7ffb3d0f4ecf62d9927c5761b034d740620 (diff) | |
download | dn42peering-8b655e6058feaa4a00c6824400d57b3969d5e4db.tar dn42peering-8b655e6058feaa4a00c6824400d57b3969d5e4db.tar.gz dn42peering-8b655e6058feaa4a00c6824400d57b3969d5e4db.tar.bz2 dn42peering-8b655e6058feaa4a00c6824400d57b3969d5e4db.zip |
feat(central): add ASN admin page
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> |