Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add BulkImport APIs and cron #966

Closed
wants to merge 22 commits into from
Closed
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
443 changes: 436 additions & 7 deletions src/main/java/io/supertokens/bulkimport/BulkImport.java

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -565,6 +565,7 @@ private void validateTenantIdsForRoleAndLoginMethods(Main main, AppIdentifier ap
} else if (!commonTenantUserPoolId.equals(tenantUserPoolId)) {
errors.add("All tenants for a user must share the same database for " + loginMethod.recipeId
+ " recipe.");
rishabhpoddar marked this conversation as resolved.
Show resolved Hide resolved
break; // Break to avoid adding the same error multiple times for the same loginMethod
}
}
}
Expand Down

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions src/main/java/io/supertokens/storageLayer/StorageLayer.java
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,9 @@ private static Storage getNewInstance(Main main, JsonObject config, TenantIdenti
result = storageLayer;
}
} else {
if (isBulkImportProxy) {
throw new QuitProgramException("Creating a bulk import proxy storage instance with in-memory DB is not supported.");
}
result = new Start(main);
rishabhpoddar marked this conversation as resolved.
Show resolved Hide resolved
}
}
Expand Down
4 changes: 4 additions & 0 deletions src/main/java/io/supertokens/webserver/Webserver.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@
import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException;
import io.supertokens.webserver.api.accountlinking.*;
import io.supertokens.webserver.api.bulkimport.BulkImportAPI;
import io.supertokens.webserver.api.bulkimport.CountBulkImportUsersAPI;
import io.supertokens.webserver.api.bulkimport.DeleteBulkImportUserAPI;
tamassoltesz marked this conversation as resolved.
Show resolved Hide resolved
import io.supertokens.webserver.api.bulkimport.ImportUserAPI;
import io.supertokens.webserver.api.core.*;
import io.supertokens.webserver.api.dashboard.*;
import io.supertokens.webserver.api.emailpassword.UserAPI;
Expand Down Expand Up @@ -264,6 +266,8 @@ private void setupRoutes() {

addAPI(new BulkImportAPI(main));
addAPI(new DeleteBulkImportUserAPI(main));
addAPI(new ImportUserAPI(main));
addAPI(new CountBulkImportUsersAPI(main));

StandardContext context = tomcatReference.getContext();
Tomcat tomcat = tomcatReference.getTomcat();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,9 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws Se
Integer limit = InputParser.getIntQueryParamOrThrowError(req, "limit", true);

if (limit != null) {
if (limit > BulkImport.GET_USERS_PAGINATION_LIMIT) {
if (limit > BulkImport.GET_USERS_PAGINATION_MAX_LIMIT) {
throw new ServletException(
new BadRequestException("Max limit allowed is " + BulkImport.GET_USERS_PAGINATION_LIMIT));
new BadRequestException("Max limit allowed is " + BulkImport.GET_USERS_PAGINATION_MAX_LIMIT));
} else if (limit < 1) {
throw new ServletException(new BadRequestException("limit must a positive integer with min value 1"));
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/*
* Copyright (c) 2024, 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.api.bulkimport;

import java.io.IOException;

import com.google.gson.JsonObject;

import io.supertokens.Main;
import io.supertokens.bulkimport.BulkImport;
import io.supertokens.multitenancy.exception.BadPermissionException;
import io.supertokens.pluginInterface.Storage;
import io.supertokens.pluginInterface.bulkimport.BulkImportStorage.BULK_IMPORT_USER_STATUS;
import io.supertokens.pluginInterface.exceptions.StorageQueryException;
import io.supertokens.pluginInterface.multitenancy.AppIdentifier;
import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException;
import io.supertokens.storageLayer.StorageLayer;
import io.supertokens.webserver.InputParser;
import io.supertokens.webserver.WebserverAPI;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

public class CountBulkImportUsersAPI extends WebserverAPI {
public CountBulkImportUsersAPI(Main main) {
super(main, "");
}

@Override
public String getPath() {
return "/bulk-import/users/count";
}

@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// API is app specific

if (StorageLayer.isInMemDb(main)) {
throw new ServletException(new BadRequestException("This API is not supported in the in-memory database."));
}

String statusString = InputParser.getQueryParamOrThrowError(req, "status", true);

BULK_IMPORT_USER_STATUS status = null;
if (statusString != null) {
try {
status = BULK_IMPORT_USER_STATUS.valueOf(statusString);
} catch (IllegalArgumentException e) {
throw new ServletException(
new BadRequestException("Invalid value for status. Pass one of NEW, PROCESSING, or FAILED!"));
}
}

AppIdentifier appIdentifier = null;
Storage storage = null;

try {
appIdentifier = getAppIdentifier(req);
storage = enforcePublicTenantAndGetPublicTenantStorage(req);

long count = BulkImport.getBulkImportUsersCount(appIdentifier, storage, status);

JsonObject result = new JsonObject();
result.addProperty("status", "OK");
result.addProperty("count", count);
super.sendJsonResponse(200, result, resp);

} catch (TenantOrAppNotFoundException | BadPermissionException | StorageQueryException e) {
throw new ServletException(e);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,9 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws S
throw new ServletException(new WebserverAPI.BadRequestException("Field name 'ids' cannot be an empty array"));
}

if (arr.size() > BulkImport.DELETE_USERS_LIMIT) {
if (arr.size() > BulkImport.DELETE_USERS_MAX_LIMIT) {
throw new ServletException(new WebserverAPI.BadRequestException("Field name 'ids' cannot contain more than "
+ BulkImport.DELETE_USERS_LIMIT + " elements"));
+ BulkImport.DELETE_USERS_MAX_LIMIT + " elements"));
}

String[] userIds = new String[arr.size()];
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/*
* Copyright (c) 2024, 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.api.bulkimport;

import java.io.IOException;

import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;

import io.supertokens.Main;
import io.supertokens.bulkimport.BulkImport;
import io.supertokens.bulkimport.BulkImportUserUtils;
import io.supertokens.multitenancy.exception.BadPermissionException;
import io.supertokens.pluginInterface.Storage;
import io.supertokens.pluginInterface.StorageUtils;
import io.supertokens.pluginInterface.authRecipe.AuthRecipeUserInfo;
import io.supertokens.pluginInterface.bulkimport.BulkImportUser;
import io.supertokens.pluginInterface.exceptions.DbInitException;
import io.supertokens.pluginInterface.exceptions.InvalidConfigException;
import io.supertokens.pluginInterface.exceptions.StorageQueryException;
import io.supertokens.pluginInterface.multitenancy.AppIdentifier;
import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException;
import io.supertokens.storageLayer.StorageLayer;
import io.supertokens.utils.Utils;
import io.supertokens.webserver.InputParser;
import io.supertokens.webserver.WebserverAPI;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

public class ImportUserAPI extends WebserverAPI {
public ImportUserAPI(Main main) {
super(main, "");
}

@Override
public String getPath() {
return "/bulk-import/import";
}

@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// API is app specific

if (StorageLayer.isInMemDb(main)) {
throw new ServletException(new BadRequestException("This API is not supported in the in-memory database."));
}

JsonObject input = InputParser.parseJsonObjectOrThrowError(req);
JsonObject jsonUser = InputParser.parseJsonObjectOrThrowError(input, "user", false);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i dont think the input should have a user prop. Cause then its like:

{
   user: {...}
}

Instead. it should just have all the details in the root of the json input

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done


AppIdentifier appIdentifier = null;
Storage storage = null;
String[] allUserRoles = null;

try {
appIdentifier = getAppIdentifier(req);
storage = enforcePublicTenantAndGetPublicTenantStorage(req);
allUserRoles = StorageUtils.getUserRolesStorage(storage).getRoles(appIdentifier);
} catch (TenantOrAppNotFoundException | BadPermissionException | StorageQueryException e) {
throw new ServletException(e);
}

BulkImportUserUtils bulkImportUserUtils = new BulkImportUserUtils(allUserRoles);

try {
BulkImportUser user = bulkImportUserUtils.createBulkImportUserFromJSON(main, appIdentifier, jsonUser,
Utils.getUUID());

AuthRecipeUserInfo importedUser = BulkImport.importUser(main, appIdentifier, user);

JsonObject result = new JsonObject();
result.addProperty("status", "OK");
result.add("user", importedUser.toJson());
super.sendJsonResponse(200, result, resp);
} catch (io.supertokens.bulkimport.exceptions.InvalidBulkImportDataException e) {
JsonArray errors = e.errors.stream()
.map(JsonPrimitive::new)
.collect(JsonArray::new, JsonArray::add, JsonArray::addAll);

JsonObject errorResponseJson = new JsonObject();
errorResponseJson.add("errors", errors);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is an example of how this output looks like?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

{
  "errors": [
    "Role role1 does not exist.",
    "Role role2 does not exist.",
    "Invalid tenantId: t1 for emailpassword recipe.",
    "Invalid tenantId: t1 for thirdparty recipe.",
    "Invalid tenantId: t1 for passwordless recipe."
  ]
}

throw new ServletException(new WebserverAPI.BadRequestException(errorResponseJson.toString()));
} catch (StorageQueryException | TenantOrAppNotFoundException | InvalidConfigException | DbInitException e) {
throw new ServletException(e);
}
}
}
Loading
Loading