diff --git a/apiv3.properties b/apiv3.properties index 1624d10..70d0854 100644 --- a/apiv3.properties +++ b/apiv3.properties @@ -3,6 +3,8 @@ mongo.port=55505 mongo.database=minehqapi mongo.username=test mongo.password=test +redis.address=localhost +redis.port=6379 http.address= http.port=80 http.workerThreads=6 diff --git a/pom.xml b/pom.xml index c2a3a58..e6a8c37 100644 --- a/pom.xml +++ b/pom.xml @@ -77,6 +77,11 @@ metrics-core 3.1.2 + + com.bugsnag + bugsnag + 2.0.0 + org.mongodb mongo-java-driver @@ -87,6 +92,11 @@ mandrillClient 1.1 + + com.twilio.sdk + twilio-java-sdk + 6.3.0 + org.mindrot jbcrypt diff --git a/src/main/java/net/frozenorb/apiv3/APIv3.java b/src/main/java/net/frozenorb/apiv3/APIv3.java index 0bbcfee..fad0cfd 100644 --- a/src/main/java/net/frozenorb/apiv3/APIv3.java +++ b/src/main/java/net/frozenorb/apiv3/APIv3.java @@ -44,6 +44,7 @@ import org.mongodb.morphia.Datastore; import org.mongodb.morphia.Morphia; import org.mongodb.morphia.logging.MorphiaLoggerFactory; import org.mongodb.morphia.logging.slf4j.SLF4JLoggerImplFactory; +import redis.clients.jedis.JedisPool; import java.io.FileInputStream; import java.io.InputStream; @@ -55,6 +56,7 @@ public final class APIv3 { @Getter private static Datastore datastore; @Getter private static Properties config = new Properties(); + @Getter private static JedisPool redisPool; @Getter private static final Gson gson = new GsonBuilder() .registerTypeAdapter(ObjectId.class, new ObjectIdTypeAdapter()) .setExclusionStrategies(new FollowAnnotationExclusionStrategy()) @@ -65,6 +67,7 @@ public final class APIv3 { setupConfig(); setupDatabase(); + setupRedis(); setupHttp(); } @@ -97,6 +100,13 @@ public final class APIv3 { datastore.ensureIndexes(); } + private void setupRedis() { + redisPool = new JedisPool( + config.getProperty("redis.address"), + Integer.parseInt(config.getProperty("redis.port")) + ); + } + private void setupHttp() { ipAddress(config.getProperty("http.address")); port(Integer.parseInt(config.getProperty("http.port"))); @@ -106,6 +116,8 @@ public final class APIv3 { before(new AuthorizationFilter()); exception(Exception.class, new LoggingExceptionHandler()); + // TODO: The commented out routes + get("/announcements", new GETAnnouncements(), gson::toJson); get("/auditLog", new GETAuditLog(), gson::toJson); get("/chatFilterList", new GETChatFilterList(), gson::toJson); @@ -151,8 +163,9 @@ public final class APIv3 { get("/user/:id/meta/:serverGroup", new GETUserMeta(), gson::toJson); get("/user/:id/grants", new GETUserGrants(), gson::toJson); get("/user/:id/ipLog", new GETUserIPLog(), gson::toJson); - post("/user/:id/verifyTOTP", new POSTUserVerifyTOTP(), gson::toJson); + get("/user/:id/requiresTOTP", new GETUserRequiresTOTP(), 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); post("/user/:id:/punish", new POSTUserPunish(), gson::toJson); post("/user/:id/login", new POSTUserLogin(), gson::toJson); diff --git a/src/main/java/net/frozenorb/apiv3/filters/ActorAttributeFilter.java b/src/main/java/net/frozenorb/apiv3/filters/ActorAttributeFilter.java index 6f66ac2..86f0712 100644 --- a/src/main/java/net/frozenorb/apiv3/filters/ActorAttributeFilter.java +++ b/src/main/java/net/frozenorb/apiv3/filters/ActorAttributeFilter.java @@ -26,6 +26,7 @@ public final class ActorAttributeFilter implements Filter { } } + @SuppressWarnings("deprecation") // We purposely get the User by their last username. private Actor processBasicAuthorization(String authHeader) { String encodedHeader = authHeader.substring("Basic ".length()); String[] credentials = Base64.base64Decode(encodedHeader).split(":"); diff --git a/src/main/java/net/frozenorb/apiv3/routes/auditLog/GETAuditLog.java b/src/main/java/net/frozenorb/apiv3/routes/auditLog/GETAuditLog.java index cfa987a..4b46517 100644 --- a/src/main/java/net/frozenorb/apiv3/routes/auditLog/GETAuditLog.java +++ b/src/main/java/net/frozenorb/apiv3/routes/auditLog/GETAuditLog.java @@ -2,6 +2,7 @@ package net.frozenorb.apiv3.routes.auditLog; import net.frozenorb.apiv3.APIv3; import net.frozenorb.apiv3.models.AuditLogEntry; +import net.frozenorb.apiv3.utils.ErrorUtils; import spark.Request; import spark.Response; import spark.Route; @@ -9,10 +10,14 @@ import spark.Route; public final class GETAuditLog implements Route { public Object handle(Request req, Response res) { - int limit = req.queryParams("limit") == null ? 100 : Integer.parseInt(req.queryParams("limit")); - int offset = req.queryParams("offset") == null ? 0 : Integer.parseInt(req.queryParams("offset")); + try { + int limit = req.queryParams("limit") == null ? 100 : Integer.parseInt(req.queryParams("limit")); + int offset = req.queryParams("offset") == null ? 0 : Integer.parseInt(req.queryParams("offset")); - return APIv3.getDatastore().createQuery(AuditLogEntry.class).order("performedAt").limit(limit).offset(offset).asList(); + return APIv3.getDatastore().createQuery(AuditLogEntry.class).order("performedAt").limit(limit).offset(offset).asList(); + } catch (NumberFormatException ex) { + return ErrorUtils.invalidInput(";imit and offset must be numerical inputs."); + } } } \ 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 3bc3142..66ac4e6 100644 --- a/src/main/java/net/frozenorb/apiv3/routes/grants/DELETEGrant.java +++ b/src/main/java/net/frozenorb/apiv3/routes/grants/DELETEGrant.java @@ -31,7 +31,11 @@ public final class DELETEGrant 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"); + } grant.delete(removedBy, reason); // TODO: Fix IP diff --git a/src/main/java/net/frozenorb/apiv3/routes/grants/GETGrants.java b/src/main/java/net/frozenorb/apiv3/routes/grants/GETGrants.java index 33a03c0..37c33ca 100644 --- a/src/main/java/net/frozenorb/apiv3/routes/grants/GETGrants.java +++ b/src/main/java/net/frozenorb/apiv3/routes/grants/GETGrants.java @@ -2,6 +2,7 @@ package net.frozenorb.apiv3.routes.grants; import net.frozenorb.apiv3.APIv3; import net.frozenorb.apiv3.models.Grant; +import net.frozenorb.apiv3.utils.ErrorUtils; import spark.Request; import spark.Response; import spark.Route; @@ -9,10 +10,14 @@ import spark.Route; public final class GETGrants implements Route { public Object handle(Request req, Response res) { - int limit = req.queryParams("limit") == null ? 100 : Integer.parseInt(req.queryParams("limit")); - int offset = req.queryParams("offset") == null ? 0 : Integer.parseInt(req.queryParams("offset")); + try { + int limit = req.queryParams("limit") == null ? 100 : Integer.parseInt(req.queryParams("limit")); + int offset = req.queryParams("offset") == null ? 0 : Integer.parseInt(req.queryParams("offset")); - return APIv3.getDatastore().createQuery(Grant.class).order("addedAt").limit(limit).offset(offset).asList(); + return APIv3.getDatastore().createQuery(Grant.class).order("addedAt").limit(limit).offset(offset).asList(); + } catch (NumberFormatException ex) { + return ErrorUtils.invalidInput("limit and offset must be numerical inputs."); + } } } \ No newline at end of file diff --git a/src/main/java/net/frozenorb/apiv3/routes/grants/POSTUserGrant.java b/src/main/java/net/frozenorb/apiv3/routes/grants/POSTUserGrant.java index dee4623..25a4d49 100644 --- a/src/main/java/net/frozenorb/apiv3/routes/grants/POSTUserGrant.java +++ b/src/main/java/net/frozenorb/apiv3/routes/grants/POSTUserGrant.java @@ -26,8 +26,8 @@ public final class POSTUserGrant implements Route { String reason = req.queryParams("reason"); - if (reason.trim().isEmpty()) { - return ErrorUtils.invalidInput("A reason must be provided."); + if (reason == null || reason.trim().isEmpty()) { + return ErrorUtils.requiredInput("reason"); } Set scopes = new HashSet<>(); 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 813b2d0..bccac9a 100644 --- a/src/main/java/net/frozenorb/apiv3/routes/punishments/DELETEPunishment.java +++ b/src/main/java/net/frozenorb/apiv3/routes/punishments/DELETEPunishment.java @@ -33,6 +33,10 @@ public final class DELETEPunishment implements Route { String reason = req.queryParams("removalReason"); + if (reason == null || reason.trim().isEmpty()) { + return ErrorUtils.requiredInput("reason"); + } + punishment.delete(removedBy, reason); // TODO: Fix IP AuditLog.log(removedBy, "", req.attribute("actor"), AuditLogActionType.DELETE_PUNISHMENT, new Document("punishmentId", punishment.getId())); diff --git a/src/main/java/net/frozenorb/apiv3/routes/punishments/GETPunishments.java b/src/main/java/net/frozenorb/apiv3/routes/punishments/GETPunishments.java index 54b6390..52b1946 100644 --- a/src/main/java/net/frozenorb/apiv3/routes/punishments/GETPunishments.java +++ b/src/main/java/net/frozenorb/apiv3/routes/punishments/GETPunishments.java @@ -2,6 +2,7 @@ package net.frozenorb.apiv3.routes.punishments; import net.frozenorb.apiv3.APIv3; import net.frozenorb.apiv3.models.Punishment; +import net.frozenorb.apiv3.utils.ErrorUtils; import spark.Request; import spark.Response; import spark.Route; @@ -9,10 +10,14 @@ import spark.Route; public final class GETPunishments implements Route { public Object handle(Request req, Response res) { - int limit = req.queryParams("limit") == null ? 100 : Integer.parseInt(req.queryParams("limit")); - int offset = req.queryParams("offset") == null ? 0 : Integer.parseInt(req.queryParams("offset")); + try { + int limit = req.queryParams("limit") == null ? 100 : Integer.parseInt(req.queryParams("limit")); + int offset = req.queryParams("offset") == null ? 0 : Integer.parseInt(req.queryParams("offset")); - return APIv3.getDatastore().createQuery(Punishment.class).order("addedAt").limit(limit).offset(offset).asList(); + return APIv3.getDatastore().createQuery(Punishment.class).order("addedAt").limit(limit).offset(offset).asList(); + } catch (NumberFormatException ex) { + return ErrorUtils.invalidInput("limit and offset must be numerical inputs."); + } } } \ No newline at end of file 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 07479e0..503f2bb 100644 --- a/src/main/java/net/frozenorb/apiv3/routes/punishments/POSTUserPunish.java +++ b/src/main/java/net/frozenorb/apiv3/routes/punishments/POSTUserPunish.java @@ -17,14 +17,16 @@ 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")); } String reason = req.queryParams("reason"); - if (reason.trim().isEmpty()) { - return ErrorUtils.invalidInput("A reason must be provided."); + if (reason == null || reason.trim().isEmpty()) { + return ErrorUtils.requiredInput("reason"); } Punishment.PunishmentType type = Punishment.PunishmentType.valueOf(req.queryParams("type")); diff --git a/src/main/java/net/frozenorb/apiv3/routes/servers/POSTServer.java b/src/main/java/net/frozenorb/apiv3/routes/servers/POSTServer.java index f182963..c92281c 100644 --- a/src/main/java/net/frozenorb/apiv3/routes/servers/POSTServer.java +++ b/src/main/java/net/frozenorb/apiv3/routes/servers/POSTServer.java @@ -4,6 +4,7 @@ import net.frozenorb.apiv3.APIv3; import net.frozenorb.apiv3.models.Server; import net.frozenorb.apiv3.models.ServerGroup; import net.frozenorb.apiv3.utils.ErrorUtils; +import net.frozenorb.apiv3.utils.IPUtils; import spark.Request; import spark.Response; import spark.Route; @@ -22,6 +23,10 @@ public final class POSTServer implements Route { return ErrorUtils.notFound("Server group", req.queryParams("group")); } + if (!IPUtils.isValidIP(ip)) { + return ErrorUtils.invalidInput("IP address \"" + ip + "\" is not valid."); + } + Server server = new Server(id, bungeeId, displayName, apiKey, group, ip); APIv3.getDatastore().save(server); return server; diff --git a/src/main/java/net/frozenorb/apiv3/routes/servers/POSTServerMetrics.java b/src/main/java/net/frozenorb/apiv3/routes/servers/POSTServerMetrics.java index 2ae3429..048aa21 100644 --- a/src/main/java/net/frozenorb/apiv3/routes/servers/POSTServerMetrics.java +++ b/src/main/java/net/frozenorb/apiv3/routes/servers/POSTServerMetrics.java @@ -1,20 +1,14 @@ package net.frozenorb.apiv3.routes.servers; import com.google.common.collect.ImmutableMap; -import com.librato.metrics.LibratoBatch; -import net.frozenorb.apiv3.APIv3; import net.frozenorb.apiv3.actors.Actor; import net.frozenorb.apiv3.actors.ActorType; import net.frozenorb.apiv3.models.Server; -import net.frozenorb.apiv3.models.User; import net.frozenorb.apiv3.utils.ErrorUtils; -import org.bson.Document; import spark.Request; import spark.Response; import spark.Route; -import java.util.*; - public final class POSTServerMetrics implements Route { public Object handle(Request req, Response res) { 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 701ef24..c0e68f0 100644 --- a/src/main/java/net/frozenorb/apiv3/routes/users/GETUserDetails.java +++ b/src/main/java/net/frozenorb/apiv3/routes/users/GETUserDetails.java @@ -20,7 +20,8 @@ public final class GETUserDetails implements Route { "user", user, "grants", user.getGrants(), "ipLog", user.getIPLog(), - "punishments", user.getPunishments() + "punishments", user.getPunishments(), + "totpRequired", user.getTotpSecret() != null ); } diff --git a/src/main/java/net/frozenorb/apiv3/routes/users/GETUserRequiresTOTP.java b/src/main/java/net/frozenorb/apiv3/routes/users/GETUserRequiresTOTP.java new file mode 100644 index 0000000..7595acd --- /dev/null +++ b/src/main/java/net/frozenorb/apiv3/routes/users/GETUserRequiresTOTP.java @@ -0,0 +1,4 @@ +package net.frozenorb.apiv3.routes.users; + +public class GETUserRequiresTOTP { +} diff --git a/src/main/java/net/frozenorb/apiv3/routes/users/POSTUserLogin.java b/src/main/java/net/frozenorb/apiv3/routes/users/POSTUserLogin.java index f3c16b9..77fe0b2 100644 --- a/src/main/java/net/frozenorb/apiv3/routes/users/POSTUserLogin.java +++ b/src/main/java/net/frozenorb/apiv3/routes/users/POSTUserLogin.java @@ -6,6 +6,7 @@ import net.frozenorb.apiv3.actors.ActorType; import net.frozenorb.apiv3.models.Server; import net.frozenorb.apiv3.models.User; import net.frozenorb.apiv3.utils.ErrorUtils; +import net.frozenorb.apiv3.utils.IPUtils; import spark.Request; import spark.Response; import spark.Route; @@ -24,6 +25,10 @@ public final class POSTUserLogin implements Route { return ErrorUtils.serverOnly(); } + if (!IPUtils.isValidIP(userIp)) { + return ErrorUtils.invalidInput("IP address \"" + userIp + "\" is not valid."); + } + if (user == null) { user = new User(UUID.fromString(req.params("id")), username); APIv3.getDatastore().save(user); 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 e6f3c60..e8dd2b3 100644 --- a/src/main/java/net/frozenorb/apiv3/routes/users/POSTUserRegister.java +++ b/src/main/java/net/frozenorb/apiv3/routes/users/POSTUserRegister.java @@ -19,9 +19,12 @@ import java.util.regex.Pattern; public final class POSTUserRegister implements Route { - private static final Pattern VALID_EMAIL_ADDRESS_REGEX = + 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 + public Object handle(Request req, Response res) { User user = User.byId(req.params("id")); @@ -35,7 +38,7 @@ public final class POSTUserRegister implements Route { String email = req.queryParams("email"); - if (!VALID_EMAIL_ADDRESS_REGEX.matcher(email).find()) { + if (!VALID_EMAIL_PATTERN.matcher(email).matches()) { return ErrorUtils.error(email + " is not a valid email."); } diff --git a/src/main/java/net/frozenorb/apiv3/unsorted/LoggingExceptionHandler.java b/src/main/java/net/frozenorb/apiv3/unsorted/LoggingExceptionHandler.java index 4eb4d47..645fee5 100644 --- a/src/main/java/net/frozenorb/apiv3/unsorted/LoggingExceptionHandler.java +++ b/src/main/java/net/frozenorb/apiv3/unsorted/LoggingExceptionHandler.java @@ -15,7 +15,7 @@ public final class LoggingExceptionHandler implements ExceptionHandler { String code = new ObjectId().toHexString(); log.error(code + ":", ex); - res.body(APIv3.getGson().toJson(ErrorUtils.error("An unknown error has occurred. Please contact a developer with the code \"" + code + "\"."))); + res.body(APIv3.getGson().toJson(ErrorUtils.error("An unknown error has occurred. Please contact an API developer with the code \"" + code + "\"."))); } } \ 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 2f6980d..908c82f 100644 --- a/src/main/java/net/frozenorb/apiv3/unsorted/Notification.java +++ b/src/main/java/net/frozenorb/apiv3/unsorted/Notification.java @@ -45,6 +45,7 @@ public final class Notification { } public void sendAsText(String phoneNumber) throws IOException { + // TODO throw new IOException(new NotImplementedException()); } diff --git a/src/main/java/net/frozenorb/apiv3/unsorted/Permissions.java b/src/main/java/net/frozenorb/apiv3/unsorted/Permissions.java index 143b0b8..7d31a74 100644 --- a/src/main/java/net/frozenorb/apiv3/unsorted/Permissions.java +++ b/src/main/java/net/frozenorb/apiv3/unsorted/Permissions.java @@ -9,6 +9,6 @@ public class Permissions { public static final String CREATE_GRANT = "minehq.grant.create"; // minehq.grant.create.%RANK% public static final String REMOVE_PUNISHMENT = "minehq.punishment.remove"; // minehq.punishment.remove.%TYPE% - public static final String CREATE_PUNISHMENT = "minehq.punishment.create"; // minehq.punishment.remove.%TYPE% + public static final String CREATE_PUNISHMENT = "minehq.punishment.create"; // minehq.punishment.create.%TYPE% } \ No newline at end of file diff --git a/src/main/java/net/frozenorb/apiv3/utils/ErrorUtils.java b/src/main/java/net/frozenorb/apiv3/utils/ErrorUtils.java index a8ae71a..74a839c 100644 --- a/src/main/java/net/frozenorb/apiv3/utils/ErrorUtils.java +++ b/src/main/java/net/frozenorb/apiv3/utils/ErrorUtils.java @@ -24,6 +24,10 @@ public class ErrorUtils { return error("Invalid input: " + message); } + public static Map requiredInput(String field) { + return error("Field \"" + field + "\" is required."); + } + public static Map error(String message) { return ImmutableMap.of( "success", false, diff --git a/src/main/java/net/frozenorb/apiv3/utils/IPUtils.java b/src/main/java/net/frozenorb/apiv3/utils/IPUtils.java new file mode 100644 index 0000000..7f868e9 --- /dev/null +++ b/src/main/java/net/frozenorb/apiv3/utils/IPUtils.java @@ -0,0 +1,20 @@ +package net.frozenorb.apiv3.utils; + +import lombok.experimental.UtilityClass; + +import java.util.regex.Pattern; + +@UtilityClass +public class IPUtils { + + private static final Pattern VALID_IP_PATTERN = Pattern.compile( + "^([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\." + + "([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\." + + "([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\." + + "([01]?\\d\\d?|2[0-4]\\d|25[0-5])$"); + + public static boolean isValidIP(String ip) { + return ip != null && VALID_IP_PATTERN.matcher(ip).matches(); + } + +} \ No newline at end of file diff --git a/src/main/java/net/frozenorb/apiv3/utils/TOTPUtils.java b/src/main/java/net/frozenorb/apiv3/utils/TOTPUtils.java index 0438650..04c954f 100644 --- a/src/main/java/net/frozenorb/apiv3/utils/TOTPUtils.java +++ b/src/main/java/net/frozenorb/apiv3/utils/TOTPUtils.java @@ -1,12 +1,14 @@ package net.frozenorb.apiv3.utils; -import com.google.common.collect.ImmutableList; import com.warrenstrange.googleauth.GoogleAuthenticator; import com.warrenstrange.googleauth.GoogleAuthenticatorKey; import com.warrenstrange.googleauth.GoogleAuthenticatorQRGenerator; import lombok.experimental.UtilityClass; +import net.frozenorb.apiv3.APIv3; import net.frozenorb.apiv3.models.User; -import org.apache.http.client.utils.URIBuilder; +import redis.clients.jedis.Jedis; + +import java.util.concurrent.TimeUnit; @UtilityClass public class TOTPUtils { @@ -23,18 +25,40 @@ public class TOTPUtils { public static String getQRCodeURL(User user, GoogleAuthenticatorKey key) { return GoogleAuthenticatorQRGenerator.getOtpAuthURL( - "MineHQ v3", + "MineHQ Network", user.getLastUsername(), key ); } - public static boolean wasRecentlyUsed(User user, int code) { + public static boolean isPreAuthorized(User user, String ip) { + try (Jedis redis = APIv3.getRedisPool().getResource()) { + return redis.exists(user.getId() + ":preAuthorizedIP:" + ip.toLowerCase()); + } + } + public static void markPreAuthorized(User user, String ip, long duration, TimeUnit unit) { + try (Jedis redis = APIv3.getRedisPool().getResource()) { + String key = user.getId() + ":preAuthorizedIP:" + ip.toLowerCase(); + + redis.set(key, ""); + redis.expire(key, (int) unit.toSeconds(duration)); + } + } + + public static boolean wasRecentlyUsed(User user, int code) { + try (Jedis redis = APIv3.getRedisPool().getResource()) { + return redis.exists(user.getId() + ":recentTOTPCodes:" + code); + } } public static void markRecentlyUsed(User user, int code) { + try (Jedis redis = APIv3.getRedisPool().getResource()) { + String key = user.getId() + ":recentTOTPCodes:" + code; + redis.set(key, ""); + redis.expire(key, (int) TimeUnit.MINUTES.toSeconds(5)); + } } } \ No newline at end of file