-
Notifications
You must be signed in to change notification settings - Fork 11
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Add interfaces and types for bulk import
- Loading branch information
Showing
5 changed files
with
397 additions
and
0 deletions.
There are no files selected for viewing
28 changes: 28 additions & 0 deletions
28
src/main/java/io/supertokens/pluginInterface/bulkimport/BulkImportStorage.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
package io.supertokens.pluginInterface.bulkimport; | ||
|
||
import java.util.ArrayList; | ||
|
||
import io.supertokens.pluginInterface.exceptions.StorageQueryException; | ||
import io.supertokens.pluginInterface.multitenancy.AppIdentifier; | ||
import io.supertokens.pluginInterface.multitenancy.exceptions.TenantOrAppNotFoundException; | ||
import io.supertokens.pluginInterface.nonAuthRecipe.NonAuthRecipeStorage; | ||
|
||
public interface BulkImportStorage extends NonAuthRecipeStorage { | ||
/** | ||
* Add users to the bulk_import_users table | ||
*/ | ||
void addBulkImportUsers(AppIdentifier appIdentifier, ArrayList<BulkImportUser> users) | ||
throws StorageQueryException, TenantOrAppNotFoundException; | ||
|
||
/** | ||
* Get users from the bulk_import_users table | ||
*/ | ||
// void getBulkImportUsers(AppIdentifier appIdentifier, @Nullable String status, @Nonnull Integer limit, @Nullable String bulkImportUserId) | ||
// throws StorageQueryException; | ||
|
||
/** | ||
* Delete users by id from the bulk_import_users table | ||
*/ | ||
// void deleteBulkImportUsers(AppIdentifier appIdentifier, @Nullable ArrayList<String> bulkImportUserIds) | ||
// throws StorageQueryException; | ||
} |
316 changes: 316 additions & 0 deletions
316
src/main/java/io/supertokens/pluginInterface/bulkimport/BulkImportUser.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,316 @@ | ||
/* | ||
* 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.pluginInterface.bulkimport; | ||
|
||
import java.util.List; | ||
import java.util.UUID; | ||
|
||
import com.google.gson.JsonArray; | ||
import com.google.gson.JsonElement; | ||
import com.google.gson.JsonObject; | ||
|
||
import io.supertokens.pluginInterface.bulkimport.exceptions.InvalidBulkImportDataException; | ||
|
||
import java.util.ArrayList; | ||
import java.util.Arrays; | ||
|
||
public class BulkImportUser { | ||
public String id; | ||
public JsonObject userData; | ||
public String externalUserId; | ||
public JsonObject userMetadata; | ||
public List<String> userRoles; | ||
public List<TotpDevice> totpDevices; | ||
public List<LoginMethod> loginMethods; | ||
public ArrayList<String> errors = new ArrayList<>(); | ||
|
||
public BulkImportUser(JsonObject userData, ArrayList<String> validTenantIds, String id) throws InvalidBulkImportDataException { | ||
this.id = id != null ? id : UUID.randomUUID().toString(); | ||
this.userData = userData; | ||
this.externalUserId = parseAndValidateField(userData, "externalUserId", ValueType.STRING, false, String.class, | ||
"."); | ||
this.userMetadata = parseAndValidateField(userData, "userMetadata", ValueType.OBJECT, false, JsonObject.class, | ||
"."); | ||
this.userRoles = getParsedUserRoles(userData); | ||
this.totpDevices = getParsedTotpDevices(userData); | ||
this.loginMethods = getParsedLoginMethods(userData, validTenantIds); | ||
|
||
if (errors.size() > 0) { | ||
throw new InvalidBulkImportDataException(errors); | ||
} | ||
} | ||
|
||
public String toString() { | ||
return this.userData.toString(); | ||
} | ||
|
||
private ArrayList<String> getParsedUserRoles(JsonObject userData) { | ||
JsonArray jsonUserRoles = parseAndValidateField(userData, "roles", ValueType.ARRAY_OF_STRING, false, | ||
JsonArray.class, "."); | ||
|
||
if (jsonUserRoles == null) { | ||
return null; | ||
} | ||
|
||
ArrayList<String> userRoles = new ArrayList<>(); | ||
jsonUserRoles.forEach(role -> userRoles.add(role.getAsString())); | ||
return userRoles; | ||
} | ||
|
||
private ArrayList<TotpDevice> getParsedTotpDevices(JsonObject userData) { | ||
JsonArray jsonTotpDevices = parseAndValidateField(userData, "totp", ValueType.ARRAY_OF_OBJECT, false, | ||
JsonArray.class, "."); | ||
if (jsonTotpDevices == null) { | ||
return null; | ||
} | ||
|
||
ArrayList<TotpDevice> totpDevices = new ArrayList<>(); | ||
for (JsonElement jsonTotpDevice : jsonTotpDevices) { | ||
totpDevices.add(new TotpDevice(jsonTotpDevice.getAsJsonObject())); | ||
} | ||
return totpDevices; | ||
} | ||
|
||
private ArrayList<LoginMethod> getParsedLoginMethods(JsonObject userData, ArrayList<String> validTenantIds) { | ||
JsonArray jsonLoginMethods = parseAndValidateField(userData, "loginMethods", ValueType.ARRAY_OF_OBJECT, true, | ||
JsonArray.class, "."); | ||
|
||
if (jsonLoginMethods == null) { | ||
return new ArrayList<>(); | ||
} | ||
|
||
if (jsonLoginMethods.size() == 0) { | ||
errors.add("At least one loginMethod is required."); | ||
return new ArrayList<>(); | ||
} | ||
|
||
Boolean hasPrimaryLoginMethod = false; | ||
|
||
ArrayList<LoginMethod> loginMethods = new ArrayList<>(); | ||
for (JsonElement jsonLoginMethod : jsonLoginMethods) { | ||
JsonObject jsonLoginMethodObj = jsonLoginMethod.getAsJsonObject(); | ||
|
||
if (validateJsonFieldType(jsonLoginMethodObj, "isPrimary", ValueType.BOOLEAN)) { | ||
if (jsonLoginMethodObj.get("isPrimary").getAsBoolean()) { | ||
if (hasPrimaryLoginMethod) { | ||
errors.add("No two loginMethods can have isPrimary as true."); | ||
} | ||
hasPrimaryLoginMethod = true; | ||
} | ||
} | ||
|
||
loginMethods.add(new LoginMethod(jsonLoginMethodObj)); | ||
} | ||
|
||
return loginMethods; | ||
} | ||
|
||
@SuppressWarnings("unchecked") | ||
private <T> T parseAndValidateField(JsonObject jsonObject, String key, ValueType expectedType, boolean isRequired, | ||
Class<T> targetType, String errorSuffix) { | ||
if (jsonObject.has(key)) { | ||
if (validateJsonFieldType(jsonObject, key, expectedType)) { | ||
T value; | ||
switch (expectedType) { | ||
case STRING: | ||
value = (T) jsonObject.get(key).getAsString(); | ||
break; | ||
case NUMBER: | ||
value = (T) jsonObject.get(key).getAsNumber(); | ||
break; | ||
case BOOLEAN: | ||
Boolean boolValue = jsonObject.get(key).getAsBoolean(); | ||
value = (T) boolValue; | ||
break; | ||
case OBJECT: | ||
value = (T) jsonObject.get(key).getAsJsonObject(); | ||
break; | ||
case ARRAY_OF_OBJECT, ARRAY_OF_STRING: | ||
value = (T) jsonObject.get(key).getAsJsonArray(); | ||
break; | ||
default: | ||
value = null; | ||
break; | ||
} | ||
if (value != null) { | ||
return targetType.cast(value); | ||
} else { | ||
errors.add(key + " should be of type " + getTypeForErrorMessage(expectedType) + errorSuffix); | ||
} | ||
} else { | ||
errors.add(key + " should be of type " + getTypeForErrorMessage(expectedType) + errorSuffix); | ||
} | ||
} else if (isRequired) { | ||
errors.add(key + " is required" + errorSuffix); | ||
} | ||
return null; | ||
} | ||
|
||
public enum ValueType { | ||
STRING, | ||
NUMBER, | ||
BOOLEAN, | ||
OBJECT, | ||
ARRAY_OF_STRING, | ||
ARRAY_OF_OBJECT | ||
} | ||
|
||
private String getTypeForErrorMessage(ValueType type) { | ||
return switch (type) { | ||
case STRING -> "string"; | ||
case NUMBER -> "number"; | ||
case BOOLEAN -> "boolean"; | ||
case OBJECT -> "object"; | ||
case ARRAY_OF_STRING -> "array of string"; | ||
case ARRAY_OF_OBJECT -> "array of object"; | ||
}; | ||
} | ||
|
||
private Boolean validateJsonFieldType(JsonObject jsonObject, String key, ValueType expectedType) { | ||
if (jsonObject.has(key)) { | ||
return switch (expectedType) { | ||
case STRING -> jsonObject.get(key).isJsonPrimitive() && jsonObject.getAsJsonPrimitive(key).isString(); | ||
case NUMBER -> jsonObject.get(key).isJsonPrimitive() && jsonObject.getAsJsonPrimitive(key).isNumber(); | ||
case BOOLEAN -> jsonObject.get(key).isJsonPrimitive() && jsonObject.getAsJsonPrimitive(key).isBoolean(); | ||
case OBJECT -> jsonObject.get(key).isJsonObject(); | ||
case ARRAY_OF_OBJECT, ARRAY_OF_STRING -> jsonObject.get(key).isJsonArray() | ||
&& validateArrayElements(jsonObject.getAsJsonArray(key), expectedType); | ||
default -> false; | ||
}; | ||
} | ||
return false; | ||
} | ||
|
||
private boolean validateArrayElements(JsonArray array, ValueType expectedType) { | ||
List<JsonElement> elements = new ArrayList<>(); | ||
array.forEach(elements::add); | ||
|
||
return switch (expectedType) { | ||
case ARRAY_OF_OBJECT -> elements.stream().allMatch(JsonElement::isJsonObject); | ||
case ARRAY_OF_STRING -> | ||
elements.stream().allMatch(el -> el.isJsonPrimitive() && el.getAsJsonPrimitive().isString()); | ||
default -> false; | ||
}; | ||
} | ||
|
||
public class TotpDevice { | ||
public String secretKey; | ||
public Number period; | ||
public Number skew; | ||
public String deviceName; | ||
|
||
public TotpDevice(JsonObject jsonTotpDevice) { | ||
this.secretKey = parseAndValidateField(jsonTotpDevice, "secretKey", ValueType.STRING, true, String.class, | ||
" for a totp device."); | ||
this.period = parseAndValidateField(jsonTotpDevice, "period", ValueType.NUMBER, true, Number.class, | ||
" for a totp device."); | ||
this.skew = parseAndValidateField(jsonTotpDevice, "skew", ValueType.NUMBER, true, Number.class, | ||
" for a totp device."); | ||
this.deviceName = parseAndValidateField(jsonTotpDevice, "deviceName", ValueType.STRING, false, String.class, | ||
" for a totp device."); | ||
} | ||
} | ||
|
||
public class LoginMethod { | ||
public String tenantId; | ||
public Boolean isVerified; | ||
public Boolean isPrimary; | ||
public long timeJoinedInMSSinceEpoch; | ||
public String recipeId; | ||
|
||
public EmailPasswordLoginMethod emailPasswordLoginMethod; | ||
public ThirdPartyLoginMethod thirdPartyLoginMethod; | ||
public PasswordlessLoginMethod passwordlessLoginMethod; | ||
|
||
public LoginMethod(JsonObject jsonLoginMethod) { | ||
this.recipeId = parseAndValidateField(jsonLoginMethod, "recipeId", ValueType.STRING, true, String.class, | ||
" for a loginMethod."); | ||
this.tenantId = parseAndValidateField(jsonLoginMethod, "tenantId", ValueType.STRING, false, String.class, | ||
" for a loginMethod."); | ||
this.isVerified = parseAndValidateField(jsonLoginMethod, "isVerified", ValueType.BOOLEAN, false, | ||
Boolean.class, " for a loginMethod."); | ||
this.isPrimary = parseAndValidateField(jsonLoginMethod, "isPrimary", ValueType.BOOLEAN, false, | ||
Boolean.class, " for a loginMethod."); | ||
Number timeJoined = parseAndValidateField(jsonLoginMethod, "timeJoinedInMSSinceEpoch", ValueType.NUMBER, | ||
false, Number.class, " for a loginMethod"); | ||
this.timeJoinedInMSSinceEpoch = timeJoined != null ? timeJoined.longValue() : 0; | ||
|
||
if ("emailpassword".equals(this.recipeId)) { | ||
this.emailPasswordLoginMethod = new EmailPasswordLoginMethod(jsonLoginMethod); | ||
} else if ("thirdparty".equals(this.recipeId)) { | ||
this.thirdPartyLoginMethod = new ThirdPartyLoginMethod(jsonLoginMethod); | ||
} else if ("passwordless".equals(this.recipeId)) { | ||
this.passwordlessLoginMethod = new PasswordlessLoginMethod(jsonLoginMethod); | ||
} else if (this.recipeId != null) { | ||
errors.add( | ||
"Invalid recipeId for loginMethod. Pass one of emailpassword, thirdparty or, passwordless!"); | ||
} | ||
} | ||
|
||
public class EmailPasswordLoginMethod { | ||
public String email; | ||
public String passwordHash; | ||
public String hashingAlgorithm; | ||
|
||
public EmailPasswordLoginMethod(JsonObject jsonLoginMethod) { | ||
this.email = parseAndValidateField(jsonLoginMethod, "email", ValueType.STRING, true, String.class, | ||
" for an emailpassword recipe."); | ||
this.passwordHash = parseAndValidateField(jsonLoginMethod, "passwordHash", ValueType.STRING, true, | ||
String.class, " for an emailpassword recipe."); | ||
this.hashingAlgorithm = parseAndValidateField(jsonLoginMethod, "hashingAlgorithm", ValueType.STRING, | ||
true, String.class, " for an emailpassword recipe."); | ||
|
||
if (this.hashingAlgorithm != null && !Arrays.asList("bcrypt", "argon2", "firebase_scrypt").contains(hashingAlgorithm)) { | ||
errors.add( | ||
"Invalid hashingAlgorithm for emailpassword recipe. Pass one of bcrypt, argon2 or, firebase_scrypt!"); | ||
} | ||
} | ||
} | ||
|
||
public class ThirdPartyLoginMethod { | ||
public String email; | ||
public String thirdPartyId; | ||
public String thirdPartyUserId; | ||
|
||
public ThirdPartyLoginMethod(JsonObject jsonObject) { | ||
this.email = parseAndValidateField(jsonObject, "email", ValueType.STRING, true, String.class, | ||
" for a thirdparty recipe."); | ||
this.thirdPartyId = parseAndValidateField(jsonObject, "thirdPartyId", ValueType.STRING, true, | ||
String.class, " for a thirdparty recipe."); | ||
this.thirdPartyUserId = parseAndValidateField(jsonObject, "thirdPartyUserId", ValueType.STRING, true, | ||
String.class, " for a thirdparty recipe."); | ||
} | ||
} | ||
|
||
public class PasswordlessLoginMethod { | ||
public String email; | ||
public String phoneNumber; | ||
|
||
public PasswordlessLoginMethod(JsonObject jsonObject) { | ||
this.email = parseAndValidateField(jsonObject, "email", ValueType.STRING, false, String.class, | ||
" for a passwordless recipe."); | ||
this.phoneNumber = parseAndValidateField(jsonObject, "phoneNumber", ValueType.STRING, false, | ||
String.class, " for a passwordless recipe."); | ||
|
||
if ((email != null && email.isEmpty()) && (phoneNumber != null && phoneNumber.isEmpty())) { | ||
errors.add( | ||
"Either email or phoneNumber is required for a passwordless recipe."); | ||
} | ||
} | ||
} | ||
} | ||
} |
35 changes: 35 additions & 0 deletions
35
.../io/supertokens/pluginInterface/bulkimport/exceptions/InvalidBulkImportDataException.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
/* | ||
* 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.pluginInterface.bulkimport.exceptions; | ||
|
||
import java.util.ArrayList; | ||
|
||
public class InvalidBulkImportDataException extends Exception { | ||
private static final long serialVersionUID = 1L; | ||
public ArrayList<String> errors; | ||
|
||
public InvalidBulkImportDataException(ArrayList<String> errors) { | ||
super("Data has missing or invalid fields. Please check the errors field for more details."); | ||
this.errors = errors; | ||
} | ||
|
||
public void addError(String error) { | ||
this.errors.add(error); | ||
} | ||
} | ||
|
||
|
Oops, something went wrong.