More work on Zang phone registration

This commit is contained in:
Colin McDonald 2016-06-26 22:24:06 -04:00
parent 9f4089a752
commit 1532bee954
9 changed files with 153 additions and 17 deletions

View File

@ -51,8 +51,8 @@ import net.frozenorb.apiv3.route.bannedCellCarriers.GETBannedCellCarriers;
import net.frozenorb.apiv3.route.bannedCellCarriers.GETBannedCellCarriersId; import net.frozenorb.apiv3.route.bannedCellCarriers.GETBannedCellCarriersId;
import net.frozenorb.apiv3.route.bannedCellCarriers.POSTBannedCellCarriers; import net.frozenorb.apiv3.route.bannedCellCarriers.POSTBannedCellCarriers;
import net.frozenorb.apiv3.route.chatFilterList.GETChatFilter; import net.frozenorb.apiv3.route.chatFilterList.GETChatFilter;
import net.frozenorb.apiv3.route.emailToken.GETEmailTokensIdOwner; import net.frozenorb.apiv3.route.emailTokens.GETEmailTokensIdOwner;
import net.frozenorb.apiv3.route.emailToken.POSTEmailTokensIdConfirm; import net.frozenorb.apiv3.route.emailTokens.POSTEmailTokensIdConfirm;
import net.frozenorb.apiv3.route.grants.DELETEGrantsId; import net.frozenorb.apiv3.route.grants.DELETEGrantsId;
import net.frozenorb.apiv3.route.grants.GETGrants; import net.frozenorb.apiv3.route.grants.GETGrants;
import net.frozenorb.apiv3.route.grants.GETGrantsId; import net.frozenorb.apiv3.route.grants.GETGrantsId;
@ -85,6 +85,7 @@ import net.frozenorb.apiv3.serialization.jackson.InstantJsonSerializer;
import net.frozenorb.apiv3.serialization.jackson.UuidJsonDeserializer; import net.frozenorb.apiv3.serialization.jackson.UuidJsonDeserializer;
import net.frozenorb.apiv3.serialization.jackson.UuidJsonSerializer; import net.frozenorb.apiv3.serialization.jackson.UuidJsonSerializer;
import net.frozenorb.apiv3.serialization.mongodb.UuidCodecProvider; import net.frozenorb.apiv3.serialization.mongodb.UuidCodecProvider;
import net.frozenorb.apiv3.util.EmailUtils;
import org.bson.Document; import org.bson.Document;
import org.bson.codecs.BsonValueCodecProvider; import org.bson.codecs.BsonValueCodecProvider;
import org.bson.codecs.DocumentCodecProvider; import org.bson.codecs.DocumentCodecProvider;
@ -205,6 +206,7 @@ public final class APIv3 extends AbstractVerticle {
Rank.updateCache(); Rank.updateCache();
Server.updateCache(); Server.updateCache();
ServerGroup.updateCache(); ServerGroup.updateCache();
EmailUtils.updateBannedEmailDomains();
} }
private ObjectMapper createMongoJacksonMapper() { private ObjectMapper createMongoJacksonMapper() {
@ -324,7 +326,8 @@ public final class APIv3 extends AbstractVerticle {
http.post("/users/:id/login").blockingHandler(new POSTUsersIdLogin()); http.post("/users/:id/login").blockingHandler(new POSTUsersIdLogin());
http.post("/users/:id/notify").blockingHandler(new POSTUsersIdNotify(), false); http.post("/users/:id/notify").blockingHandler(new POSTUsersIdNotify(), false);
http.post("/users/:id/passwordReset").blockingHandler(new POSTUsersIdPasswordReset(), false); http.post("/users/:id/passwordReset").blockingHandler(new POSTUsersIdPasswordReset(), false);
http.post("/users/:id/register").blockingHandler(new POSTUsersIdRegister(), false); http.post("/users/:id/registerEmail").blockingHandler(new POSTUsersIdRegisterEmail(), false);
http.post("/users/:id/registerPhone").blockingHandler(new POSTUsersIdRegisterPhone(), false);
http.post("/users/:id/setupTotp").blockingHandler(new POSTUsersIdSetupTotp(), false); http.post("/users/:id/setupTotp").blockingHandler(new POSTUsersIdSetupTotp(), false);
http.post("/users/:id/verifyTotp").handler(new POSTUsersIdVerifyTotp()); http.post("/users/:id/verifyTotp").handler(new POSTUsersIdVerifyTotp());

View File

@ -36,8 +36,10 @@ public enum AuditLogActionType {
SERVER_DELETE(false), SERVER_DELETE(false),
USER_CHANGE_PASSWORD(false), USER_CHANGE_PASSWORD(false),
USER_PASSWORD_RESET(false), USER_PASSWORD_RESET(false),
USER_REGISTER(false), USER_REGISTER_EMAIL(false),
USER_REGISTER_PHONE(false),
USER_CONFIRM_EMAIL(false), USER_CONFIRM_EMAIL(false),
USER_CONFIRM_PHONE(false),
USER_SETUP_TOTP(false), USER_SETUP_TOTP(false),
USER_VERIFY_TOTP(false); USER_VERIFY_TOTP(false);

View File

@ -55,7 +55,11 @@ public final class User {
@Getter @ExcludeFromReplies private String pendingEmail; @Getter @ExcludeFromReplies private String pendingEmail;
@Getter @ExcludeFromReplies private String pendingEmailToken; @Getter @ExcludeFromReplies private String pendingEmailToken;
@Getter @ExcludeFromReplies private Instant pendingEmailTokenSetAt; @Getter @ExcludeFromReplies private Instant pendingEmailTokenSetAt;
@Getter private String phoneNumber; @Getter private String phone;
@Getter private Instant phoneRegisteredAt;
@Getter @ExcludeFromReplies private String pendingPhone;
@Getter @ExcludeFromReplies private String pendingPhoneToken;
@Getter @ExcludeFromReplies private Instant pendingPhoneTokenSetAt;
@Getter private String lastSeenOn; @Getter private String lastSeenOn;
@Getter private Instant lastSeenAt; @Getter private Instant lastSeenAt;
@Getter private Instant firstSeenAt; @Getter private Instant firstSeenAt;
@ -78,6 +82,10 @@ public final class User {
} }
} }
public static void findByPhone(String phoneNumber, SingleResultCallback<User> callback) {
usersCollection.find(new Document("phone", phoneNumber)).first(callback);
}
public static void findByEmail(String email, SingleResultCallback<User> callback) { public static void findByEmail(String email, SingleResultCallback<User> callback) {
usersCollection.find(new Document("email", email)).first(callback); usersCollection.find(new Document("email", email)).first(callback);
} }
@ -460,7 +468,13 @@ public final class User {
}); });
} }
public void completeRegistration(String email) { public void startEmailRegistration(String pendingEmail) {
this.pendingEmail = pendingEmail;
this.pendingEmailToken = UUID.randomUUID().toString().replace("-", "");
this.pendingEmailTokenSetAt = Instant.now();
}
public void completeEmailRegistration(String email) {
this.email = email; this.email = email;
this.registeredAt = Instant.now(); this.registeredAt = Instant.now();
this.pendingEmail = null; this.pendingEmail = null;
@ -468,12 +482,20 @@ public final class User {
this.pendingEmailTokenSetAt = null; this.pendingEmailTokenSetAt = null;
} }
public void startRegistration(String pendingEmail) { public void startPhoneRegistration(String phoneNumber) {
this.pendingEmail = pendingEmail; this.pendingPhone = phoneNumber;
this.pendingEmailToken = UUID.randomUUID().toString().replace("-", ""); this.pendingEmailToken = UUID.randomUUID().toString().replace("-", "");
this.pendingEmailTokenSetAt = Instant.now(); this.pendingEmailTokenSetAt = 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 hasPermissionAnywhere(String permission, SingleResultCallback<Boolean> callback) { public void hasPermissionAnywhere(String permission, SingleResultCallback<Boolean> callback) {
getCompoundedPermissions((permissions, error) -> { getCompoundedPermissions((permissions, error) -> {
if (error != null) { if (error != null) {

View File

@ -1,4 +1,4 @@
package net.frozenorb.apiv3.route.emailToken; package net.frozenorb.apiv3.route.emailTokens;
import io.vertx.core.Handler; import io.vertx.core.Handler;
import io.vertx.ext.web.RoutingContext; import io.vertx.ext.web.RoutingContext;

View File

@ -1,4 +1,4 @@
package net.frozenorb.apiv3.route.emailToken; package net.frozenorb.apiv3.route.emailTokens;
import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMap;
import com.mongodb.client.result.UpdateResult; import com.mongodb.client.result.UpdateResult;
@ -46,7 +46,7 @@ public final class POSTEmailTokensIdConfirm implements Handler<RoutingContext> {
return; return;
} }
user.completeRegistration(user.getPendingEmail()); user.completeEmailRegistration(user.getPendingEmail());
user.updatePassword(password); user.updatePassword(password);
BlockingCallback<UpdateResult> callback = new BlockingCallback<>(); BlockingCallback<UpdateResult> callback = new BlockingCallback<>();
user.save(callback); user.save(callback);

View File

@ -18,7 +18,7 @@ import net.frozenorb.apiv3.util.ErrorUtils;
import java.util.Map; import java.util.Map;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
public final class POSTUsersIdRegister implements Handler<RoutingContext> { public final class POSTUsersIdRegisterEmail implements Handler<RoutingContext> {
public void handle(RoutingContext ctx) { public void handle(RoutingContext ctx) {
BlockingCallback<User> userCallback = new BlockingCallback<>(); BlockingCallback<User> userCallback = new BlockingCallback<>();
@ -61,7 +61,7 @@ public final class POSTUsersIdRegister implements Handler<RoutingContext> {
return; return;
} }
user.startRegistration(email); user.startEmailRegistration(email);
BlockingCallback<UpdateResult> callback = new BlockingCallback<>(); BlockingCallback<UpdateResult> callback = new BlockingCallback<>();
user.save(callback); user.save(callback);
callback.get(); callback.get();
@ -80,7 +80,7 @@ public final class POSTUsersIdRegister implements Handler<RoutingContext> {
if (error != null) { if (error != null) {
ErrorUtils.respondInternalError(ctx, error); ErrorUtils.respondInternalError(ctx, error);
} else { } else {
AuditLog.log(user.getId(), requestBody.getString("userIp"), ctx, AuditLogActionType.USER_REGISTER, (ignored2, error2) -> { AuditLog.log(user.getId(), requestBody.getString("userIp"), ctx, AuditLogActionType.USER_REGISTER_EMAIL, (ignored2, error2) -> {
if (error2 != null) { if (error2 != null) {
ErrorUtils.respondInternalError(ctx, error2); ErrorUtils.respondInternalError(ctx, error2);
} else { } else {

View File

@ -0,0 +1,91 @@
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.NotificationTemplate;
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 java.util.Map;
import java.util.concurrent.TimeUnit;
public final class POSTUsersIdRegisterPhone implements Handler<RoutingContext> {
public void handle(RoutingContext ctx) {
BlockingCallback<User> 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 phone set.");
return;
}
JsonObject requestBody = ctx.getBodyAsJson();
String phone = requestBody.getString("phone");
if (!PhoneUtils.isValidPhone(phone)) {
ErrorUtils.respondInvalidInput(ctx, phone + " is not a valid phone number.");
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.");
return;
}
BlockingCallback<User> samePhoneCallback = new BlockingCallback<>();
User.findByPhone(phone, samePhoneCallback);
if (samePhoneCallback.get() != null) {
ErrorUtils.respondInvalidInput(ctx, phone + " is already in use.");
return;
}
user.startPhoneRegistration(phone);
BlockingCallback<UpdateResult> callback = new BlockingCallback<>();
user.save(callback);
callback.get();
Map<String, Object> replacements = ImmutableMap.of(
"username", user.getLastUsername(),
"phone", user.getPendingPhone(),
"phoneToken", user.getPendingPhoneToken()
);
BlockingCallback<NotificationTemplate> notificationTemplateCallback = new BlockingCallback<>();
NotificationTemplate.findById("phone-confirmation", notificationTemplateCallback);
Notification notification = new Notification(notificationTemplateCallback.get(), replacements, replacements);
notification.sendAsText(user.getPendingPhone(), (ignored, error) -> {
if (error != null) {
ErrorUtils.respondInternalError(ctx, error);
} else {
AuditLog.log(user.getId(), requestBody.getString("userIp"), ctx, AuditLogActionType.USER_REGISTER_PHONE, (ignored2, error2) -> {
if (error2 != null) {
ErrorUtils.respondInternalError(ctx, error2);
} else {
APIv3.respondJson(ctx, ImmutableMap.of(
"success", true
));
}
});
}
});
}
}

View File

@ -11,7 +11,6 @@ import java.util.Set;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern; import java.util.regex.Pattern;
@Slf4j
@UtilityClass @UtilityClass
public class EmailUtils { public class EmailUtils {
@ -23,11 +22,10 @@ public class EmailUtils {
private static Set<String> bannedEmailDomains = ImmutableSet.of(); private static Set<String> bannedEmailDomains = ImmutableSet.of();
static { static {
updateBannedEmailDomains();
APIv3.getVertxInstance().setPeriodic(TimeUnit.MINUTES.toMillis(10), (id) -> updateBannedEmailDomains()); APIv3.getVertxInstance().setPeriodic(TimeUnit.MINUTES.toMillis(10), (id) -> updateBannedEmailDomains());
} }
private static void updateBannedEmailDomains() { public static void updateBannedEmailDomains() {
httpsClient.get(443, "raw.githubusercontent.com", "/martenson/disposable-email-domains/master/disposable_email_blacklist.conf", (response) -> { httpsClient.get(443, "raw.githubusercontent.com", "/martenson/disposable-email-domains/master/disposable_email_blacklist.conf", (response) -> {
response.bodyHandler((body) -> bannedEmailDomains = ImmutableSet.copyOf(body.toString().split("\n"))); response.bodyHandler((body) -> bannedEmailDomains = ImmutableSet.copyOf(body.toString().split("\n")));
response.exceptionHandler(Throwable::printStackTrace); response.exceptionHandler(Throwable::printStackTrace);

View File

@ -0,0 +1,20 @@
package net.frozenorb.apiv3.util;
import lombok.experimental.UtilityClass;
import java.util.regex.Pattern;
@UtilityClass
public class PhoneUtils {
private static final Pattern VALID_PHONE_PATTERN = Pattern.compile(
"^[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,6}$",
Pattern.CASE_INSENSITIVE
);
public static boolean isValidPhone(String phoneNumber) {
return true; // TODO
//return VALID_EMAIL_PATTERN.matcher(email).matches();
}
}