diff --git a/src/main/java/net/frozenorb/apiv3/APIv3.java b/src/main/java/net/frozenorb/apiv3/APIv3.java index 7039274..2d0e5e5 100644 --- a/src/main/java/net/frozenorb/apiv3/APIv3.java +++ b/src/main/java/net/frozenorb/apiv3/APIv3.java @@ -62,6 +62,8 @@ import net.frozenorb.apiv3.route.bannedCellCarriers.GETBannedCellCarriers; import net.frozenorb.apiv3.route.bannedCellCarriers.GETBannedCellCarriersId; import net.frozenorb.apiv3.route.bannedCellCarriers.POSTBannedCellCarriers; import net.frozenorb.apiv3.route.chatFilterList.GETChatFilter; +import net.frozenorb.apiv3.route.disposableLoginTokens.POSTDisposableLoginTokens; +import net.frozenorb.apiv3.route.disposableLoginTokens.POSTDisposableLoginTokensIdUse; import net.frozenorb.apiv3.route.emailTokens.GETEmailTokensIdOwner; import net.frozenorb.apiv3.route.emailTokens.POSTEmailTokensIdConfirm; import net.frozenorb.apiv3.route.grants.DELETEGrantsId; @@ -306,6 +308,9 @@ public final class APIv3 extends AbstractVerticle { http.get("/chatFilter").handler(new GETChatFilter()); + http.post("/disposableLoginTokens").blockingHandler(new POSTDisposableLoginTokens(), false); + http.post("/disposableLoginTokens/:disposableLoginToken/use").blockingHandler(new POSTDisposableLoginTokensIdUse(), false); + http.get("/emailTokens/:emailToken/owner").blockingHandler(new GETEmailTokensIdOwner(), false); http.post("/emailTokens/:emailToken/confirm").blockingHandler(new POSTEmailTokensIdConfirm(), false); diff --git a/src/main/java/net/frozenorb/apiv3/auditLog/AuditLogActionType.java b/src/main/java/net/frozenorb/apiv3/auditLog/AuditLogActionType.java index f6023d1..9bbcb42 100644 --- a/src/main/java/net/frozenorb/apiv3/auditLog/AuditLogActionType.java +++ b/src/main/java/net/frozenorb/apiv3/auditLog/AuditLogActionType.java @@ -10,6 +10,8 @@ import java.time.Instant; public enum AuditLogActionType { // TODO + DISPOSABLE_LOGIN_TOKEN_USE(false), + DISPOSABLE_LOGIN_TOKEN_CREATE(false), ACCESS_TOKEN_CREATE(false), ACCESS_TOKEN_UPDATE(false), ACCESS_TOKEN_DELETE(false), diff --git a/src/main/java/net/frozenorb/apiv3/route/disposableLoginTokens/POSTDisposableLoginTokens.java b/src/main/java/net/frozenorb/apiv3/route/disposableLoginTokens/POSTDisposableLoginTokens.java new file mode 100644 index 0000000..313fcd0 --- /dev/null +++ b/src/main/java/net/frozenorb/apiv3/route/disposableLoginTokens/POSTDisposableLoginTokens.java @@ -0,0 +1,43 @@ +package net.frozenorb.apiv3.route.disposableLoginTokens; + +import com.google.common.collect.ImmutableMap; +import io.vertx.core.Handler; +import io.vertx.core.json.JsonObject; +import io.vertx.ext.web.RoutingContext; +import net.frozenorb.apiv3.APIv3; +import net.frozenorb.apiv3.model.User; +import net.frozenorb.apiv3.util.DisposableLoginTokenUtils; +import net.frozenorb.apiv3.util.ErrorUtils; +import net.frozenorb.apiv3.util.IpUtils; +import net.frozenorb.apiv3.util.SyncUtils; + +public final class POSTDisposableLoginTokens implements Handler { + + public void handle(RoutingContext ctx) { + JsonObject requestBody = ctx.getBodyAsJson(); + User user = SyncUtils.runBlocking(v -> User.findById(requestBody.getString("user"), v)); + String userIp = requestBody.getString("userIp"); + + if (user == null) { + ErrorUtils.respondNotFound(ctx, "User", requestBody.getString("user")); + return; + } + + if (IpUtils.isValidIp(userIp)) { + ErrorUtils.respondInvalidInput(ctx, "Ip address \"" + userIp + "\" is not valid."); + return; + } + + DisposableLoginTokenUtils.createToken(user.getId(), userIp, (token, error) -> { + if (error != null) { + ErrorUtils.respondInternalError(ctx, error); + } else { + APIv3.respondJson(ctx, 200, ImmutableMap.of( + "success", true, + "token", token + )); + } + }); + } + +} \ No newline at end of file diff --git a/src/main/java/net/frozenorb/apiv3/route/disposableLoginTokens/POSTDisposableLoginTokensIdUse.java b/src/main/java/net/frozenorb/apiv3/route/disposableLoginTokens/POSTDisposableLoginTokensIdUse.java new file mode 100644 index 0000000..5fc120e --- /dev/null +++ b/src/main/java/net/frozenorb/apiv3/route/disposableLoginTokens/POSTDisposableLoginTokensIdUse.java @@ -0,0 +1,55 @@ +package net.frozenorb.apiv3.route.disposableLoginTokens; + +import com.google.common.collect.ImmutableMap; +import io.vertx.core.Handler; +import io.vertx.core.json.JsonObject; +import io.vertx.ext.web.RoutingContext; +import net.frozenorb.apiv3.APIv3; +import net.frozenorb.apiv3.auditLog.AuditLog; +import net.frozenorb.apiv3.auditLog.AuditLogActionType; +import net.frozenorb.apiv3.util.*; + +public final class POSTDisposableLoginTokensIdUse implements Handler { + + public void handle(RoutingContext ctx) { + JsonObject requestBody = ctx.getBodyAsJson(); + String disposableLoginToken = ctx.request().getParam("disposableLoginToken"); + String userIp = requestBody.getString("userip"); + + if (disposableLoginToken == null || disposableLoginToken.isEmpty()) { + ErrorUtils.respondRequiredInput(ctx, "disposableLoginToken"); + return; + } + + if (!IpUtils.isValidIp(userIp)) { + ErrorUtils.respondInvalidInput(ctx, "Ip address \"" + userIp + "\" is not valid."); + return; + } + + DisposableLoginTokenUtils.useToken(disposableLoginToken, userIp, (user, error) -> { + if (error != null) { + ErrorUtils.respondInternalError(ctx, error); + return; + } + + if (user == null) { + ErrorUtils.respondOther(ctx, 409, "Disposable login token provided is not valid", "disposableLoginTokenNotValid", ImmutableMap.of()); + return; + } + + AuditLog.log(user.getId(), userIp, ctx, AuditLogActionType.DISPOSABLE_LOGIN_TOKEN_USE, (ignored, error2) -> { + if (error2 != null) { + ErrorUtils.respondInternalError(ctx, error2); + return; + } + + APIv3.respondJson(ctx, 200, ImmutableMap.of( + "authorized", true, + "uuid", user.getId(), + "session", SyncUtils.runBlocking(v -> UserSessionUtils.createSession(user.getId(), userIp, v)) + )); + }); + }); + } + +} \ No newline at end of file diff --git a/src/main/java/net/frozenorb/apiv3/util/DisposableLoginTokenUtils.java b/src/main/java/net/frozenorb/apiv3/util/DisposableLoginTokenUtils.java new file mode 100644 index 0000000..ea2de2a --- /dev/null +++ b/src/main/java/net/frozenorb/apiv3/util/DisposableLoginTokenUtils.java @@ -0,0 +1,68 @@ +package net.frozenorb.apiv3.util; + +import com.mongodb.async.SingleResultCallback; +import io.vertx.redis.RedisClient; +import io.vertx.redis.RedisOptions; +import lombok.experimental.UtilityClass; +import net.frozenorb.apiv3.APIv3; +import net.frozenorb.apiv3.model.User; + +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +@UtilityClass +public class DisposableLoginTokenUtils { + + private static final RedisClient redisClient = RedisClient.create(APIv3.getVertxInstance(), + new RedisOptions() + .setAddress(APIv3.getConfig().getProperty("redis.address")) + .setPort(Integer.parseInt(APIv3.getConfig().getProperty("redis.port"))) + ); + + public static void useToken(String token, String userIp, SingleResultCallback callback) { + if (token == null || token.isEmpty()) { + callback.onResult(null, null); + return; + } + + redisClient.get("apiv3:disposableLoginTokens:" + userIp + ":" + token, (result) -> { + if (result.failed()) { + callback.onResult(null, result.cause()); + return; + } + + if (result.result() == null) { + callback.onResult(null, null); + return; + } + + User.findById(result.result(), (user, error) -> { + if (error != null) { + callback.onResult(null, error); + return; + } + + redisClient.del("apiv3:disposableLoginTokens:" + userIp + ":" + token, (result2) -> { + if (result2.failed()) { + callback.onResult(null, result2.cause()); + } else { + callback.onResult(user, null); + } + }); + }); + }); + } + + public static void createToken(UUID user, String userIp, SingleResultCallback callback) { + String token = UUID.randomUUID().toString().replaceAll("-", ""); + + redisClient.setex("apiv3:disposableLoginTokens:" + userIp + ":" + token, TimeUnit.MINUTES.toSeconds(5), user.toString(), (result) -> { + if (result.succeeded()) { + callback.onResult(token, null); + } else { + callback.onResult(null, result.cause()); + } + }); + } + +} \ No newline at end of file