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