Skip to content

Commit

Permalink
Merge branch '7.0' into feat/mfa
Browse files Browse the repository at this point in the history
  • Loading branch information
sattvikc authored Nov 6, 2023
2 parents 6cd3c59 + b9d2044 commit 51f1da7
Show file tree
Hide file tree
Showing 14 changed files with 708 additions and 23 deletions.
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,15 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres
to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [7.0.10] - 2023-11-03

- Collects requests stats per app
- Adds `/requests/stats` API to return requests stats for the last day

## [7.0.9] - 2023-11-01

- Tests `verified` in `loginMethods` for users with userId mapping

## [7.0.8] - 2023-10-19

- Tests thirdParty serialization fix
Expand Down
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ compileTestJava { options.encoding = "UTF-8" }
// }
//}

version = "7.0.8"
version = "7.0.10"


repositories {
Expand Down
Binary file modified cli/jar/cli.jar
Binary file not shown.
Binary file modified downloader/jar/downloader.jar
Binary file not shown.
Binary file modified ee/jar/ee.jar
Binary file not shown.
Binary file renamed jar/core-7.0.8.jar → jar/core-7.0.10.jar
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -243,26 +243,50 @@ public static List<String> isEmailVerified_transaction(Start start, Connection s
return new ArrayList<>();
}
List<String> emails = new ArrayList<>();
List<String> userIds = new ArrayList<>();
Map<String, String> userIdToEmailMap = new HashMap<>();
List<String> supertokensUserIds = new ArrayList<>();
for (UserIdAndEmail ue : userIdAndEmail) {
emails.add(ue.email);
userIds.add(ue.userId);
supertokensUserIds.add(ue.userId);
}

// We have external user id stored in the email verification table, so we need to fetch the mapped userids for
// calculating the verified emails

HashMap<String, String> supertokensUserIdToExternalUserIdMap = UserIdMappingQueries.getUserIdMappingWithUserIds_Transaction(start,
sqlCon, supertokensUserIds);
HashMap<String, String> externalUserIdToSupertokensUserIdMap = new HashMap<>();

List<String> supertokensOrExternalUserIdsToQuery = new ArrayList<>();
for (String userId : supertokensUserIds) {
if (supertokensUserIdToExternalUserIdMap.containsKey(userId)) {
supertokensOrExternalUserIdsToQuery.add(supertokensUserIdToExternalUserIdMap.get(userId));
externalUserIdToSupertokensUserIdMap.put(supertokensUserIdToExternalUserIdMap.get(userId), userId);
} else {
supertokensOrExternalUserIdsToQuery.add(userId);
externalUserIdToSupertokensUserIdMap.put(userId, userId);
}
}

Map<String, String> supertokensOrExternalUserIdToEmailMap = new HashMap<>();
for (UserIdAndEmail ue : userIdAndEmail) {
if (userIdToEmailMap.containsKey(ue.userId)) {
String supertokensOrExternalUserId = ue.userId;
if (supertokensUserIdToExternalUserIdMap.containsKey(supertokensOrExternalUserId)) {
supertokensOrExternalUserId = supertokensUserIdToExternalUserIdMap.get(supertokensOrExternalUserId);
}
if (supertokensOrExternalUserIdToEmailMap.containsKey(supertokensOrExternalUserId)) {
throw new RuntimeException("Found a bug!");
}
userIdToEmailMap.put(ue.userId, ue.email);
supertokensOrExternalUserIdToEmailMap.put(supertokensOrExternalUserId, ue.email);
}

String QUERY = "SELECT * FROM " + getConfig(start).getEmailVerificationTable()
+ " WHERE app_id = ? AND user_id IN (" + Utils.generateCommaSeperatedQuestionMarks(userIds.size()) +
+ " WHERE app_id = ? AND user_id IN (" + Utils.generateCommaSeperatedQuestionMarks(supertokensOrExternalUserIdsToQuery.size()) +
") AND email IN (" + Utils.generateCommaSeperatedQuestionMarks(emails.size()) + ")";

return execute(sqlCon, QUERY, pst -> {
pst.setString(1, appIdentifier.getAppId());
int index = 2;
for (String userId : userIds) {
for (String userId : supertokensOrExternalUserIdsToQuery) {
pst.setString(index++, userId);
}
for (String email : emails) {
Expand All @@ -271,10 +295,10 @@ public static List<String> isEmailVerified_transaction(Start start, Connection s
}, result -> {
List<String> res = new ArrayList<>();
while (result.next()) {
String userId = result.getString("user_id");
String supertokensOrExternalUserId = result.getString("user_id");
String email = result.getString("email");
if (Objects.equals(userIdToEmailMap.get(userId), email)) {
res.add(userId);
if (Objects.equals(supertokensOrExternalUserIdToEmailMap.get(supertokensOrExternalUserId), email)) {
res.add(externalUserIdToSupertokensUserIdMap.get(supertokensOrExternalUserId));
}
}
return res;
Expand All @@ -288,26 +312,46 @@ public static List<String> isEmailVerified(Start start, AppIdentifier appIdentif
return new ArrayList<>();
}
List<String> emails = new ArrayList<>();
List<String> userIds = new ArrayList<>();
Map<String, String> userIdToEmailMap = new HashMap<>();
List<String> supertokensUserIds = new ArrayList<>();

for (UserIdAndEmail ue : userIdAndEmail) {
emails.add(ue.email);
userIds.add(ue.userId);
supertokensUserIds.add(ue.userId);
}
// We have external user id stored in the email verification table, so we need to fetch the mapped userids for
// calculating the verified emails
HashMap<String, String> supertokensUserIdToExternalUserIdMap = UserIdMappingQueries.getUserIdMappingWithUserIds(start,
supertokensUserIds);
HashMap<String, String> externalUserIdToSupertokensUserIdMap = new HashMap<>();
List<String> supertokensOrExternalUserIdsToQuery = new ArrayList<>();
for (String userId : supertokensUserIds) {
if (supertokensUserIdToExternalUserIdMap.containsKey(userId)) {
supertokensOrExternalUserIdsToQuery.add(supertokensUserIdToExternalUserIdMap.get(userId));
externalUserIdToSupertokensUserIdMap.put(supertokensUserIdToExternalUserIdMap.get(userId), userId);
} else {
supertokensOrExternalUserIdsToQuery.add(userId);
externalUserIdToSupertokensUserIdMap.put(userId, userId);
}
}

Map<String, String> supertokensOrExternalUserIdToEmailMap = new HashMap<>();
for (UserIdAndEmail ue : userIdAndEmail) {
if (userIdToEmailMap.containsKey(ue.userId)) {
String supertokensOrExternalUserId = ue.userId;
if (supertokensUserIdToExternalUserIdMap.containsKey(supertokensOrExternalUserId)) {
supertokensOrExternalUserId = supertokensUserIdToExternalUserIdMap.get(supertokensOrExternalUserId);
}
if (supertokensOrExternalUserIdToEmailMap.containsKey(supertokensOrExternalUserId)) {
throw new RuntimeException("Found a bug!");
}
userIdToEmailMap.put(ue.userId, ue.email);
supertokensOrExternalUserIdToEmailMap.put(supertokensOrExternalUserId, ue.email);
}
String QUERY = "SELECT * FROM " + getConfig(start).getEmailVerificationTable()
+ " WHERE app_id = ? AND user_id IN (" + Utils.generateCommaSeperatedQuestionMarks(userIds.size()) +
+ " WHERE app_id = ? AND user_id IN (" + Utils.generateCommaSeperatedQuestionMarks(supertokensOrExternalUserIdsToQuery.size()) +
") AND email IN (" + Utils.generateCommaSeperatedQuestionMarks(emails.size()) + ")";

return execute(start, QUERY, pst -> {
pst.setString(1, appIdentifier.getAppId());
int index = 2;
for (String userId : userIds) {
for (String userId : supertokensOrExternalUserIdsToQuery) {
pst.setString(index++, userId);
}
for (String email : emails) {
Expand All @@ -316,10 +360,10 @@ public static List<String> isEmailVerified(Start start, AppIdentifier appIdentif
}, result -> {
List<String> res = new ArrayList<>();
while (result.next()) {
String userId = result.getString("user_id");
String supertokensOrExternalUserId = result.getString("user_id");
String email = result.getString("email");
if (Objects.equals(userIdToEmailMap.get(userId), email)) {
res.add(userId);
if (Objects.equals(supertokensOrExternalUserIdToEmailMap.get(supertokensOrExternalUserId), email)) {
res.add(externalUserIdToSupertokensUserIdMap.get(supertokensOrExternalUserId));
}
}
return res;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

import static io.supertokens.inmemorydb.QueryExecutorTemplate.execute;
import static io.supertokens.inmemorydb.QueryExecutorTemplate.update;
Expand Down Expand Up @@ -135,7 +136,7 @@ public static UserIdMapping[] getUserIdMappingWithEitherSuperTokensUserIdOrExter

}

public static HashMap<String, String> getUserIdMappingWithUserIds(Start start, ArrayList<String> userIds)
public static HashMap<String, String> getUserIdMappingWithUserIds(Start start, List<String> userIds)
throws SQLException, StorageQueryException {

if (userIds.size() == 0) {
Expand Down Expand Up @@ -168,6 +169,39 @@ public static HashMap<String, String> getUserIdMappingWithUserIds(Start start, A
});
}

public static HashMap<String, String> getUserIdMappingWithUserIds_Transaction(Start start, Connection sqlCon, List<String> userIds)
throws SQLException, StorageQueryException {

if (userIds.size() == 0) {
return new HashMap<>();
}

// No need to filter based on tenantId because the id list is already filtered for a tenant
StringBuilder QUERY = new StringBuilder(
"SELECT * FROM " + Config.getConfig(start).getUserIdMappingTable() + " WHERE supertokens_user_id IN (");
for (int i = 0; i < userIds.size(); i++) {
QUERY.append("?");
if (i != userIds.size() - 1) {
// not the last element
QUERY.append(",");
}
}
QUERY.append(")");
return execute(sqlCon, QUERY.toString(), pst -> {
for (int i = 0; i < userIds.size(); i++) {
// i+1 cause this starts with 1 and not 0
pst.setString(i + 1, userIds.get(i));
}
}, result -> {
HashMap<String, String> userIdMappings = new HashMap<>();
while (result.next()) {
UserIdMapping temp = UserIdMappingRowMapper.getInstance().mapOrThrow(result);
userIdMappings.put(temp.superTokensUserId, temp.externalUserId);
}
return userIdMappings;
});
}

public static boolean deleteUserIdMappingWithSuperTokensUserId(Start start, AppIdentifier appIdentifier, String userId)
throws SQLException, StorageQueryException {
String QUERY = "DELETE FROM " + Config.getConfig(start).getUserIdMappingTable()
Expand Down
143 changes: 143 additions & 0 deletions src/main/java/io/supertokens/webserver/RequestStats.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
/*
* Copyright (c) 2023, VRAI Labs and/or its affiliates. All rights reserved.
*
* This software is licensed under the Apache License, Version 2.0 (the
* "License") as published by the Apache Software Foundation.
*
* You may not use this file except in compliance with the License. You may
* obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/

package io.supertokens.webserver;

import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;
import io.supertokens.Main;
import io.supertokens.ResourceDistributor;
import io.supertokens.multitenancy.Multitenancy;
import io.supertokens.pluginInterface.multitenancy.AppIdentifier;
import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException;

public class RequestStats extends ResourceDistributor.SingletonResource {
public static final String RESOURCE_KEY = "io.supertokens.webserver.RequestStats";

private final int MAX_MINUTES = 24 * 60;

private long currentMinute; // current minute since epoch
private final int[] currentMinuteRequestCounts; // array of 60 items representing number of requests at each second in the current minute

// The 2 arrays below contains stats for a day for every minute
// the array is stored in such a way that array[currentMinute % MAX_MINUTES] contains the stats for a day ago
// until array[(currentMinute - 1) % MAX_MINUTES] which contains the stats for the last minute, circling around
// from end of array to the beginning
// for e.g. if currentMinute % MAX_MINUTES = 250,
// then array[250] contains stats for now - 1440 minutes
// array[251] contains stats for now - 1439 minutes
// ...
// array[1439] contains stats for now - 1191 minutes
// array[0] contains stats for now - 1190 minutes
// array[1] contains stats for now - 1189 minutes
// ...
// array[249] contains stats for now - 1 minute
private final double[] averageRequestsPerSecond;
private final int[] peakRequestsPerSecond;

private RequestStats() {
currentMinute = System.currentTimeMillis() / 60000;
currentMinuteRequestCounts = new int[60];

averageRequestsPerSecond = new double[MAX_MINUTES];
peakRequestsPerSecond = new int[MAX_MINUTES];
for (int i = 0; i < MAX_MINUTES; i++) {
averageRequestsPerSecond[i] = -1;
peakRequestsPerSecond[i] = -1;
}
}

private void checkAndUpdateMinute(long currentSecond) {
if (currentSecond / 60 == currentMinute) {
return; // stats update not required
}

int sum = 0;
int max = 0;
for (int i = 0; i < 60; i++) {
sum += currentMinuteRequestCounts[i];
max = Math.max(max, currentMinuteRequestCounts[i]);
}

averageRequestsPerSecond[(int) (currentMinute % MAX_MINUTES)] = sum / 60.0;
peakRequestsPerSecond[(int) (currentMinute % MAX_MINUTES)] = max;

// fill zeros for passed minutes
for (long i = currentMinute + 1; i < currentSecond / 60; i++) {
averageRequestsPerSecond[(int) (i % MAX_MINUTES)] = 0;
peakRequestsPerSecond[(int) (i % MAX_MINUTES)] = 0;
}

currentMinute = currentSecond / 60;
for (int i = 0; i < 60; i++) {
currentMinuteRequestCounts[i] = 0;
}
}

private void updateCounts(long currentSecond) {
currentMinuteRequestCounts[(int) (currentSecond % 60)]++;
}

public static RequestStats getInstance(Main main, AppIdentifier appIdentifier) throws TenantOrAppNotFoundException {
try {
return (RequestStats) main.getResourceDistributor()
.getResource(appIdentifier.getAsPublicTenantIdentifier(), RESOURCE_KEY);
} catch (TenantOrAppNotFoundException e) {
// appIdentifier parameter is coming from the API request and hence we need to check if the app exists
// before creating a resource for it, otherwise someone could fill up memory by making requests for apps
// that don't exist.
// The other resources are created during init or while refreshing tenants from the db, so we don't need
// this kind of pattern for those resources.
if (Multitenancy.getTenantInfo(main, appIdentifier.getAsPublicTenantIdentifier()) == null) {
throw e;
}
return (RequestStats) main.getResourceDistributor()
.setResource(appIdentifier.getAsPublicTenantIdentifier(), RESOURCE_KEY, new RequestStats());
}
}

public void updateRequestStats() {
this.updateRequestStats(true);
}

synchronized private void updateRequestStats(boolean updateCounts) {
long now = System.currentTimeMillis() / 1000;
this.checkAndUpdateMinute(now);
if (updateCounts) { this.updateCounts(now); }
}

public JsonObject getStats() {
this.updateRequestStats(false);

JsonArray avgRps = new JsonArray();
JsonArray peakRps = new JsonArray();

long atMinute = System.currentTimeMillis() / 60000;

int offset = (int) (atMinute % MAX_MINUTES);
for (int i = 0; i < MAX_MINUTES; i++) {
avgRps.add(new JsonPrimitive(this.averageRequestsPerSecond[(i + offset) % MAX_MINUTES]));
peakRps.add(new JsonPrimitive(this.peakRequestsPerSecond[(i + offset) % MAX_MINUTES]));
}

JsonObject result = new JsonObject();
result.addProperty("atMinute", atMinute);
result.add("averageRequestsPerSecond", avgRps);
result.add("peakRequestsPerSecond", peakRps);
return result;
}
}
2 changes: 2 additions & 0 deletions src/main/java/io/supertokens/webserver/Webserver.java
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,8 @@ private void setupRoutes() {
addAPI(new io.supertokens.webserver.api.mfa.CreatePrimaryUserAPI(main));
addAPI(new io.supertokens.webserver.api.mfa.LinkAccountsAPI(main));

addAPI(new RequestStatsAPI(main));

StandardContext context = tomcatReference.getContext();
Tomcat tomcat = tomcatReference.getTomcat();

Expand Down
5 changes: 5 additions & 0 deletions src/main/java/io/supertokens/webserver/WebserverAPI.java
Original file line number Diff line number Diff line change
Expand Up @@ -493,6 +493,11 @@ protected void service(HttpServletRequest req, HttpServletResponse resp) throws
}
Logging.info(main, tenantIdentifier, "API ended: " + req.getRequestURI() + ". Method: " + req.getMethod(),
false);
try {
RequestStats.getInstance(main, tenantIdentifier.toAppIdentifier()).updateRequestStats();
} catch (TenantOrAppNotFoundException e) {
// Ignore the error as we would have already sent the response for tenantNotFound
}
}

protected String getRIDFromRequest(HttpServletRequest req) {
Expand Down
Loading

0 comments on commit 51f1da7

Please sign in to comment.