From 799a8de3866a3db9d63d4d47dde78d402e89adac Mon Sep 17 00:00:00 2001 From: Colin McDonald Date: Mon, 2 May 2016 19:34:30 -0400 Subject: [PATCH] Push code for griffin --- pom.xml | 5 +++ src/main/java/net/frozenorb/apiv3/APIv3.java | 6 ++-- .../frozenorb/apiv3/auditLog/AuditLog.java | 4 +-- .../apiv3/auditLog/AuditLogActionType.java | 11 ++++-- .../apiv3/filters/ActorAttributeFilter.java | 9 +++-- .../apiv3/filters/AuthorizationFilter.java | 3 +- .../frozenorb/apiv3/models/AuditLogEntry.java | 6 ++-- .../net/frozenorb/apiv3/models/Grant.java | 7 ++-- .../frozenorb/apiv3/models/IPLogEntry.java | 5 +-- .../frozenorb/apiv3/models/Punishment.java | 9 ++--- .../java/net/frozenorb/apiv3/models/Rank.java | 3 +- .../net/frozenorb/apiv3/models/Server.java | 3 +- .../java/net/frozenorb/apiv3/models/User.java | 9 ++--- .../frozenorb/apiv3/models/UserMetaEntry.java | 5 +-- .../net/frozenorb/apiv3/routes/GETWhoAmI.java | 1 - .../net/frozenorb/apiv3/routes/NotFound.java | 4 +-- .../apiv3/routes/grants/DELETEGrant.java | 6 ++-- .../apiv3/routes/grants/GETGrant.java | 1 - .../routes/punishments/DELETEPunishment.java | 6 ++-- .../apiv3/routes/users/GETUserVerifyTOTP.java | 31 ++++++++++++++++ .../routes/users/POSTUserConfirmRegister.java | 6 ++-- .../apiv3/routes/users/POSTUserNotify.java | 6 ++-- .../apiv3/routes/users/POSTUserRegister.java | 5 +-- .../apiv3/routes/users/POSTUserSetupTOTP.java | 36 +++++++++++++++++++ .../unsorted/LoggingExceptionHandler.java | 3 +- .../net/frozenorb/apiv3/utils/ErrorUtils.java | 19 ++++++---- .../net/frozenorb/apiv3/utils/TOTPUtils.java | 32 +++++++++++++++++ 27 files changed, 184 insertions(+), 57 deletions(-) create mode 100644 src/main/java/net/frozenorb/apiv3/routes/users/GETUserVerifyTOTP.java create mode 100644 src/main/java/net/frozenorb/apiv3/routes/users/POSTUserSetupTOTP.java create mode 100644 src/main/java/net/frozenorb/apiv3/utils/TOTPUtils.java diff --git a/pom.xml b/pom.xml index d89e8b3..b9edb8b 100644 --- a/pom.xml +++ b/pom.xml @@ -87,6 +87,11 @@ morphia-logging-slf4j 1.1.0 + + com.warrenstrange + googleauth + 0.5.0 + org.projectlombok lombok diff --git a/src/main/java/net/frozenorb/apiv3/APIv3.java b/src/main/java/net/frozenorb/apiv3/APIv3.java index 20f40e1..69d5e17 100644 --- a/src/main/java/net/frozenorb/apiv3/APIv3.java +++ b/src/main/java/net/frozenorb/apiv3/APIv3.java @@ -10,7 +10,6 @@ import lombok.Getter; import net.frozenorb.apiv3.filters.ActorAttributeFilter; import net.frozenorb.apiv3.filters.AuthorizationFilter; import net.frozenorb.apiv3.filters.ContentTypeFilter; -import net.frozenorb.apiv3.models.Server; import net.frozenorb.apiv3.routes.GETDump; import net.frozenorb.apiv3.routes.GETWhoAmI; import net.frozenorb.apiv3.routes.NotFound; @@ -45,7 +44,6 @@ import org.mongodb.morphia.Datastore; import org.mongodb.morphia.Morphia; import org.mongodb.morphia.logging.MorphiaLoggerFactory; import org.mongodb.morphia.logging.slf4j.SLF4JLoggerImplFactory; -import org.slf4j.impl.StaticLoggerBinder; import java.io.FileInputStream; import java.io.InputStream; @@ -57,7 +55,7 @@ public final class APIv3 { @Getter private static Datastore datastore; @Getter private static Properties config = new Properties(); - private final Gson gson = new GsonBuilder() + @Getter private static final Gson gson = new GsonBuilder() .registerTypeAdapter(ObjectId.class, new ObjectIdTypeAdapter()) .setExclusionStrategies(new FollowAnnotationExclusionStrategy()) .create(); @@ -152,12 +150,14 @@ public final class APIv3 { get("/user/:id/meta/:serverGroup", new GETUserMeta(), gson::toJson); get("/user/:id/grants", new GETUserGrants(), gson::toJson); get("/user/:id/ipLog", new GETUserIPLog(), gson::toJson); + get("/user/:id/verifyTOTP", new GETUserVerifyTOTP(), gson::toJson); get("/user/:id", new GETUser(), gson::toJson); post("/user/:id:/grant", new POSTUserGrant(), gson::toJson); post("/user/:id:/punish", new POSTUserPunish(), gson::toJson); post("/user/:id/loginInfo", new POSTUserLoginInfo(), gson::toJson); post("/user/:id/notify", new POSTUserNotify(), gson::toJson); post("/user/:id/register", new POSTUserRegister(), gson::toJson); + post("/user/:id/setupTOTP", new POSTUserSetupTOTP(), gson::toJson); post("/user/confirmRegister/:emailToken", new POSTUserConfirmRegister(), gson::toJson); put("/user/:id/meta/:serverGroup", new PUTUserMeta(), gson::toJson); delete("/user/:id/meta", new DELETEUserMeta(), gson::toJson); diff --git a/src/main/java/net/frozenorb/apiv3/auditLog/AuditLog.java b/src/main/java/net/frozenorb/apiv3/auditLog/AuditLog.java index 9ce6b07..0c4fca0 100644 --- a/src/main/java/net/frozenorb/apiv3/auditLog/AuditLog.java +++ b/src/main/java/net/frozenorb/apiv3/auditLog/AuditLog.java @@ -3,12 +3,10 @@ package net.frozenorb.apiv3.auditLog; import lombok.experimental.UtilityClass; import net.frozenorb.apiv3.APIv3; import net.frozenorb.apiv3.actors.Actor; -import net.frozenorb.apiv3.auditLog.actions.CreatePunishmentAction; import net.frozenorb.apiv3.models.AuditLogEntry; import net.frozenorb.apiv3.models.User; import org.bson.Document; -import java.util.HashMap; import java.util.Map; @UtilityClass @@ -18,7 +16,7 @@ public class AuditLog { log(performedBy, performedByIp, actor, actionType, new Document()); } - public static void log(User performedBy, String performedByIp, Actor actor, AuditLogActionType actionType, Document actionData) { + public static void log(User performedBy, String performedByIp, Actor actor, AuditLogActionType actionType, Map actionData) { APIv3.getDatastore().save(new AuditLogEntry(performedBy, performedByIp, actor, actionType, actionData)); } diff --git a/src/main/java/net/frozenorb/apiv3/auditLog/AuditLogActionType.java b/src/main/java/net/frozenorb/apiv3/auditLog/AuditLogActionType.java index 2786bc4..a661093 100644 --- a/src/main/java/net/frozenorb/apiv3/auditLog/AuditLogActionType.java +++ b/src/main/java/net/frozenorb/apiv3/auditLog/AuditLogActionType.java @@ -1,11 +1,18 @@ package net.frozenorb.apiv3.auditLog; -import lombok.Getter; import net.frozenorb.apiv3.models.AuditLogEntry; public enum AuditLogActionType { - CREATE_PUNISHMENT { + DELETE_PUNISHMENT { + + @Override + public void revert(AuditLogEntry entry) { + + } + + }, + DELETE_GRANT { @Override public void revert(AuditLogEntry entry) { diff --git a/src/main/java/net/frozenorb/apiv3/filters/ActorAttributeFilter.java b/src/main/java/net/frozenorb/apiv3/filters/ActorAttributeFilter.java index ed2bb19..6f66ac2 100644 --- a/src/main/java/net/frozenorb/apiv3/filters/ActorAttributeFilter.java +++ b/src/main/java/net/frozenorb/apiv3/filters/ActorAttributeFilter.java @@ -9,8 +9,7 @@ import net.frozenorb.apiv3.utils.ErrorUtils; import spark.Filter; import spark.Request; import spark.Response; - -import static spark.Spark.halt; +import spark.Spark; public final class ActorAttributeFilter implements Filter { @@ -40,7 +39,7 @@ public final class ActorAttributeFilter implements Filter { } } - halt(401, ErrorUtils.error("Failed to authorize.").toJson()); + Spark.halt(401, APIv3.getGson().toJson(ErrorUtils.error("Failed to authorize."))); return null; } @@ -61,7 +60,7 @@ public final class ActorAttributeFilter implements Filter { Server server = Server.byId(split[1]); if (server == null) { - halt(401, ErrorUtils.notFound("Server", split[1]).toJson()); + Spark.halt(401, APIv3.getGson().toJson(ErrorUtils.notFound("Server", split[1]))); } String givenKey = split[2]; @@ -73,7 +72,7 @@ public final class ActorAttributeFilter implements Filter { } } - halt(401, ErrorUtils.error("Failed to authorize.").toJson()); + Spark.halt(401, APIv3.getGson().toJson(ErrorUtils.error("Failed to authorize."))); return null; } diff --git a/src/main/java/net/frozenorb/apiv3/filters/AuthorizationFilter.java b/src/main/java/net/frozenorb/apiv3/filters/AuthorizationFilter.java index d961722..a20a17b 100644 --- a/src/main/java/net/frozenorb/apiv3/filters/AuthorizationFilter.java +++ b/src/main/java/net/frozenorb/apiv3/filters/AuthorizationFilter.java @@ -1,5 +1,6 @@ package net.frozenorb.apiv3.filters; +import net.frozenorb.apiv3.APIv3; import net.frozenorb.apiv3.actors.Actor; import net.frozenorb.apiv3.utils.ErrorUtils; import spark.Filter; @@ -13,7 +14,7 @@ public final class AuthorizationFilter implements Filter { Actor actor = req.attribute("actor"); if (!actor.isAuthorized()) { - Spark.halt(ErrorUtils.error("Unauthorized access: Please authenticate as either a server, the website, or an authorized user.").toJson()); + Spark.halt(APIv3.getGson().toJson(ErrorUtils.error("Unauthorized access: Please authenticate as either a server, the website, or an authorized user. You're currently authorized as " + actor.getName()))); } } diff --git a/src/main/java/net/frozenorb/apiv3/models/AuditLogEntry.java b/src/main/java/net/frozenorb/apiv3/models/AuditLogEntry.java index 90ae7bf..32d2d07 100644 --- a/src/main/java/net/frozenorb/apiv3/models/AuditLogEntry.java +++ b/src/main/java/net/frozenorb/apiv3/models/AuditLogEntry.java @@ -4,10 +4,10 @@ import com.google.common.collect.ImmutableMap; import lombok.Getter; import net.frozenorb.apiv3.actors.Actor; import net.frozenorb.apiv3.auditLog.AuditLogActionType; -import org.bson.Document; import org.bson.types.ObjectId; import org.mongodb.morphia.annotations.Entity; import org.mongodb.morphia.annotations.Id; +import org.mongodb.morphia.annotations.Indexed; import java.util.Date; import java.util.Map; @@ -17,9 +17,9 @@ import java.util.UUID; public final class AuditLogEntry { @Getter @Id private String id; - @Getter private UUID performedBy; + @Getter @Indexed private UUID performedBy; @Getter private String performedByIp; - @Getter private Date performedAt; + @Getter @Indexed private Date performedAt; @Getter private String actorName; @Getter private Actor.Type actorType; @Getter private AuditLogActionType actionType; diff --git a/src/main/java/net/frozenorb/apiv3/models/Grant.java b/src/main/java/net/frozenorb/apiv3/models/Grant.java index 41f848e..62ba897 100644 --- a/src/main/java/net/frozenorb/apiv3/models/Grant.java +++ b/src/main/java/net/frozenorb/apiv3/models/Grant.java @@ -6,6 +6,7 @@ import net.frozenorb.apiv3.APIv3; import org.bson.types.ObjectId; import org.mongodb.morphia.annotations.Entity; import org.mongodb.morphia.annotations.Id; +import org.mongodb.morphia.annotations.Indexed; import java.util.Date; import java.util.HashSet; @@ -16,14 +17,14 @@ import java.util.UUID; public final class Grant { @Getter @Id private String id; - @Getter private UUID target; + @Getter @Indexed private UUID target; @Getter private String reason; @Getter private Set scopes; - @Getter private String rank; + @Getter @Indexed private String rank; @Getter private Date expiresAt; @Getter private UUID addedBy; - @Getter private Date addedAt; + @Getter @Indexed private Date addedAt; @Getter private UUID removedBy; @Getter private Date removedAt; diff --git a/src/main/java/net/frozenorb/apiv3/models/IPLogEntry.java b/src/main/java/net/frozenorb/apiv3/models/IPLogEntry.java index 648119d..dc40569 100644 --- a/src/main/java/net/frozenorb/apiv3/models/IPLogEntry.java +++ b/src/main/java/net/frozenorb/apiv3/models/IPLogEntry.java @@ -5,6 +5,7 @@ import net.frozenorb.apiv3.APIv3; import org.bson.types.ObjectId; import org.mongodb.morphia.annotations.Entity; import org.mongodb.morphia.annotations.Id; +import org.mongodb.morphia.annotations.Indexed; import java.util.Date; import java.util.UUID; @@ -13,8 +14,8 @@ import java.util.UUID; public final class IPLogEntry { @Getter @Id private String id; - @Getter private UUID user; - @Getter private String ip; + @Getter @Indexed private UUID user; + @Getter @Indexed private String ip; @Getter private Date firstSeen; @Getter private Date lastSeen; @Getter private int uses; diff --git a/src/main/java/net/frozenorb/apiv3/models/Punishment.java b/src/main/java/net/frozenorb/apiv3/models/Punishment.java index 3936f2a..d827430 100644 --- a/src/main/java/net/frozenorb/apiv3/models/Punishment.java +++ b/src/main/java/net/frozenorb/apiv3/models/Punishment.java @@ -5,6 +5,7 @@ import net.frozenorb.apiv3.APIv3; import org.bson.types.ObjectId; import org.mongodb.morphia.annotations.Entity; import org.mongodb.morphia.annotations.Id; +import org.mongodb.morphia.annotations.Indexed; import java.util.Date; import java.util.UUID; @@ -13,14 +14,14 @@ import java.util.UUID; public final class Punishment { @Getter @Id private String id; - @Getter private UUID target; + @Getter @Indexed private UUID target; @Getter private String reason; - @Getter private PunishmentType type; + @Getter @Indexed private PunishmentType type; // Type is indexed for the rank dump @Getter private Date expiresAt; @Getter private UUID addedBy; - @Getter private Date addedAt; - @Getter private String addedOn; + @Getter @Indexed private Date addedAt; + @Getter private String addedOn; // TODO: Make this store actor like the audit log? @Getter private UUID removedBy; @Getter private Date removedAt; diff --git a/src/main/java/net/frozenorb/apiv3/models/Rank.java b/src/main/java/net/frozenorb/apiv3/models/Rank.java index cefd54e..e4054fe 100644 --- a/src/main/java/net/frozenorb/apiv3/models/Rank.java +++ b/src/main/java/net/frozenorb/apiv3/models/Rank.java @@ -4,6 +4,7 @@ import lombok.Getter; import net.frozenorb.apiv3.APIv3; import org.mongodb.morphia.annotations.Entity; import org.mongodb.morphia.annotations.Id; +import org.mongodb.morphia.annotations.Indexed; import java.util.List; @@ -15,7 +16,7 @@ public final class Rank { @Getter private String displayName; @Getter private String gameColor; @Getter private String websiteColor; - @Getter private boolean staffRank; + @Getter @Indexed private boolean staffRank; public static Rank byId(String id) { return APIv3.getDatastore().createQuery(Rank.class).field("id").equal(id).get(); diff --git a/src/main/java/net/frozenorb/apiv3/models/Server.java b/src/main/java/net/frozenorb/apiv3/models/Server.java index c7ca776..2a93725 100644 --- a/src/main/java/net/frozenorb/apiv3/models/Server.java +++ b/src/main/java/net/frozenorb/apiv3/models/Server.java @@ -6,6 +6,7 @@ import net.frozenorb.apiv3.APIv3; import net.frozenorb.apiv3.serialization.ExcludeFromReplies; import org.mongodb.morphia.annotations.Entity; import org.mongodb.morphia.annotations.Id; +import org.mongodb.morphia.annotations.Indexed; import java.util.*; @@ -16,7 +17,7 @@ public final class Server { @Getter private String bungeeId; @Getter private String displayName; @Getter @ExcludeFromReplies String apiKey; - @Getter private String group; + @Getter @Indexed private String group; @Getter private String ip; @Getter @Setter private Date lastUpdate; @Getter @Setter private double lastTps; diff --git a/src/main/java/net/frozenorb/apiv3/models/User.java b/src/main/java/net/frozenorb/apiv3/models/User.java index 5d628ab..ec85bf3 100644 --- a/src/main/java/net/frozenorb/apiv3/models/User.java +++ b/src/main/java/net/frozenorb/apiv3/models/User.java @@ -11,6 +11,7 @@ import org.bson.Document; import org.mindrot.jbcrypt.BCrypt; import org.mongodb.morphia.annotations.Entity; import org.mongodb.morphia.annotations.Id; +import org.mongodb.morphia.annotations.Indexed; import java.util.*; @@ -18,10 +19,10 @@ import java.util.*; public final class User { @Getter @Id private UUID id; - @Getter private String lastUsername; + @Getter @Indexed private String lastUsername; @Getter @ExcludeFromReplies private Map aliases; - @Getter @ExcludeFromReplies private String otpCode; - @Getter @ExcludeFromReplies @Setter private String emailToken; + @Getter @Setter @ExcludeFromReplies private String totpSecret; + @Getter @Indexed @ExcludeFromReplies @Setter private String emailToken; @Getter @ExcludeFromReplies @Setter private Date emailTokenSet; @Getter @ExcludeFromReplies private String password; @Getter @Setter private String email; @@ -57,7 +58,7 @@ public final class User { this.id = id; this.lastUsername = lastUsername; this.aliases = new HashMap<>(); - this.otpCode = null; + this.totpSecret = null; this.password = null; this.email = null; this.phoneNumber = -1; diff --git a/src/main/java/net/frozenorb/apiv3/models/UserMetaEntry.java b/src/main/java/net/frozenorb/apiv3/models/UserMetaEntry.java index 5b39f5b..066e5f9 100644 --- a/src/main/java/net/frozenorb/apiv3/models/UserMetaEntry.java +++ b/src/main/java/net/frozenorb/apiv3/models/UserMetaEntry.java @@ -7,6 +7,7 @@ import org.bson.Document; import org.bson.types.ObjectId; import org.mongodb.morphia.annotations.Entity; import org.mongodb.morphia.annotations.Id; +import org.mongodb.morphia.annotations.Indexed; import java.util.UUID; @@ -14,8 +15,8 @@ import java.util.UUID; public final class UserMetaEntry { @Getter @Id private String id; - @Getter private UUID user; - @Getter private String serverGroup; + @Getter @Indexed private UUID user; + @Getter @Indexed private String serverGroup; @Getter @Setter private Document data; public UserMetaEntry() {} // For Morphia diff --git a/src/main/java/net/frozenorb/apiv3/routes/GETWhoAmI.java b/src/main/java/net/frozenorb/apiv3/routes/GETWhoAmI.java index 4f63eee..3126bed 100644 --- a/src/main/java/net/frozenorb/apiv3/routes/GETWhoAmI.java +++ b/src/main/java/net/frozenorb/apiv3/routes/GETWhoAmI.java @@ -2,7 +2,6 @@ package net.frozenorb.apiv3.routes; import com.google.common.collect.ImmutableMap; import net.frozenorb.apiv3.actors.Actor; -import org.bson.Document; import spark.Request; import spark.Response; import spark.Route; diff --git a/src/main/java/net/frozenorb/apiv3/routes/NotFound.java b/src/main/java/net/frozenorb/apiv3/routes/NotFound.java index 39572f6..9a3ea92 100644 --- a/src/main/java/net/frozenorb/apiv3/routes/NotFound.java +++ b/src/main/java/net/frozenorb/apiv3/routes/NotFound.java @@ -1,8 +1,6 @@ package net.frozenorb.apiv3.routes; import net.frozenorb.apiv3.APIv3; -import net.frozenorb.apiv3.models.Grant; -import net.frozenorb.apiv3.models.Punishment; import net.frozenorb.apiv3.utils.ErrorUtils; import spark.Request; import spark.Response; @@ -12,7 +10,7 @@ import spark.Spark; public final class NotFound implements Route { public Object handle(Request req, Response res) { - Spark.halt(404, ErrorUtils.notFound("Route", req.url()).toJson()); + Spark.halt(404, APIv3.getGson().toJson(ErrorUtils.notFound("Route", req.url()))); return null; } diff --git a/src/main/java/net/frozenorb/apiv3/routes/grants/DELETEGrant.java b/src/main/java/net/frozenorb/apiv3/routes/grants/DELETEGrant.java index 5993d93..3bc3142 100644 --- a/src/main/java/net/frozenorb/apiv3/routes/grants/DELETEGrant.java +++ b/src/main/java/net/frozenorb/apiv3/routes/grants/DELETEGrant.java @@ -1,8 +1,9 @@ package net.frozenorb.apiv3.routes.grants; +import net.frozenorb.apiv3.auditLog.AuditLog; +import net.frozenorb.apiv3.auditLog.AuditLogActionType; import net.frozenorb.apiv3.models.Grant; import net.frozenorb.apiv3.models.User; -import net.frozenorb.apiv3.auditLog.AuditLog; import net.frozenorb.apiv3.unsorted.Permissions; import net.frozenorb.apiv3.utils.ErrorUtils; import org.bson.Document; @@ -33,7 +34,8 @@ public final class DELETEGrant implements Route { String reason = req.queryParams("removalReason"); grant.delete(removedBy, reason); - AuditLog.log(removedBy, req.attribute("actor"), "grant.remove", new Document("grantId", grant.getId())); + // TODO: Fix IP + AuditLog.log(removedBy, "", req.attribute("actor"), AuditLogActionType.DELETE_GRANT, new Document("grantId", grant.getId())); return grant; } diff --git a/src/main/java/net/frozenorb/apiv3/routes/grants/GETGrant.java b/src/main/java/net/frozenorb/apiv3/routes/grants/GETGrant.java index 1f612b2..226ac9f 100644 --- a/src/main/java/net/frozenorb/apiv3/routes/grants/GETGrant.java +++ b/src/main/java/net/frozenorb/apiv3/routes/grants/GETGrant.java @@ -1,7 +1,6 @@ package net.frozenorb.apiv3.routes.grants; import net.frozenorb.apiv3.models.Grant; -import net.frozenorb.apiv3.models.Punishment; import spark.Request; import spark.Response; import spark.Route; diff --git a/src/main/java/net/frozenorb/apiv3/routes/punishments/DELETEPunishment.java b/src/main/java/net/frozenorb/apiv3/routes/punishments/DELETEPunishment.java index beb3030..813b2d0 100644 --- a/src/main/java/net/frozenorb/apiv3/routes/punishments/DELETEPunishment.java +++ b/src/main/java/net/frozenorb/apiv3/routes/punishments/DELETEPunishment.java @@ -1,8 +1,9 @@ package net.frozenorb.apiv3.routes.punishments; +import net.frozenorb.apiv3.auditLog.AuditLog; +import net.frozenorb.apiv3.auditLog.AuditLogActionType; import net.frozenorb.apiv3.models.Punishment; import net.frozenorb.apiv3.models.User; -import net.frozenorb.apiv3.auditLog.AuditLog; import net.frozenorb.apiv3.unsorted.Permissions; import net.frozenorb.apiv3.utils.ErrorUtils; import org.bson.Document; @@ -33,7 +34,8 @@ public final class DELETEPunishment implements Route { String reason = req.queryParams("removalReason"); punishment.delete(removedBy, reason); - AuditLog.log(removedBy, req.attribute("actor"), "punishment.remove", new Document("punishmentId", punishment.getId())); + // TODO: Fix IP + AuditLog.log(removedBy, "", req.attribute("actor"), AuditLogActionType.DELETE_PUNISHMENT, new Document("punishmentId", punishment.getId())); return punishment; } diff --git a/src/main/java/net/frozenorb/apiv3/routes/users/GETUserVerifyTOTP.java b/src/main/java/net/frozenorb/apiv3/routes/users/GETUserVerifyTOTP.java new file mode 100644 index 0000000..4b8167f --- /dev/null +++ b/src/main/java/net/frozenorb/apiv3/routes/users/GETUserVerifyTOTP.java @@ -0,0 +1,31 @@ +package net.frozenorb.apiv3.routes.users; + +import com.google.common.collect.ImmutableMap; +import net.frozenorb.apiv3.models.User; +import net.frozenorb.apiv3.utils.ErrorUtils; +import net.frozenorb.apiv3.utils.TOTPUtils; +import spark.Request; +import spark.Response; +import spark.Route; + +public final class GETUserVerifyTOTP implements Route { + + public Object handle(Request req, Response res) { + User user = User.byId(req.params("id")); + + if (user == null) { + return ErrorUtils.notFound("User", req.params("id")); + } + + if (user.getTotpSecret() == null) { + return ErrorUtils.invalidInput("User provided does not have TOTP code set."); + } + + int providedCode = Integer.parseInt(req.queryParams("code")); + + return ImmutableMap.of( + "verified", TOTPUtils.authorizeUser(user, providedCode) + ); + } + +} \ No newline at end of file diff --git a/src/main/java/net/frozenorb/apiv3/routes/users/POSTUserConfirmRegister.java b/src/main/java/net/frozenorb/apiv3/routes/users/POSTUserConfirmRegister.java index 2486890..0aef098 100644 --- a/src/main/java/net/frozenorb/apiv3/routes/users/POSTUserConfirmRegister.java +++ b/src/main/java/net/frozenorb/apiv3/routes/users/POSTUserConfirmRegister.java @@ -1,10 +1,10 @@ package net.frozenorb.apiv3.routes.users; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; import net.frozenorb.apiv3.APIv3; import net.frozenorb.apiv3.models.User; import net.frozenorb.apiv3.utils.ErrorUtils; -import org.bson.Document; import spark.Request; import spark.Response; import spark.Route; @@ -51,7 +51,9 @@ public final class POSTUserConfirmRegister implements Route { user.setPassword(password.toCharArray()); APIv3.getDatastore().save(user); - return new Document("success", true).append("message", "User confirmed"); + return ImmutableMap.of( + "success", true + ); } } \ No newline at end of file diff --git a/src/main/java/net/frozenorb/apiv3/routes/users/POSTUserNotify.java b/src/main/java/net/frozenorb/apiv3/routes/users/POSTUserNotify.java index 7672144..4774817 100644 --- a/src/main/java/net/frozenorb/apiv3/routes/users/POSTUserNotify.java +++ b/src/main/java/net/frozenorb/apiv3/routes/users/POSTUserNotify.java @@ -1,10 +1,10 @@ package net.frozenorb.apiv3.routes.users; +import com.google.common.collect.ImmutableMap; import net.frozenorb.apiv3.models.NotificationTemplate; import net.frozenorb.apiv3.models.User; import net.frozenorb.apiv3.unsorted.Notification; import net.frozenorb.apiv3.utils.ErrorUtils; -import org.bson.Document; import spark.Request; import spark.Response; import spark.Route; @@ -46,7 +46,9 @@ public final class POSTUserNotify implements Route { Notification notification = new Notification(template, subjectReplacements, bodyReplacements); notification.sendAsEmail(user.getEmail()); - return new Document("success", true); + return ImmutableMap.of( + "success", true + ); } catch (Exception ex) { ex.printStackTrace(); return ErrorUtils.error("Failed to send notification"); diff --git a/src/main/java/net/frozenorb/apiv3/routes/users/POSTUserRegister.java b/src/main/java/net/frozenorb/apiv3/routes/users/POSTUserRegister.java index 60480de..e6f3c60 100644 --- a/src/main/java/net/frozenorb/apiv3/routes/users/POSTUserRegister.java +++ b/src/main/java/net/frozenorb/apiv3/routes/users/POSTUserRegister.java @@ -6,7 +6,6 @@ import net.frozenorb.apiv3.models.NotificationTemplate; import net.frozenorb.apiv3.models.User; import net.frozenorb.apiv3.unsorted.Notification; import net.frozenorb.apiv3.utils.ErrorUtils; -import org.bson.Document; import spark.Request; import spark.Response; import spark.Route; @@ -59,7 +58,9 @@ public final class POSTUserRegister implements Route { try { notification.sendAsEmail(user.getEmail()); - return new Document("success", true).append("message", "User registered"); + return ImmutableMap.of( + "success", true + ); } catch (Exception ex) { ex.printStackTrace(); return ErrorUtils.error("Failed to send confirmation email. Please contact a MineHQ staff member."); diff --git a/src/main/java/net/frozenorb/apiv3/routes/users/POSTUserSetupTOTP.java b/src/main/java/net/frozenorb/apiv3/routes/users/POSTUserSetupTOTP.java new file mode 100644 index 0000000..0305087 --- /dev/null +++ b/src/main/java/net/frozenorb/apiv3/routes/users/POSTUserSetupTOTP.java @@ -0,0 +1,36 @@ +package net.frozenorb.apiv3.routes.users; + +import com.google.common.collect.ImmutableMap; +import com.warrenstrange.googleauth.GoogleAuthenticatorKey; +import net.frozenorb.apiv3.APIv3; +import net.frozenorb.apiv3.models.User; +import net.frozenorb.apiv3.utils.ErrorUtils; +import net.frozenorb.apiv3.utils.TOTPUtils; +import spark.Request; +import spark.Response; +import spark.Route; + +public final class POSTUserSetupTOTP implements Route { + + public Object handle(Request req, Response res) { + User user = User.byId(req.params("id")); + + if (user == null) { + return ErrorUtils.notFound("User", req.params("id")); + } + + if (user.getTotpSecret() != null) { + return ErrorUtils.invalidInput("User provided already has TOTP code set."); + } + + GoogleAuthenticatorKey generated = TOTPUtils.generateTOTPKey(); + + user.setTotpSecret(generated.getKey()); + APIv3.getDatastore().save(user); + + return ImmutableMap.of( + "qrCode", TOTPUtils.getQRCodeURL(user, generated) + ); + } + +} \ No newline at end of file diff --git a/src/main/java/net/frozenorb/apiv3/unsorted/LoggingExceptionHandler.java b/src/main/java/net/frozenorb/apiv3/unsorted/LoggingExceptionHandler.java index e9a8a03..3ac1f92 100644 --- a/src/main/java/net/frozenorb/apiv3/unsorted/LoggingExceptionHandler.java +++ b/src/main/java/net/frozenorb/apiv3/unsorted/LoggingExceptionHandler.java @@ -1,6 +1,7 @@ package net.frozenorb.apiv3.unsorted; import lombok.extern.slf4j.Slf4j; +import net.frozenorb.apiv3.APIv3; import net.frozenorb.apiv3.utils.ErrorUtils; import org.bson.types.ObjectId; import spark.ExceptionHandler; @@ -16,7 +17,7 @@ public final class LoggingExceptionHandler implements ExceptionHandler { log.error(code + ":"); ex.printStackTrace(); - res.body(ErrorUtils.error("An unknown error has occurred. Please contact a developer with the code \"" + code + "\".").toJson()); + res.body(APIv3.getGson().toJson(ErrorUtils.error("An unknown error has occurred. Please contact a developer with the code \"" + code + "\"."))); } } \ No newline at end of file diff --git a/src/main/java/net/frozenorb/apiv3/utils/ErrorUtils.java b/src/main/java/net/frozenorb/apiv3/utils/ErrorUtils.java index 6963440..a8ae71a 100644 --- a/src/main/java/net/frozenorb/apiv3/utils/ErrorUtils.java +++ b/src/main/java/net/frozenorb/apiv3/utils/ErrorUtils.java @@ -1,29 +1,34 @@ package net.frozenorb.apiv3.utils; +import com.google.common.collect.ImmutableMap; import lombok.experimental.UtilityClass; -import org.bson.Document; + +import java.util.Map; @UtilityClass public class ErrorUtils { - public static Document serverOnly() { + public static Map serverOnly() { return error("This action can only be performed when requested by a server."); } - public static Document notFound(String itemType, String id) { + public static Map notFound(String itemType, String id) { return error("Not found: " + itemType + " with id " + id + " cannot be found."); } - public static Document unauthorized(String permission) { + public static Map unauthorized(String permission) { return error("Unauthorized access: Permission \"" + permission + "\" required."); } - public static Document invalidInput(String message) { + public static Map invalidInput(String message) { return error("Invalid input: " + message); } - public static Document error(String message) { - return new Document("success", false).append("message", message); + public static Map error(String message) { + return ImmutableMap.of( + "success", false, + "message", message + ); } } \ No newline at end of file diff --git a/src/main/java/net/frozenorb/apiv3/utils/TOTPUtils.java b/src/main/java/net/frozenorb/apiv3/utils/TOTPUtils.java new file mode 100644 index 0000000..70a5514 --- /dev/null +++ b/src/main/java/net/frozenorb/apiv3/utils/TOTPUtils.java @@ -0,0 +1,32 @@ +package net.frozenorb.apiv3.utils; + +import com.google.common.collect.ImmutableList; +import com.warrenstrange.googleauth.GoogleAuthenticator; +import com.warrenstrange.googleauth.GoogleAuthenticatorKey; +import com.warrenstrange.googleauth.GoogleAuthenticatorQRGenerator; +import lombok.experimental.UtilityClass; +import net.frozenorb.apiv3.models.User; +import org.apache.http.client.utils.URIBuilder; + +@UtilityClass +public class TOTPUtils { + + private static GoogleAuthenticator googleAuthenticator = new GoogleAuthenticator(); + + public static GoogleAuthenticatorKey generateTOTPKey() { + return googleAuthenticator.createCredentials(); + } + + public static boolean authorizeUser(User user, int code) { + return googleAuthenticator.authorize(user.getTotpSecret(), code); + } + + public static String getQRCodeURL(User user, GoogleAuthenticatorKey key) { + return GoogleAuthenticatorQRGenerator.getOtpAuthURL( + "MineHQ v3", + user.getLastUsername(), + key + ); + } + +} \ No newline at end of file