More work

This commit is contained in:
Colin McDonald 2016-05-05 16:00:32 -04:00
parent 03bc01e5ce
commit 04e4db8fcd
26 changed files with 322 additions and 102 deletions

View File

@ -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

10
pom.xml
View File

@ -112,6 +112,16 @@
<artifactId>morphia-logging-slf4j</artifactId>
<version>1.1.0</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>1.6.4</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpcore</artifactId>
<version>4.4</version>
</dependency>
<dependency>
<groupId>com.warrenstrange</groupId>
<artifactId>googleauth</artifactId>

View File

@ -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);

View File

@ -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<String, Object> actionData) {

View File

@ -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;

View File

@ -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;

View File

@ -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) {

View File

@ -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<String> announcements = new HashSet<>();
@Getter private Set<String> chatFilterList = new HashSet<>();
@Getter @Setter @ExcludeFromReplies private Set<String> announcements = new HashSet<>();
@Getter private Map<String, List<String>> 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<String> announcements) {
this.announcements = ImmutableSet.copyOf(announcements);
APIv3.getDatastore().save(this);
}
public void setChatFilterList(Set<String> chatFilterList) {
this.chatFilterList = ImmutableSet.copyOf(chatFilterList);
APIv3.getDatastore().save(this);
this.isPublic = isPublic;
}
public Map<String, Boolean> calculatePermissions(Rank userRank) {
return PermissionUtils.mergePermissions(
PermissionUtils.getDefaultPermissions(userRank),
PermissionUtils.mergeUpTo(permissions, userRank)
);
return PermissionUtils.mergeUpTo(permissions, userRank);
}
public void delete() {

View File

@ -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;
@ -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<String, Boolean> permissions = scope.calculatePermissions(getHighestRank(scope));
return permissions.containsKey(permission) && permissions.get(permission);
Rank highestRank = getHighestRank(scope);
Map<String, Boolean> scopedPermissions = PermissionUtils.mergePermissions(
PermissionUtils.getDefaultPermissions(highestRank),
scope.calculatePermissions(highestRank)
);
return scopedPermissions.containsKey(permission) && scopedPermissions.get(permission);
}
// TODO
public boolean hasPermissionAnywhere(String permission) {
Map<String, Boolean> permissions = /*scope.calculatePermissions(getHighestRank(scope));*/ ImmutableMap.of();
return permissions.containsKey(permission) && permissions.get(permission);
Map<String, Boolean> globalPermissions = PermissionUtils.getDefaultPermissions(getHighestRank());
for (Map.Entry<ServerGroup, Rank> 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<Grant> 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<ServerGroup, Rank> getHighestRanks() {
Map<ServerGroup, Rank> highestRanks = new HashMap<>();
Rank defaultRank = Rank.byId("default");
List<Grant> 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<String, Object> getLoginInfo(Server server) {
@ -187,18 +230,21 @@ public final class User {
ServerGroup actorGroup = ServerGroup.byId(server.getGroup());
Rank rank = getHighestRank(actorGroup);
Map<String, Boolean> rankPermissions = actorGroup.calculatePermissions(rank);
Rank highestRank = getHighestRank(actorGroup);
Map<String, Boolean> 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
);
}

View File

@ -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<String, Object> data;
public UserMetaEntry() {} // For Morphia
public UserMetaEntry(User user, ServerGroup serverGroup, Document data) {
public UserMetaEntry(User user, ServerGroup serverGroup, Map<String, Object> 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() {

View File

@ -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<Punishment> activePunishments = new ArrayList<>();
List<UUID> 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<UUID> 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<Grant> activeGrants = new ArrayList<>();
Map<String, List<UUID>> grantDump = new HashMap<>();
APIv3.getDatastore().createQuery(Grant.class).forEach((grant) -> {
if (grant.isActive()) {
activeGrants.add(grant);
List<UUID> 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]");
}
}

View File

@ -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();
}
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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());

View File

@ -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);
}
}

View File

@ -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."
);
}
}

View File

@ -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
);
}
}

View File

@ -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(

View File

@ -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"));

View File

@ -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"));
if (TOTPUtils.wasRecentlyUsed(user, providedCode)) {
return ImmutableMap.of(
"verified", TOTPUtils.authorizeUser(user, providedCode)
"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."
);
}
}
}

View File

@ -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<NameValuePair> 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);
}
}
}

View File

@ -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";
}

View File

@ -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();
}
}