diff --git a/src/main/java/net/frozenorb/apiv3/APIv3.java b/src/main/java/net/frozenorb/apiv3/APIv3.java index 5cb0bdf..f4e7ffb 100644 --- a/src/main/java/net/frozenorb/apiv3/APIv3.java +++ b/src/main/java/net/frozenorb/apiv3/APIv3.java @@ -71,6 +71,7 @@ import net.frozenorb.apiv3.route.notificationTemplates.DELETENotificationTemplat import net.frozenorb.apiv3.route.notificationTemplates.GETNotificationTemplates; import net.frozenorb.apiv3.route.notificationTemplates.GETNotificationTemplatesId; import net.frozenorb.apiv3.route.notificationTemplates.POSTNotificationTemplates; +import net.frozenorb.apiv3.route.phoneIntel.GETPhoneInteld; import net.frozenorb.apiv3.route.punishments.*; import net.frozenorb.apiv3.route.ranks.DELETERanksId; import net.frozenorb.apiv3.route.ranks.GETRanks; @@ -299,6 +300,8 @@ public final class APIv3 extends AbstractVerticle { //http.put("/notificationTemplates/:id").blockingHandler(new PUTNotificationTemplatesId(), false); http.delete("/notificationTemplates/:id").blockingHandler(new DELETENotificationTemplatesId(), false); + http.get("/phoneIntel/:id").handler(new GETPhoneInteld()); + http.get("/punishments/:id").handler(new GETPunishmentsId()); http.get("/punishments").handler(new GETPunishments()); http.post("/punishments").blockingHandler(new POSTPunishments(), false); diff --git a/src/main/java/net/frozenorb/apiv3/model/PhoneIntel.java b/src/main/java/net/frozenorb/apiv3/model/PhoneIntel.java new file mode 100644 index 0000000..6203bc5 --- /dev/null +++ b/src/main/java/net/frozenorb/apiv3/model/PhoneIntel.java @@ -0,0 +1,70 @@ +package net.frozenorb.apiv3.model; + +import com.mongodb.async.SingleResultCallback; +import com.mongodb.async.client.MongoCollection; +import fr.javatic.mongo.jacksonCodec.Entity; +import fr.javatic.mongo.jacksonCodec.objectId.Id; +import lombok.AllArgsConstructor; +import lombok.Getter; +import net.frozenorb.apiv3.APIv3; +import net.frozenorb.apiv3.util.ZangUtils; +import net.frozenorb.apiv3.zang.ZangResult; +import org.bson.Document; + +import java.time.Instant; +import java.util.LinkedList; +import java.util.List; + +@Entity +@AllArgsConstructor +public final class PhoneIntel { + + private static final MongoCollection phoneIntelCollection = APIv3.getDatabase().getCollection("phoneIntel", PhoneIntel.class); + + @Getter @Id private String id; + @Getter private Instant lastUpdatedAt; + @Getter private ZangResult result; + + public static void findAll(SingleResultCallback> callback) { + phoneIntelCollection.find().sort(new Document("lastSeenAt", -1)).into(new LinkedList<>(), callback); + } + + public static void findById(String id, SingleResultCallback callback) { + phoneIntelCollection.find(new Document("_id", id)).first(callback); + } + + public static void findOrCreateById(String id, SingleResultCallback callback) { + findById(id, (existingPhoneIntel, error) -> { + if (error != null) { + callback.onResult(null, error); + } else if (existingPhoneIntel != null) { + callback.onResult(existingPhoneIntel, null); + } else { + ZangUtils.getCarrierInfo(id, (zangResult, error2) -> { + if (error2 != null) { + callback.onResult(null, error2); + } else { + PhoneIntel newPhoneIntel = new PhoneIntel(id, zangResult); + + phoneIntelCollection.insertOne(newPhoneIntel, (ignored, error3) -> { + if (error3 != null) { + callback.onResult(null, error3); + } else { + callback.onResult(newPhoneIntel, null); + } + }); + } + }); + } + }); + } + + private PhoneIntel() {} // For Jackson + + private PhoneIntel(String phoneNumber, ZangResult result) { + this.id = phoneNumber; + this.lastUpdatedAt = Instant.now(); + this.result = result; + } + +} \ No newline at end of file diff --git a/src/main/java/net/frozenorb/apiv3/model/User.java b/src/main/java/net/frozenorb/apiv3/model/User.java index b4594f0..405c8d5 100644 --- a/src/main/java/net/frozenorb/apiv3/model/User.java +++ b/src/main/java/net/frozenorb/apiv3/model/User.java @@ -486,16 +486,16 @@ public final class User { public void startPhoneRegistration(String phoneNumber) { this.pendingPhone = phoneNumber; - this.pendingEmailToken = UUID.randomUUID().toString().replace("-", ""); - this.pendingEmailTokenSetAt = Instant.now(); + this.pendingPhoneToken = String.valueOf(new Random().nextInt(999999 - 100000) + 100000); + this.pendingPhoneTokenSetAt = Instant.now(); } - public void completeRegistration(String email) { - this.email = email; - this.registeredAt = Instant.now(); - this.pendingEmail = null; - this.pendingEmailToken = null; - this.pendingEmailTokenSetAt = null; + public void completePhoneRegistration(String phoneNumber) { + this.phone = phoneNumber; + this.phoneRegisteredAt = Instant.now(); + this.pendingPhone = null; + this.pendingPhoneToken = null; + this.pendingPhoneTokenSetAt = null; } public void hasPermissionAnywhere(String permission, SingleResultCallback callback) { diff --git a/src/main/java/net/frozenorb/apiv3/route/emailTokens/POSTEmailTokensIdConfirm.java b/src/main/java/net/frozenorb/apiv3/route/emailTokens/POSTEmailTokensIdConfirm.java index 8d998c3..0f1d29a 100644 --- a/src/main/java/net/frozenorb/apiv3/route/emailTokens/POSTEmailTokensIdConfirm.java +++ b/src/main/java/net/frozenorb/apiv3/route/emailTokens/POSTEmailTokensIdConfirm.java @@ -26,9 +26,7 @@ public final class POSTEmailTokensIdConfirm implements Handler { return; } - // We can't check email != null as that's set while we're pending - // confirmation, we have to check the token. - if (user.getPendingEmailToken() == null) { + if (user.getEmail() != null) { ErrorUtils.respondGeneric(ctx, 400, "User provided already has email set."); return; } diff --git a/src/main/java/net/frozenorb/apiv3/route/phoneIntel/GETPhoneInteld.java b/src/main/java/net/frozenorb/apiv3/route/phoneIntel/GETPhoneInteld.java new file mode 100644 index 0000000..15b5a67 --- /dev/null +++ b/src/main/java/net/frozenorb/apiv3/route/phoneIntel/GETPhoneInteld.java @@ -0,0 +1,29 @@ +package net.frozenorb.apiv3.route.phoneIntel; + +import io.vertx.core.Handler; +import io.vertx.ext.web.RoutingContext; +import net.frozenorb.apiv3.APIv3; +import net.frozenorb.apiv3.model.PhoneIntel; +import net.frozenorb.apiv3.util.ErrorUtils; +import net.frozenorb.apiv3.util.PhoneUtils; + +public final class GETPhoneInteld implements Handler { + + public void handle(RoutingContext ctx) { + String phoneNumber = ctx.request().getParam("id"); + + if (!PhoneUtils.isValidPhone(phoneNumber)) { + ErrorUtils.respondInvalidInput(ctx, "Phone number \"" + phoneNumber + "\" is not valid."); + return; + } + + PhoneIntel.findOrCreateById(phoneNumber, (ipIntel, error) -> { + if (error != null) { + ErrorUtils.respondInternalError(ctx, error); + } else { + APIv3.respondJson(ctx, ipIntel); + } + }); + } + +} \ No newline at end of file diff --git a/src/main/java/net/frozenorb/apiv3/route/users/POSTUsersIdConfirmPhone.java b/src/main/java/net/frozenorb/apiv3/route/users/POSTUsersIdConfirmPhone.java new file mode 100644 index 0000000..d81b601 --- /dev/null +++ b/src/main/java/net/frozenorb/apiv3/route/users/POSTUsersIdConfirmPhone.java @@ -0,0 +1,68 @@ +package net.frozenorb.apiv3.route.users; + +import com.google.common.collect.ImmutableMap; +import com.mongodb.client.result.UpdateResult; +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.model.User; +import net.frozenorb.apiv3.unsorted.BlockingCallback; +import net.frozenorb.apiv3.util.ErrorUtils; + +import java.util.concurrent.TimeUnit; + +public final class POSTUsersIdConfirmPhone implements Handler { + + public void handle(RoutingContext ctx) { + BlockingCallback userCallback = new BlockingCallback<>(); + User.findById(ctx.request().getParam("id"), userCallback); + User user = userCallback.get(); + + if (user == null) { + ErrorUtils.respondNotFound(ctx, "User", ctx.request().getParam("id")); + return; + } + + if (user.getPhone() != null) { + ErrorUtils.respondInvalidInput(ctx, "User provided already has a confirmed phone number."); + return; + } + + if (user.getPendingPhoneToken() == null) { + ErrorUtils.respondInvalidInput(ctx, "User provided already hasn't started confirming a phone number."); + return; + } + + JsonObject requestBody = ctx.getBodyAsJson(); + int phoneCode = requestBody.getInteger("phoneCode"); + + if ((System.currentTimeMillis() - user.getPendingPhoneTokenSetAt().toEpochMilli()) > TimeUnit.MINUTES.toMillis(20)) { + ErrorUtils.respondInvalidInput(ctx, "Phone token is expired"); + return; + } + + if (!String.valueOf(phoneCode).equals(user.getPendingPhoneToken())) { + ErrorUtils.respondInvalidInput(ctx, "Phone token doesn't match"); + return; + } + + user.completePhoneRegistration(user.getPendingPhone()); + BlockingCallback callback = new BlockingCallback<>(); + user.save(callback); + callback.get(); + + AuditLog.log(user.getId(), requestBody.getString("userIp"), ctx, AuditLogActionType.USER_CONFIRM_PHONE, (ignored, error) -> { + if (error != null) { + ErrorUtils.respondInternalError(ctx, error); + } else { + APIv3.respondJson(ctx, ImmutableMap.of( + "success", true + )); + } + }); + } + +} \ No newline at end of file diff --git a/src/main/java/net/frozenorb/apiv3/route/users/POSTUsersIdRegisterPhone.java b/src/main/java/net/frozenorb/apiv3/route/users/POSTUsersIdRegisterPhone.java index 6185fee..b367783 100644 --- a/src/main/java/net/frozenorb/apiv3/route/users/POSTUsersIdRegisterPhone.java +++ b/src/main/java/net/frozenorb/apiv3/route/users/POSTUsersIdRegisterPhone.java @@ -8,12 +8,15 @@ 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.model.BannedCellCarrier; import net.frozenorb.apiv3.model.NotificationTemplate; +import net.frozenorb.apiv3.model.PhoneIntel; import net.frozenorb.apiv3.model.User; import net.frozenorb.apiv3.unsorted.BlockingCallback; import net.frozenorb.apiv3.unsorted.Notification; import net.frozenorb.apiv3.util.ErrorUtils; import net.frozenorb.apiv3.util.PhoneUtils; +import net.frozenorb.apiv3.zang.ZangResult; import java.util.Map; import java.util.concurrent.TimeUnit; @@ -43,8 +46,8 @@ public final class POSTUsersIdRegisterPhone implements Handler { return; } - if (user.getPendingPhoneToken() != null && (System.currentTimeMillis() - user.getPendingPhoneTokenSetAt().toEpochMilli()) < TimeUnit.DAYS.toMillis(2)) { - ErrorUtils.respondGeneric(ctx, 200, "We just recently sent you a confirmation code. Please wait before trying to register again."); + if (user.getPendingPhoneToken() != null && (System.currentTimeMillis() - user.getPendingPhoneTokenSetAt().toEpochMilli()) < TimeUnit.MINUTES.toMillis(20)) { + ErrorUtils.respondGeneric(ctx, 200, "We just recently sent you a confirmation code. Please wait before trying to register your phone again."); return; } @@ -56,6 +59,15 @@ public final class POSTUsersIdRegisterPhone implements Handler { return; } + BlockingCallback phoneIntelCallback = new BlockingCallback<>(); + PhoneIntel.findOrCreateById(phone, phoneIntelCallback); + PhoneIntel phoneIntel = phoneIntelCallback.get(); + + if (BannedCellCarrier.findById(phoneIntel.getResult().getCarrierId()) != null) { + ErrorUtils.respondInvalidInput(ctx, phone + " is from a banned cell provider."); + return; + } + user.startPhoneRegistration(phone); BlockingCallback callback = new BlockingCallback<>(); user.save(callback);