diff --git a/apiv3.properties b/apiv3.properties index 70d0854..126d564 100644 --- a/apiv3.properties +++ b/apiv3.properties @@ -8,6 +8,8 @@ redis.port=6379 http.address= http.port=80 http.workerThreads=6 +twillio.accountSID=AC9e2f88c5690134d29a56f698de3cd740 +twillio.authToken=982592505a171d3be6b0722f5ecacc0e mandrill.apiKey=0OYtwymqJP6oqvszeJu0vQ auth.permittedUserRanks=developer,owner auth.websiteApiKey=RVbp4hY6sCFVaf \ No newline at end of file diff --git a/pom.xml b/pom.xml index e6a8c37..a5a8f9f 100644 --- a/pom.xml +++ b/pom.xml @@ -112,6 +112,16 @@ morphia-logging-slf4j 1.1.0 + + org.slf4j + slf4j-simple + 1.6.4 + + + org.apache.httpcomponents + httpcore + 4.4 + com.warrenstrange googleauth diff --git a/src/main/java/net/frozenorb/apiv3/APIv3.java b/src/main/java/net/frozenorb/apiv3/APIv3.java index fad0cfd..fa9a72c 100644 --- a/src/main/java/net/frozenorb/apiv3/APIv3.java +++ b/src/main/java/net/frozenorb/apiv3/APIv3.java @@ -1,6 +1,7 @@ package net.frozenorb.apiv3; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.mongodb.MongoClient; @@ -10,6 +11,7 @@ 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.NotificationTemplate; import net.frozenorb.apiv3.routes.GETDump; import net.frozenorb.apiv3.routes.GETWhoAmI; import net.frozenorb.apiv3.routes.NotFound; @@ -39,6 +41,7 @@ import net.frozenorb.apiv3.routes.users.*; import net.frozenorb.apiv3.serialization.FollowAnnotationExclusionStrategy; import net.frozenorb.apiv3.serialization.ObjectIdTypeAdapter; import net.frozenorb.apiv3.unsorted.LoggingExceptionHandler; +import net.frozenorb.apiv3.unsorted.Notification; import org.bson.types.ObjectId; import org.mongodb.morphia.Datastore; import org.mongodb.morphia.Morphia; @@ -164,6 +167,7 @@ public final class APIv3 { get("/user/:id/grants", new GETUserGrants(), gson::toJson); get("/user/:id/ipLog", new GETUserIPLog(), gson::toJson); get("/user/:id/requiresTOTP", new GETUserRequiresTOTP(), gson::toJson); + get("/user/:id/verifyPassword", new GETUserVerifyPassword(), gson::toJson); get("/user/:id", new GETUser(), gson::toJson); post("/user/:id/verifyTOTP", new POSTUserVerifyTOTP(), gson::toJson); post("/user/:id:/grant", new POSTUserGrant(), 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 0c4fca0..f246540 100644 --- a/src/main/java/net/frozenorb/apiv3/auditLog/AuditLog.java +++ b/src/main/java/net/frozenorb/apiv3/auditLog/AuditLog.java @@ -1,5 +1,6 @@ package net.frozenorb.apiv3.auditLog; +import com.google.common.collect.ImmutableMap; import lombok.experimental.UtilityClass; import net.frozenorb.apiv3.APIv3; import net.frozenorb.apiv3.actors.Actor; @@ -13,7 +14,7 @@ import java.util.Map; public class AuditLog { public static void log(User performedBy, String performedByIp, Actor actor, AuditLogActionType actionType) { - log(performedBy, performedByIp, actor, actionType, new Document()); + log(performedBy, performedByIp, actor, actionType, ImmutableMap.of()); } public static void log(User performedBy, String performedByIp, Actor actor, AuditLogActionType actionType, Map actionData) { diff --git a/src/main/java/net/frozenorb/apiv3/models/Grant.java b/src/main/java/net/frozenorb/apiv3/models/Grant.java index 62ba897..3db6e92 100644 --- a/src/main/java/net/frozenorb/apiv3/models/Grant.java +++ b/src/main/java/net/frozenorb/apiv3/models/Grant.java @@ -3,6 +3,8 @@ package net.frozenorb.apiv3.models; import com.google.common.collect.Collections2; import lombok.Getter; import net.frozenorb.apiv3.APIv3; +import net.frozenorb.apiv3.actors.Actor; +import net.frozenorb.apiv3.actors.ActorType; import org.bson.types.ObjectId; import org.mongodb.morphia.annotations.Entity; import org.mongodb.morphia.annotations.Id; diff --git a/src/main/java/net/frozenorb/apiv3/models/IPLogEntry.java b/src/main/java/net/frozenorb/apiv3/models/IPLogEntry.java index dc40569..73836e3 100644 --- a/src/main/java/net/frozenorb/apiv3/models/IPLogEntry.java +++ b/src/main/java/net/frozenorb/apiv3/models/IPLogEntry.java @@ -2,6 +2,7 @@ package net.frozenorb.apiv3.models; import lombok.Getter; import net.frozenorb.apiv3.APIv3; +import net.frozenorb.apiv3.serialization.ExcludeFromReplies; import org.bson.types.ObjectId; import org.mongodb.morphia.annotations.Entity; import org.mongodb.morphia.annotations.Id; @@ -14,7 +15,7 @@ import java.util.UUID; public final class IPLogEntry { @Getter @Id private String id; - @Getter @Indexed private UUID user; + @Getter @ExcludeFromReplies @Indexed private UUID user; @Getter @Indexed private String ip; @Getter private Date firstSeen; @Getter private Date lastSeen; diff --git a/src/main/java/net/frozenorb/apiv3/models/Punishment.java b/src/main/java/net/frozenorb/apiv3/models/Punishment.java index d827430..1b63e52 100644 --- a/src/main/java/net/frozenorb/apiv3/models/Punishment.java +++ b/src/main/java/net/frozenorb/apiv3/models/Punishment.java @@ -2,6 +2,8 @@ package net.frozenorb.apiv3.models; import lombok.Getter; import net.frozenorb.apiv3.APIv3; +import net.frozenorb.apiv3.actors.Actor; +import net.frozenorb.apiv3.actors.ActorType; import org.bson.types.ObjectId; import org.mongodb.morphia.annotations.Entity; import org.mongodb.morphia.annotations.Id; @@ -21,7 +23,8 @@ public final class Punishment { @Getter private UUID addedBy; @Getter @Indexed private Date addedAt; - @Getter private String addedOn; // TODO: Make this store actor like the audit log? + @Getter private String actorName; + @Getter private ActorType actorType; @Getter private UUID removedBy; @Getter private Date removedAt; @@ -33,7 +36,7 @@ public final class Punishment { public Punishment() {} // For Morphia - public Punishment(User target, String reason, PunishmentType type, Date expiresAt, User addedBy, Server addedOn) { + public Punishment(User target, String reason, PunishmentType type, Date expiresAt, User addedBy, Actor actor) { this.id = new ObjectId().toString(); this.target = target.getId(); this.reason = reason; @@ -41,7 +44,8 @@ public final class Punishment { this.expiresAt = (Date) expiresAt.clone(); this.addedBy = addedBy.getId(); this.addedAt = new Date(); - this.addedOn = addedOn.getId(); + this.actorName = actor.getName(); + this.actorType = actor.getType(); } public void delete(User removedBy, String reason) { diff --git a/src/main/java/net/frozenorb/apiv3/models/ServerGroup.java b/src/main/java/net/frozenorb/apiv3/models/ServerGroup.java index a322627..fa3c7dc 100644 --- a/src/main/java/net/frozenorb/apiv3/models/ServerGroup.java +++ b/src/main/java/net/frozenorb/apiv3/models/ServerGroup.java @@ -2,7 +2,9 @@ package net.frozenorb.apiv3.models; import com.google.common.collect.ImmutableSet; import lombok.Getter; +import lombok.Setter; import net.frozenorb.apiv3.APIv3; +import net.frozenorb.apiv3.serialization.ExcludeFromReplies; import net.frozenorb.apiv3.utils.PermissionUtils; import org.mongodb.morphia.annotations.Entity; import org.mongodb.morphia.annotations.Id; @@ -13,11 +15,10 @@ import java.util.*; public final class ServerGroup { @Getter @Id private String id; - @Getter private String displayName; + @Getter private boolean isPublic; // We define these HashSets up here because, in the event they're // empty, Morphia will load them as null, not empty sets. - @Getter private Set announcements = new HashSet<>(); - @Getter private Set chatFilterList = new HashSet<>(); + @Getter @Setter @ExcludeFromReplies private Set announcements = new HashSet<>(); @Getter private Map> permissions = new HashMap<>(); public static ServerGroup byId(String id) { @@ -30,26 +31,13 @@ public final class ServerGroup { public ServerGroup() {} // For Morphia - public ServerGroup(String id, String displayName) { + public ServerGroup(String id, boolean isPublic) { this.id = id; - this.displayName = displayName; - } - - public void setAnnouncements(Set announcements) { - this.announcements = ImmutableSet.copyOf(announcements); - APIv3.getDatastore().save(this); - } - - public void setChatFilterList(Set chatFilterList) { - this.chatFilterList = ImmutableSet.copyOf(chatFilterList); - APIv3.getDatastore().save(this); + this.isPublic = isPublic; } public Map calculatePermissions(Rank userRank) { - return PermissionUtils.mergePermissions( - PermissionUtils.getDefaultPermissions(userRank), - PermissionUtils.mergeUpTo(permissions, userRank) - ); + return PermissionUtils.mergeUpTo(permissions, userRank); } public void delete() { diff --git a/src/main/java/net/frozenorb/apiv3/models/User.java b/src/main/java/net/frozenorb/apiv3/models/User.java index 622c110..cfcf96a 100644 --- a/src/main/java/net/frozenorb/apiv3/models/User.java +++ b/src/main/java/net/frozenorb/apiv3/models/User.java @@ -6,6 +6,7 @@ import lombok.Getter; import lombok.Setter; import net.frozenorb.apiv3.APIv3; import net.frozenorb.apiv3.serialization.ExcludeFromReplies; +import net.frozenorb.apiv3.utils.PermissionUtils; import net.frozenorb.apiv3.utils.TimeUtils; import org.bson.Document; import org.mindrot.jbcrypt.BCrypt; @@ -16,7 +17,7 @@ import org.mongodb.morphia.annotations.Indexed; import java.util.*; @Entity(value = "users", noClassnameStored = true) -public final class User { +public final class User { @Getter @Id private UUID id; @Getter @Indexed private String lastUsername; @@ -26,7 +27,7 @@ public final class User { @Getter @ExcludeFromReplies @Setter private Date emailTokenSet; @Getter @ExcludeFromReplies private String password; @Getter @Setter private String email; - @Getter private int phoneNumber; + @Getter private String phoneNumber; @Getter private String lastSeenOn; @Getter private Date lastSeenAt; @Getter private Date firstSeen; @@ -61,7 +62,7 @@ public final class User { this.totpSecret = null; this.password = null; this.email = null; - this.phoneNumber = -1; + this.phoneNumber = null; this.lastSeenOn = null; this.lastSeenAt = new Date(); this.firstSeen = new Date(); @@ -70,14 +71,29 @@ public final class User { } public boolean hasPermissionScoped(String permission, ServerGroup scope) { - Map permissions = scope.calculatePermissions(getHighestRank(scope)); - return permissions.containsKey(permission) && permissions.get(permission); + Rank highestRank = getHighestRank(scope); + Map scopedPermissions = PermissionUtils.mergePermissions( + PermissionUtils.getDefaultPermissions(highestRank), + scope.calculatePermissions(highestRank) + ); + + return scopedPermissions.containsKey(permission) && scopedPermissions.get(permission); } - // TODO public boolean hasPermissionAnywhere(String permission) { - Map permissions = /*scope.calculatePermissions(getHighestRank(scope));*/ ImmutableMap.of(); - return permissions.containsKey(permission) && permissions.get(permission); + Map globalPermissions = PermissionUtils.getDefaultPermissions(getHighestRank()); + + for (Map.Entry serverGroupEntry : getHighestRanks().entrySet()) { + ServerGroup serverGroup = serverGroupEntry.getKey(); + Rank rank = serverGroupEntry.getValue(); + + globalPermissions = PermissionUtils.mergePermissions( + globalPermissions, + serverGroup.calculatePermissions(rank) + ); + } + + return globalPermissions.containsKey(permission) && globalPermissions.get(permission); } public List getGrants() { @@ -122,9 +138,10 @@ public final class User { } } - public void seenOnServer(Server server) { + public void seenOnServer(String username, Server server) { this.lastSeenOn = server.getId(); this.lastSeenAt = new Date(); + this.aliases.put(username, new Date()); } public void setPassword(char[] unencrypted) { @@ -135,6 +152,10 @@ public final class User { return BCrypt.checkpw(new String(unencrypted), password); } + public Rank getHighestRank() { + return getHighestRank(null); + } + public Rank getHighestRank(ServerGroup serverGroup) { Rank highest = null; @@ -157,8 +178,30 @@ public final class User { } } - public Rank getHighestRank() { - return getHighestRank(null); + public Map getHighestRanks() { + Map highestRanks = new HashMap<>(); + Rank defaultRank = Rank.byId("default"); + List userGrants = getGrants(); + + for (ServerGroup serverGroup : ServerGroup.values()) { + Rank highest = defaultRank; + + for (Grant grant : userGrants) { + if (!grant.isActive() || !grant.appliesOn(serverGroup)) { + continue; + } + + Rank rank = Rank.byId(grant.getRank()); + + if (highest == null || rank.getWeight() > highest.getWeight()) { + highest = rank; + } + } + + highestRanks.put(serverGroup, highest); + } + + return highestRanks; } public Map getLoginInfo(Server server) { @@ -187,18 +230,21 @@ public final class User { ServerGroup actorGroup = ServerGroup.byId(server.getGroup()); - Rank rank = getHighestRank(actorGroup); - Map rankPermissions = actorGroup.calculatePermissions(rank); + Rank highestRank = getHighestRank(actorGroup); + Map scopedPermissions = PermissionUtils.mergePermissions( + PermissionUtils.getDefaultPermissions(highestRank), + actorGroup.calculatePermissions(highestRank) + ); return ImmutableMap.of( "user", this, "access", ImmutableMap.of( "allowed", accessDenialReason == null, - "reason", accessDenialReason == null ? "Public server" : accessDenialReason + "message", accessDenialReason == null ? "Public server" : accessDenialReason ), - "rank", rank, - "permissions", rankPermissions, - "totpRequired", getTotpSecret() != null + "rank", highestRank.getId(), + "permissions", scopedPermissions, + "totpSetup", getTotpSecret() != null ); } diff --git a/src/main/java/net/frozenorb/apiv3/models/UserMetaEntry.java b/src/main/java/net/frozenorb/apiv3/models/UserMetaEntry.java index 066e5f9..48dfe79 100644 --- a/src/main/java/net/frozenorb/apiv3/models/UserMetaEntry.java +++ b/src/main/java/net/frozenorb/apiv3/models/UserMetaEntry.java @@ -1,5 +1,6 @@ package net.frozenorb.apiv3.models; +import com.google.common.collect.ImmutableMap; import lombok.Getter; import lombok.Setter; import net.frozenorb.apiv3.APIv3; @@ -9,6 +10,7 @@ import org.mongodb.morphia.annotations.Entity; import org.mongodb.morphia.annotations.Id; import org.mongodb.morphia.annotations.Indexed; +import java.util.Map; import java.util.UUID; @Entity(value = "userMeta", noClassnameStored = true) @@ -17,15 +19,15 @@ public final class UserMetaEntry { @Getter @Id private String id; @Getter @Indexed private UUID user; @Getter @Indexed private String serverGroup; - @Getter @Setter private Document data; + @Getter @Setter private Map data; public UserMetaEntry() {} // For Morphia - public UserMetaEntry(User user, ServerGroup serverGroup, Document data) { + public UserMetaEntry(User user, ServerGroup serverGroup, Map data) { this.id = new ObjectId().toString(); this.user = user.getId(); this.serverGroup = serverGroup.getId(); - this.data = new Document(data); + this.data = ImmutableMap.copyOf(data); } public void delete() { diff --git a/src/main/java/net/frozenorb/apiv3/routes/GETDump.java b/src/main/java/net/frozenorb/apiv3/routes/GETDump.java index 2d8bf1a..5a81542 100644 --- a/src/main/java/net/frozenorb/apiv3/routes/GETDump.java +++ b/src/main/java/net/frozenorb/apiv3/routes/GETDump.java @@ -1,5 +1,6 @@ package net.frozenorb.apiv3.routes; +import com.google.common.collect.ImmutableSet; import net.frozenorb.apiv3.APIv3; import net.frozenorb.apiv3.models.Grant; import net.frozenorb.apiv3.models.Punishment; @@ -8,8 +9,7 @@ import spark.Request; import spark.Response; import spark.Route; -import java.util.ArrayList; -import java.util.List; +import java.util.*; public final class GETDump implements Route { @@ -21,27 +21,49 @@ public final class GETDump implements Route { case "ban": case "mute": case "warn": - List activePunishments = new ArrayList<>(); + List effectedUsers = new ArrayList<>(); APIv3.getDatastore().createQuery(Punishment.class).field("type").equal(type.toUpperCase()).forEach((punishment) -> { if (punishment.isActive()) { - activePunishments.add(punishment); + effectedUsers.add(punishment.getTarget()); } }); - return activePunishments; + return effectedUsers; + case "accessDeniable": + // We have to name it effectedUsers2 because Java's + // scoping in switch statements is really dumb. + List effectedUsers2 = new ArrayList<>(); + + APIv3.getDatastore().createQuery(Punishment.class).field("type").in(ImmutableSet.of( + Punishment.PunishmentType.BAN, + Punishment.PunishmentType.BLACKLIST + )).forEach((punishment) -> { + if (punishment.isActive()) { + effectedUsers2.add(punishment.getTarget()); + } + }); + + return effectedUsers2; case "grant": - List activeGrants = new ArrayList<>(); + Map> grantDump = new HashMap<>(); APIv3.getDatastore().createQuery(Grant.class).forEach((grant) -> { if (grant.isActive()) { - activeGrants.add(grant); + List users = grantDump.get(grant.getRank()); + + if (users == null) { + users = new ArrayList<>(); + grantDump.put(grant.getRank(), users); + } + + users.add(grant.getTarget()); } }); - return activeGrants; + return grantDump; default: - return ErrorUtils.invalidInput(type + " is not a valid type. Not in [blacklist, ban, mute, warn, grant]"); + return ErrorUtils.invalidInput(type + " is not a valid type. Not in [blacklist, ban, mute, warn, accessDeniable, grant]"); } } diff --git a/src/main/java/net/frozenorb/apiv3/routes/chatFilterList/GETChatFilterList.java b/src/main/java/net/frozenorb/apiv3/routes/chatFilterList/GETChatFilterList.java index 36807c3..eaad9f0 100644 --- a/src/main/java/net/frozenorb/apiv3/routes/chatFilterList/GETChatFilterList.java +++ b/src/main/java/net/frozenorb/apiv3/routes/chatFilterList/GETChatFilterList.java @@ -1,5 +1,6 @@ package net.frozenorb.apiv3.routes.chatFilterList; +import com.google.common.collect.ImmutableSet; import net.frozenorb.apiv3.actors.Actor; import net.frozenorb.apiv3.actors.ActorType; import net.frozenorb.apiv3.models.Server; @@ -12,16 +13,7 @@ import spark.Route; public final class GETChatFilterList implements Route { public Object handle(Request req, Response res) { - Actor actor = req.attribute("actor"); - - if (actor.getType() != ActorType.SERVER) { - return ErrorUtils.serverOnly(); - } - - Server sender = Server.byId(actor.getName()); - ServerGroup senderGroup = ServerGroup.byId(sender.getGroup()); - - return senderGroup.getChatFilterList(); + return ImmutableSet.of(); } } \ No newline at end of file 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 66ac4e6..4255feb 100644 --- a/src/main/java/net/frozenorb/apiv3/routes/grants/DELETEGrant.java +++ b/src/main/java/net/frozenorb/apiv3/routes/grants/DELETEGrant.java @@ -1,5 +1,6 @@ package net.frozenorb.apiv3.routes.grants; +import com.google.common.collect.ImmutableMap; import net.frozenorb.apiv3.auditLog.AuditLog; import net.frozenorb.apiv3.auditLog.AuditLogActionType; import net.frozenorb.apiv3.models.Grant; @@ -39,7 +40,7 @@ public final class DELETEGrant implements Route { grant.delete(removedBy, reason); // TODO: Fix IP - AuditLog.log(removedBy, "", req.attribute("actor"), AuditLogActionType.DELETE_GRANT, new Document("grantId", grant.getId())); + AuditLog.log(removedBy, "", req.attribute("actor"), AuditLogActionType.DELETE_GRANT, ImmutableMap.of()); return grant; } 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 bccac9a..4a8dc95 100644 --- a/src/main/java/net/frozenorb/apiv3/routes/punishments/DELETEPunishment.java +++ b/src/main/java/net/frozenorb/apiv3/routes/punishments/DELETEPunishment.java @@ -1,5 +1,6 @@ package net.frozenorb.apiv3.routes.punishments; +import com.google.common.collect.ImmutableMap; import net.frozenorb.apiv3.auditLog.AuditLog; import net.frozenorb.apiv3.auditLog.AuditLogActionType; import net.frozenorb.apiv3.models.Punishment; @@ -31,7 +32,7 @@ public final class DELETEPunishment implements Route { return ErrorUtils.unauthorized(requiredPermission); } - String reason = req.queryParams("removalReason"); + String reason = req.queryParams("reason"); if (reason == null || reason.trim().isEmpty()) { return ErrorUtils.requiredInput("reason"); @@ -39,7 +40,7 @@ public final class DELETEPunishment implements Route { punishment.delete(removedBy, reason); // TODO: Fix IP - AuditLog.log(removedBy, "", req.attribute("actor"), AuditLogActionType.DELETE_PUNISHMENT, new Document("punishmentId", punishment.getId())); + AuditLog.log(removedBy, "", req.attribute("actor"), AuditLogActionType.DELETE_PUNISHMENT, ImmutableMap.of()); return punishment; } diff --git a/src/main/java/net/frozenorb/apiv3/routes/punishments/POSTUserPunish.java b/src/main/java/net/frozenorb/apiv3/routes/punishments/POSTUserPunish.java index 503f2bb..083a0a5 100644 --- a/src/main/java/net/frozenorb/apiv3/routes/punishments/POSTUserPunish.java +++ b/src/main/java/net/frozenorb/apiv3/routes/punishments/POSTUserPunish.java @@ -17,8 +17,6 @@ public final class POSTUserPunish implements Route { public Object handle(Request req, Response res) { User target = User.byId(req.params("id")); - // TODO: PROTECTED USERS - if (target == null) { return ErrorUtils.notFound("User", req.params("id")); } @@ -45,13 +43,11 @@ public final class POSTUserPunish implements Route { return ErrorUtils.unauthorized(requiredPermission); } - Server addedOn = Server.byId(req.queryParams("addedOn")); - - if (addedOn == null) { - return ErrorUtils.notFound("Server", req.queryParams("addedOn")); + if (target.hasPermissionAnywhere(Permissions.PROTECTED_PUNISHMENT)) { + return ErrorUtils.error(target.getLastSeenOn() + " is protected from punishments."); } - Punishment punishment = new Punishment(target, reason, type, expiresAt, addedBy, addedOn); + Punishment punishment = new Punishment(target, reason, type, expiresAt, addedBy, req.attribute("actor")); APIv3.getDatastore().save(punishment); return punishment; } diff --git a/src/main/java/net/frozenorb/apiv3/routes/serverGroups/POSTServerGroup.java b/src/main/java/net/frozenorb/apiv3/routes/serverGroups/POSTServerGroup.java index 9277200..4c08bf0 100644 --- a/src/main/java/net/frozenorb/apiv3/routes/serverGroups/POSTServerGroup.java +++ b/src/main/java/net/frozenorb/apiv3/routes/serverGroups/POSTServerGroup.java @@ -10,9 +10,9 @@ public final class POSTServerGroup implements Route { public Object handle(Request req, Response res) { String id = req.queryParams("id"); - String displayName = req.queryParams("displayName"); + boolean isPublic = Boolean.valueOf(req.queryParams("public")); - ServerGroup serverGroup = new ServerGroup(id, displayName); + ServerGroup serverGroup = new ServerGroup(id, isPublic); APIv3.getDatastore().save(serverGroup); return serverGroup; } diff --git a/src/main/java/net/frozenorb/apiv3/routes/servers/POSTServerHeartbeat.java b/src/main/java/net/frozenorb/apiv3/routes/servers/POSTServerHeartbeat.java index bb5732a..6c157f2 100644 --- a/src/main/java/net/frozenorb/apiv3/routes/servers/POSTServerHeartbeat.java +++ b/src/main/java/net/frozenorb/apiv3/routes/servers/POSTServerHeartbeat.java @@ -39,7 +39,7 @@ public final class POSTServerHeartbeat implements Route { user = new User(UUID.fromString(playerJson.getString("uuid")), username); } - user.seenOnServer(actorServer); + user.seenOnServer(username, actorServer); APIv3.getDatastore().save(user); onlinePlayers.add(user.getId()); diff --git a/src/main/java/net/frozenorb/apiv3/routes/users/GETUserDetails.java b/src/main/java/net/frozenorb/apiv3/routes/users/GETUserDetails.java index c0e68f0..b46bb97 100644 --- a/src/main/java/net/frozenorb/apiv3/routes/users/GETUserDetails.java +++ b/src/main/java/net/frozenorb/apiv3/routes/users/GETUserDetails.java @@ -16,13 +16,14 @@ public final class GETUserDetails implements Route { return ErrorUtils.notFound("User", req.params("id")); } - return ImmutableMap.of( - "user", user, - "grants", user.getGrants(), - "ipLog", user.getIPLog(), - "punishments", user.getPunishments(), - "totpRequired", user.getTotpSecret() != null - ); + // Too many fields to use .of() + return ImmutableMap.builder() + .put("user", user) + .put("grants", user.getGrants()) + .put("ipLog", user.getIPLog()) + .put("punishments", user.getPunishments()) + .put("aliases", user.getAliases()) + .put("totpSetup", user.getTotpSecret() != null); } } \ No newline at end of file diff --git a/src/main/java/net/frozenorb/apiv3/routes/users/GETUserRequiresTOTP.java b/src/main/java/net/frozenorb/apiv3/routes/users/GETUserRequiresTOTP.java index 7595acd..201d0c7 100644 --- a/src/main/java/net/frozenorb/apiv3/routes/users/GETUserRequiresTOTP.java +++ b/src/main/java/net/frozenorb/apiv3/routes/users/GETUserRequiresTOTP.java @@ -1,4 +1,47 @@ package net.frozenorb.apiv3.routes.users; -public class GETUserRequiresTOTP { -} +import com.google.common.collect.ImmutableMap; +import net.frozenorb.apiv3.models.User; +import net.frozenorb.apiv3.utils.ErrorUtils; +import net.frozenorb.apiv3.utils.IPUtils; +import net.frozenorb.apiv3.utils.TOTPUtils; +import spark.Request; +import spark.Response; +import spark.Route; + +public final class GETUserRequiresTOTP 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 ImmutableMap.of( + "required", false, + "message", "User does not have TOTP setup." + ); + } + + String userIp = req.queryParams("userIp"); + + if (!IPUtils.isValidIP(userIp)) { + return ErrorUtils.invalidInput("IP address \"" + userIp + "\" is not valid."); + } + + if (TOTPUtils.isPreAuthorized(user, userIp)) { + return ImmutableMap.of( + "required", false, + "message", "User's IP has already been validated" + ); + } + + return ImmutableMap.of( + "required", true, + "message", "User has no TOTP exemptions." + ); + } + +} \ No newline at end of file diff --git a/src/main/java/net/frozenorb/apiv3/routes/users/GETUserVerifyPassword.java b/src/main/java/net/frozenorb/apiv3/routes/users/GETUserVerifyPassword.java new file mode 100644 index 0000000..8cc4698 --- /dev/null +++ b/src/main/java/net/frozenorb/apiv3/routes/users/GETUserVerifyPassword.java @@ -0,0 +1,35 @@ +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.IPUtils; +import net.frozenorb.apiv3.utils.TOTPUtils; +import spark.Request; +import spark.Response; +import spark.Route; + +import java.util.concurrent.TimeUnit; + +public final class GETUserVerifyPassword 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.getPassword() == null) { + return ErrorUtils.invalidInput("User provided does not have password set."); + } + + char[] password = req.queryParams("password").toCharArray(); + boolean authorized = user.checkPassword(password); + + return ImmutableMap.of( + "authorized", authorized + ); + } + +} \ 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 0aef098..099187b 100644 --- a/src/main/java/net/frozenorb/apiv3/routes/users/POSTUserConfirmRegister.java +++ b/src/main/java/net/frozenorb/apiv3/routes/users/POSTUserConfirmRegister.java @@ -39,16 +39,16 @@ public final class POSTUserConfirmRegister implements Route { return ErrorUtils.error("Email token is expired"); } - String password = req.queryParams("password"); + char[] password = req.queryParams("password").toCharArray(); - if (password.length() < 8) { + if (password.length < 8) { return ErrorUtils.error("Your password is too short."); - } else if (commonPasswords.contains(password)) { + } else if (commonPasswords.contains(new String(password))) { return ErrorUtils.error("Your password is too common. Please use a more secure password."); } user.setEmailToken(null); - user.setPassword(password.toCharArray()); + user.setPassword(password); APIv3.getDatastore().save(user); return ImmutableMap.of( 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 e8dd2b3..24927d9 100644 --- a/src/main/java/net/frozenorb/apiv3/routes/users/POSTUserRegister.java +++ b/src/main/java/net/frozenorb/apiv3/routes/users/POSTUserRegister.java @@ -19,11 +19,10 @@ import java.util.regex.Pattern; public final class POSTUserRegister implements Route { - private static final Pattern VALID_EMAIL_PATTERN = - Pattern.compile("^[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,6}$", Pattern.CASE_INSENSITIVE); - - // TODO: POSSIBLE? perms check - // TODO: make messages better + private static final Pattern VALID_EMAIL_PATTERN = Pattern.compile( + "^[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,6}$", + Pattern.CASE_INSENSITIVE + ); public Object handle(Request req, Response res) { User user = User.byId(req.params("id")); diff --git a/src/main/java/net/frozenorb/apiv3/routes/users/POSTUserVerifyTOTP.java b/src/main/java/net/frozenorb/apiv3/routes/users/POSTUserVerifyTOTP.java index 529f585..2e0c19b 100644 --- a/src/main/java/net/frozenorb/apiv3/routes/users/POSTUserVerifyTOTP.java +++ b/src/main/java/net/frozenorb/apiv3/routes/users/POSTUserVerifyTOTP.java @@ -3,11 +3,14 @@ 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.IPUtils; import net.frozenorb.apiv3.utils.TOTPUtils; import spark.Request; import spark.Response; import spark.Route; +import java.util.concurrent.TimeUnit; + public final class POSTUserVerifyTOTP implements Route { public Object handle(Request req, Response res) { @@ -21,11 +24,37 @@ public final class POSTUserVerifyTOTP implements Route { return ErrorUtils.invalidInput("User provided does not have TOTP code set."); } + String userIp = req.queryParams("userIp"); + + if (!IPUtils.isValidIP(userIp)) { + return ErrorUtils.invalidInput("IP address \"" + userIp + "\" is not valid."); + } + int providedCode = Integer.parseInt(req.queryParams("code")); - return ImmutableMap.of( - "verified", TOTPUtils.authorizeUser(user, providedCode) - ); + if (TOTPUtils.wasRecentlyUsed(user, providedCode)) { + return ImmutableMap.of( + "authorized", false, + "message", "TOTP code was recently used." + ); + } + + boolean authorized = TOTPUtils.authorizeUser(user, providedCode); + + if (authorized) { + TOTPUtils.markPreAuthorized(user, userIp, 3, TimeUnit.DAYS); + TOTPUtils.markRecentlyUsed(user, providedCode); + + return ImmutableMap.of( + "authorized", true, + "message", "Valid TOTP code provided." + ); + } else { + return ImmutableMap.of( + "authorized", false, + "message", "TOTP code was not valid." + ); + } } } \ No newline at end of file diff --git a/src/main/java/net/frozenorb/apiv3/unsorted/Notification.java b/src/main/java/net/frozenorb/apiv3/unsorted/Notification.java index 908c82f..bfea3c9 100644 --- a/src/main/java/net/frozenorb/apiv3/unsorted/Notification.java +++ b/src/main/java/net/frozenorb/apiv3/unsorted/Notification.java @@ -5,16 +5,25 @@ import com.cribbstechnologies.clients.mandrill.model.MandrillHtmlMessage; import com.cribbstechnologies.clients.mandrill.model.MandrillMessageRequest; import com.cribbstechnologies.clients.mandrill.model.MandrillRecipient; import com.cribbstechnologies.clients.mandrill.request.MandrillMessagesRequest; +import com.twilio.sdk.TwilioRestException; +import com.twilio.sdk.resource.factory.MessageFactory; +import com.twilio.sdk.resource.instance.Message; +import net.frozenorb.apiv3.APIv3; import net.frozenorb.apiv3.models.NotificationTemplate; import net.frozenorb.apiv3.utils.MandrillUtils; -import sun.reflect.generics.reflectiveObjects.NotImplementedException; +import net.frozenorb.apiv3.utils.TwillioUtils; +import org.apache.http.NameValuePair; +import org.apache.http.message.BasicNameValuePair; import java.io.IOException; +import java.util.ArrayList; +import java.util.List; import java.util.Map; public final class Notification { - private static final MandrillMessagesRequest messagesRequest = MandrillUtils.createMessagesRequest(); + private static final MandrillMessagesRequest mandrillMessagesRequest = MandrillUtils.createMessagesRequest(); + private static final MessageFactory twillioMessageFactory = TwillioUtils.createMessageFactory(); private final String subject; private final String body; @@ -38,15 +47,24 @@ public final class Notification { try { MandrillMessageRequest request = new MandrillMessageRequest(); request.setMessage(message); - messagesRequest.sendMessage(request); + mandrillMessagesRequest.sendMessage(request); } catch (RequestFailedException ex) { throw new IOException("Failed to send notification to user", ex); } } public void sendAsText(String phoneNumber) throws IOException { - // TODO - throw new IOException(new NotImplementedException()); + List params = new ArrayList<>(); + + params.add(new BasicNameValuePair("To", phoneNumber)); + params.add(new BasicNameValuePair("From", "+13108795180")); + params.add(new BasicNameValuePair("Body", body)); + + try { + twillioMessageFactory.create(params); + } catch (TwilioRestException ex) { + throw new IOException("Failed to send notification to user", ex); + } } } \ No newline at end of file diff --git a/src/main/java/net/frozenorb/apiv3/unsorted/Permissions.java b/src/main/java/net/frozenorb/apiv3/unsorted/Permissions.java index 7d31a74..eca08a1 100644 --- a/src/main/java/net/frozenorb/apiv3/unsorted/Permissions.java +++ b/src/main/java/net/frozenorb/apiv3/unsorted/Permissions.java @@ -10,5 +10,6 @@ public class Permissions { public static final String REMOVE_PUNISHMENT = "minehq.punishment.remove"; // minehq.punishment.remove.%TYPE% public static final String CREATE_PUNISHMENT = "minehq.punishment.create"; // minehq.punishment.create.%TYPE% + public static final String PROTECTED_PUNISHMENT = "minehq.punishment.protected"; } \ No newline at end of file diff --git a/src/main/java/net/frozenorb/apiv3/utils/TwillioUtils.java b/src/main/java/net/frozenorb/apiv3/utils/TwillioUtils.java new file mode 100644 index 0000000..9021c6a --- /dev/null +++ b/src/main/java/net/frozenorb/apiv3/utils/TwillioUtils.java @@ -0,0 +1,22 @@ +package net.frozenorb.apiv3.utils; + +import com.twilio.sdk.TwilioRestClient; +import com.twilio.sdk.resource.factory.MessageFactory; +import com.twilio.sdk.resource.instance.Account; +import lombok.experimental.UtilityClass; +import net.frozenorb.apiv3.APIv3; + +@UtilityClass +public class TwillioUtils { + + public static MessageFactory createMessageFactory() { + TwilioRestClient twillioClient = new TwilioRestClient( + APIv3.getConfig().getProperty("twillio.accountSID"), + APIv3.getConfig().getProperty("twillio.authToken") + ); + + Account account = twillioClient.getAccount(); + return account.getMessageFactory(); + } + +} \ No newline at end of file