diff --git a/CHANGELOG.md b/CHANGELOG.md index d5536f6ef..1cc984909 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Type of `User` object returned by get users function - Functions `deleteuser`, `getUsersNewestFirst` and `getUsersOldestFirst` are now based on account linking recipe - Function `deleteuser` takes a new parameter `removeAllLinkedAccounts` which will be `true` by default +- Generate Password Reset Token API logic updated + +### Removed: + +- For EmailPassword recipe input, resetPasswordUsingTokenFeature user input removed ## [13.0.2] - 2023-02-10 diff --git a/lib/build/recipe/accountlinking/accountLinkingClaim.d.ts b/lib/build/recipe/accountlinking/accountLinkingClaim.d.ts new file mode 100644 index 000000000..f7141ffb6 --- /dev/null +++ b/lib/build/recipe/accountlinking/accountLinkingClaim.d.ts @@ -0,0 +1,9 @@ +// @ts-nocheck +import { PrimitiveClaim } from "../session/claims"; +/** + * We include "Class" in the class name, because it makes it easier to import the right thing (the instance) instead of this. + * */ +export declare class AccountLinkingClaimClass extends PrimitiveClaim { + constructor(); +} +export declare const AccountLinkingClaim: AccountLinkingClaimClass; diff --git a/lib/build/recipe/accountlinking/accountLinkingClaim.js b/lib/build/recipe/accountlinking/accountLinkingClaim.js new file mode 100644 index 000000000..ea25bce78 --- /dev/null +++ b/lib/build/recipe/accountlinking/accountLinkingClaim.js @@ -0,0 +1,19 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.AccountLinkingClaim = exports.AccountLinkingClaimClass = void 0; +const claims_1 = require("../session/claims"); +/** + * We include "Class" in the class name, because it makes it easier to import the right thing (the instance) instead of this. + * */ +class AccountLinkingClaimClass extends claims_1.PrimitiveClaim { + constructor() { + super({ + key: "st-linking", + fetchValue(_, __, ___) { + return undefined; + }, + }); + } +} +exports.AccountLinkingClaimClass = AccountLinkingClaimClass; +exports.AccountLinkingClaim = new AccountLinkingClaimClass(); diff --git a/lib/build/recipe/accountlinking/index.d.ts b/lib/build/recipe/accountlinking/index.d.ts index 619a1ff00..5918abe8a 100644 --- a/lib/build/recipe/accountlinking/index.d.ts +++ b/lib/build/recipe/accountlinking/index.d.ts @@ -37,7 +37,7 @@ export default class Wrapper { ): Promise< | { status: "OK"; - user: import("../../types").User; + user: import("../emailpassword").User; wasAlreadyAPrimaryUser: boolean; } | { @@ -104,7 +104,7 @@ export default class Wrapper { static fetchFromAccountToLinkTable( recipeUserId: string, userContext?: any - ): Promise; + ): Promise; static storeIntoAccountToLinkTable( recipeUserId: string, primaryUserId: string, diff --git a/lib/build/recipe/accountlinking/recipe.d.ts b/lib/build/recipe/accountlinking/recipe.d.ts index 851135115..91a4acd71 100644 --- a/lib/build/recipe/accountlinking/recipe.d.ts +++ b/lib/build/recipe/accountlinking/recipe.d.ts @@ -64,23 +64,43 @@ export default class Recipe extends RecipeModule { newUser: AccountInfoWithRecipeId; userContext: any; }) => Promise; - linkAccountsWithUserFromSession: ({ + linkAccountsWithUserFromSession: ({ session, newUser, createRecipeUserFunc, + verifyCredentialsFunc, userContext, }: { session: SessionContainer; newUser: AccountInfoWithRecipeId; - createRecipeUserFunc: (newUser: AccountInfoWithRecipeId) => Promise; + createRecipeUserFunc: () => Promise; + verifyCredentialsFunc: () => Promise< + | { + status: "OK"; + } + | { + status: "CUSTOM_RESPONSE"; + resp: T; + } + >; userContext: any; }) => Promise< | { - status: "OK" | "NEW_ACCOUNT_NEEDS_TO_BE_VERIFIED_ERROR"; + status: "OK"; + wereAccountsAlreadyLinked: boolean; } | { status: "ACCOUNT_LINKING_NOT_ALLOWED_ERROR"; description: string; } + | { + status: "NEW_ACCOUNT_NEEDS_TO_BE_VERIFIED_ERROR"; + primaryUserId: string; + recipeUserId: string; + } + | { + status: "CUSTOM_RESPONSE"; + resp: T; + } >; } diff --git a/lib/build/recipe/accountlinking/recipe.js b/lib/build/recipe/accountlinking/recipe.js index da58201a4..3dc9e8d69 100644 --- a/lib/build/recipe/accountlinking/recipe.js +++ b/lib/build/recipe/accountlinking/recipe.js @@ -295,7 +295,13 @@ class Recipe extends recipeModule_1.default { } return false; }); - this.linkAccountsWithUserFromSession = ({ session, newUser, createRecipeUserFunc, userContext }) => + this.linkAccountsWithUserFromSession = ({ + session, + newUser, + createRecipeUserFunc, + verifyCredentialsFunc, + userContext, + }) => __awaiter(this, void 0, void 0, function* () { // In order to link the newUser to the session user, // we need to first make sure that the session user @@ -384,6 +390,7 @@ class Recipe extends recipeModule_1.default { session, newUser, createRecipeUserFunc, + verifyCredentialsFunc, userContext, }); } else if ( @@ -426,11 +433,9 @@ class Recipe extends recipeModule_1.default { accountInfo: newUser, userContext, }); - let newUserIsVerified = false; const userObjThatHasSameAccountInfoAndRecipeIdAsNewUser = usersArrayThatHaveSameAccountInfoAsNewUser.find( (u) => u.loginMethods.find((lU) => { - let found = false; if (lU.recipeId !== newUser.recipeId) { return false; } @@ -438,18 +443,14 @@ class Recipe extends recipeModule_1.default { if (lU.thirdParty === undefined) { return false; } - found = + return ( lU.thirdParty.id === newUser.thirdParty.id && - lU.thirdParty.userId === newUser.thirdParty.userId; + lU.thirdParty.userId === newUser.thirdParty.userId + ); } else { - found = lU.email === newUser.email || newUser.phoneNumber === newUser.phoneNumber; - } - if (!found) { - return false; + return lU.email === newUser.email || newUser.phoneNumber === newUser.phoneNumber; } - newUserIsVerified = lU.verified; - return true; - }) + }) !== undefined ); if (userObjThatHasSameAccountInfoAndRecipeIdAsNewUser === undefined) { /* @@ -468,14 +469,40 @@ class Recipe extends recipeModule_1.default { }; } // we create the new recipe user - yield createRecipeUserFunc(newUser); + yield createRecipeUserFunc(); // now when we recurse, the new recipe user will be found and we can try linking again. return yield this.linkAccountsWithUserFromSession({ session, newUser, createRecipeUserFunc, + verifyCredentialsFunc, userContext, }); + } else { + // since the user already exists, we should first verify the credentials + // before continuing to link the accounts. + let verifyResult = yield verifyCredentialsFunc(); + if (verifyResult.status === "CUSTOM_RESPONSE") { + return verifyResult; + } + // this means that the verification was fine and we can continue.. + } + // we check if the userObjThatHasSameAccountInfoAndRecipeIdAsNewUser is + // a primary user or not, and if it is, then it means that our newUser + // is already linked so we can return early. + if (userObjThatHasSameAccountInfoAndRecipeIdAsNewUser.isPrimaryUser) { + if (userObjThatHasSameAccountInfoAndRecipeIdAsNewUser.id === existingUser.id) { + // this means that the accounts we want to link are already linked. + return { + status: "OK", + wereAccountsAlreadyLinked: true, + }; + } else { + return { + status: "ACCOUNT_LINKING_NOT_ALLOWED_ERROR", + description: "New user is already linked to another account", + }; + } } // now we check about the email verification of the new user. If it's verified, we proceed // to try and link the accounts, and if not, we send email verification error ONLY if the email @@ -483,10 +510,17 @@ class Recipe extends recipeModule_1.default { if (usersArrayThatHaveSameAccountInfoAsNewUser.find((u) => u.id === existingUser.id) === undefined) { // this means that the existing user does not share anything in common with the new user // in terms of account info. So we check for email verification status.. - if (!newUserIsVerified && shouldDoAccountLinking.shouldRequireVerification) { + if ( + !userObjThatHasSameAccountInfoAndRecipeIdAsNewUser.loginMethods[0].verified && + shouldDoAccountLinking.shouldRequireVerification + ) { // we stop the flow and ask the user to verify this email first. + // the recipe ID is the userObjThatHasSameAccountInfoAndRecipeIdAsNewUser.id + // cause above we checked that userObjThatHasSameAccountInfoAndRecipeIdAsNewUser.isPrimaryUser is false. return { status: "NEW_ACCOUNT_NEEDS_TO_BE_VERIFIED_ERROR", + primaryUserId: existingUser.id, + recipeUserId: userObjThatHasSameAccountInfoAndRecipeIdAsNewUser.id, }; } } @@ -498,6 +532,7 @@ class Recipe extends recipeModule_1.default { if (linkAccountResponse.status === "OK") { return { status: "OK", + wereAccountsAlreadyLinked: linkAccountResponse.accountsAlreadyLinked, }; } else if ( linkAccountResponse.status === "RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" diff --git a/lib/build/recipe/accountlinking/recipeImplementation.js b/lib/build/recipe/accountlinking/recipeImplementation.js index a52e7f3c0..ca9628ee7 100644 --- a/lib/build/recipe/accountlinking/recipeImplementation.js +++ b/lib/build/recipe/accountlinking/recipeImplementation.js @@ -96,24 +96,22 @@ function getRecipeImplementation(querier, config) { }, canCreatePrimaryUserId: function ({ recipeUserId }) { return __awaiter(this, void 0, void 0, function* () { - let result = yield querier.sendGetRequest( + return yield querier.sendGetRequest( new normalisedURLPath_1.default("/recipe/accountlinking/user/primary/check"), { recipeUserId, } ); - return result; }); }, createPrimaryUser: function ({ recipeUserId }) { return __awaiter(this, void 0, void 0, function* () { - let result = yield querier.sendPostRequest( + return yield querier.sendPostRequest( new normalisedURLPath_1.default("/recipe/accountlinking/user/primary"), { recipeUserId, } ); - return result; }); }, canLinkAccounts: function ({ recipeUserId, primaryUserId }) { @@ -157,59 +155,20 @@ function getRecipeImplementation(querier, config) { }, unlinkAccounts: function ({ recipeUserId, userContext }) { return __awaiter(this, void 0, void 0, function* () { - let recipeUserIdToPrimaryUserIdMapping = yield this.getPrimaryUserIdsForRecipeUserIds({ - recipeUserIds: [recipeUserId], - userContext, - }); - let primaryUserId = recipeUserIdToPrimaryUserIdMapping[recipeUserId]; - if (primaryUserId === undefined) { - return { - status: "RECIPE_USER_NOT_FOUND_ERROR", - description: "No user exists with the provided recipeUserId", - }; - } - if (primaryUserId === null) { - return { - status: "PRIMARY_USER_NOT_FOUND_ERROR", - description: - "The input recipeUserId is not linked to any primary user, or is not a primary user itself", - }; - } - if (primaryUserId === recipeUserId) { - let user = yield this.getUser({ - userId: primaryUserId, - userContext, - }); - if (user === undefined) { - // this can happen cause of some race condition.. - return this.unlinkAccounts({ - recipeUserId, - userContext, - }); - } - if (user.loginMethods.length > 1) { - // we delete the user here cause if we didn't - // do that, then it would result in the primary user ID having the same - // user ID as the recipe user ID, but they are not linked. So this is not allowed. - yield this.deleteUser({ - userId: recipeUserId, - removeAllLinkedAccounts: false, - userContext, - }); - return { - status: "OK", - wasRecipeUserDeleted: true, - }; - } - } let accountsUnlinkingResult = yield querier.sendPostRequest( new normalisedURLPath_1.default("/recipe/accountlinking/user/unlink"), { recipeUserId, - primaryUserId, } ); - if (accountsUnlinkingResult.status === "OK") { + if (accountsUnlinkingResult.status === "OK" && !accountsUnlinkingResult.wasRecipeUserDeleted) { + // we have the !accountsUnlinkingResult.wasRecipeUserDeleted check + // cause if the user was deleted, it means that it's user ID was the + // same as the primary user ID, AND that the primary user ID has more + // than one login method - so if we revoke the session in this case, + // it will revoke the session for all login methods as well (since recipeUserId == primaryUserID). + // The reason we don't do this in the core is that if the user has overriden + // session recipe, it goes through their logic. yield session_1.default.revokeAllSessionsForUser(recipeUserId, userContext); } return accountsUnlinkingResult; @@ -240,11 +199,10 @@ function getRecipeImplementation(querier, config) { }, deleteUser: function ({ userId, removeAllLinkedAccounts }) { return __awaiter(this, void 0, void 0, function* () { - let result = yield querier.sendPostRequest(new normalisedURLPath_1.default("/user/remove"), { + return yield querier.sendPostRequest(new normalisedURLPath_1.default("/user/remove"), { userId, removeAllLinkedAccounts, }); - return result; }); }, fetchFromAccountToLinkTable: function ({ recipeUserId }) { diff --git a/lib/build/recipe/dashboard/api/userdetails/userPasswordPut.js b/lib/build/recipe/dashboard/api/userdetails/userPasswordPut.js index 43b63e871..4159bd21c 100644 --- a/lib/build/recipe/dashboard/api/userdetails/userPasswordPut.js +++ b/lib/build/recipe/dashboard/api/userdetails/userPasswordPut.js @@ -42,12 +42,10 @@ const recipe_1 = __importDefault(require("../../../emailpassword/recipe")); const emailpassword_1 = __importDefault(require("../../../emailpassword")); const recipe_2 = __importDefault(require("../../../thirdpartyemailpassword/recipe")); const thirdpartyemailpassword_1 = __importDefault(require("../../../thirdpartyemailpassword")); -const constants_1 = require("../../../emailpassword/constants"); const userPasswordPut = (_, options) => __awaiter(void 0, void 0, void 0, function* () { const requestBody = yield options.req.getJSONBody(); const userId = requestBody.userId; - const email = requestBody.email; const newPassword = requestBody.newPassword; if (userId === undefined || typeof userId !== "string") { throw new error_1.default({ @@ -77,54 +75,50 @@ const userPasswordPut = (_, options) => throw new Error("Should never come here"); } if (recipeToUse === "emailpassword") { - let passwordFormFields = recipe_1.default - .getInstanceOrThrowError() - .config.signUpFeature.formFields.filter((field) => field.id === constants_1.FORM_FIELD_PASSWORD_ID); - let passwordValidationError = yield passwordFormFields[0].validate(newPassword); - if (passwordValidationError !== undefined) { - return { - status: "INVALID_PASSWORD_ERROR", - error: passwordValidationError, - }; - } - const passwordResetToken = yield emailpassword_1.default.createResetPasswordToken(userId, email); - if (passwordResetToken.status === "UNKNOWN_USER_ID_ERROR") { + const updateResponse = yield emailpassword_1.default.updateEmailOrPassword({ + userId, + password: newPassword, + }); + if ( + updateResponse.status === "UNKNOWN_USER_ID_ERROR" || + updateResponse.status === "EMAIL_ALREADY_EXISTS_ERROR" || + updateResponse.status === "EMAIL_CHANGE_NOT_ALLOWED_ERROR" + ) { // Techincally it can but its an edge case so we assume that it wont throw new Error("Should never come here"); } - const passwordResetResponse = yield emailpassword_1.default.resetPasswordUsingToken( - passwordResetToken.token, - newPassword - ); - if (passwordResetResponse.status === "RESET_PASSWORD_INVALID_TOKEN_ERROR") { - throw new Error("Should never come here"); - } - return { - status: "OK", - }; - } - let passwordFormFields = recipe_2.default - .getInstanceOrThrowError() - .config.signUpFeature.formFields.filter((field) => field.id === constants_1.FORM_FIELD_PASSWORD_ID); - let passwordValidationError = yield passwordFormFields[0].validate(newPassword); - if (passwordValidationError !== undefined) { - return { + // TODO: check for password policy error has well. + /** + * + * return { status: "INVALID_PASSWORD_ERROR", error: passwordValidationError, }; + */ + return { + status: "OK", + }; } - const passwordResetToken = yield thirdpartyemailpassword_1.default.createResetPasswordToken(userId, email); - if (passwordResetToken.status === "UNKNOWN_USER_ID_ERROR") { + const updateResponse = yield thirdpartyemailpassword_1.default.updateEmailOrPassword({ + userId, + password: newPassword, + }); + if ( + updateResponse.status === "UNKNOWN_USER_ID_ERROR" || + updateResponse.status === "EMAIL_ALREADY_EXISTS_ERROR" || + updateResponse.status === "EMAIL_CHANGE_NOT_ALLOWED_ERROR" + ) { // Techincally it can but its an edge case so we assume that it wont throw new Error("Should never come here"); } - const passwordResetResponse = yield thirdpartyemailpassword_1.default.resetPasswordUsingToken( - passwordResetToken.token, - newPassword - ); - if (passwordResetResponse.status === "RESET_PASSWORD_INVALID_TOKEN_ERROR") { - throw new Error("Should never come here"); - } + // TODO: check for password policy error has well. + /** + * + * return { + status: "INVALID_PASSWORD_ERROR", + error: passwordValidationError, + }; + */ return { status: "OK", }; diff --git a/lib/build/recipe/dashboard/types.d.ts b/lib/build/recipe/dashboard/types.d.ts index b35894316..be7b06b28 100644 --- a/lib/build/recipe/dashboard/types.d.ts +++ b/lib/build/recipe/dashboard/types.d.ts @@ -50,6 +50,17 @@ export declare type RecipeLevelUser = { id: string; userId: string; }; +}; +export declare type RecipeLevelUserWithFirstAndLastName = { + recipeId: "emailpassword" | "thirdparty" | "passwordless"; + timeJoined: number; + recipeUserId: string; + email?: string; + phoneNumber?: string; + thirdParty?: { + id: string; + userId: string; + }; firstName: string; lastName: string; }; diff --git a/lib/build/recipe/dashboard/utils.d.ts b/lib/build/recipe/dashboard/utils.d.ts index dc6d04a43..61864bada 100644 --- a/lib/build/recipe/dashboard/utils.d.ts +++ b/lib/build/recipe/dashboard/utils.d.ts @@ -2,7 +2,7 @@ import { BaseResponse } from "../../framework"; import NormalisedURLPath from "../../normalisedURLPath"; import { HTTPMethod, NormalisedAppinfo } from "../../types"; -import { RecipeIdForUser, TypeInput, TypeNormalisedInput, RecipeLevelUser } from "./types"; +import { RecipeIdForUser, TypeInput, TypeNormalisedInput, RecipeLevelUserWithFirstAndLastName } from "./types"; export declare function validateAndNormaliseUserInput(config: TypeInput): TypeNormalisedInput; export declare function isApiPath(path: NormalisedURLPath, appInfo: NormalisedAppinfo): boolean; export declare function getApiIdIfMatched(path: NormalisedURLPath, method: HTTPMethod): string | undefined; @@ -12,7 +12,7 @@ export declare function getUserForRecipeId( userId: string, recipeId: string ): Promise<{ - user: RecipeLevelUser | undefined; + user: RecipeLevelUserWithFirstAndLastName | undefined; recipe: | "emailpassword" | "thirdparty" diff --git a/lib/build/recipe/dashboard/utils.js b/lib/build/recipe/dashboard/utils.js index 002811dff..60dff2aee 100644 --- a/lib/build/recipe/dashboard/utils.js +++ b/lib/build/recipe/dashboard/utils.js @@ -54,12 +54,15 @@ exports.isRecipeInitialised = exports.getUserForRecipeId = exports.isValidRecipe const normalisedURLPath_1 = __importDefault(require("../../normalisedURLPath")); const utils_1 = require("../../utils"); const constants_1 = require("./constants"); -const supertokens_1 = __importDefault(require("../../supertokens")); -const recipe_1 = __importDefault(require("../emailpassword/recipe")); -const recipe_2 = __importDefault(require("../thirdparty/recipe")); -const recipe_3 = __importDefault(require("../passwordless/recipe")); -const recipe_4 = __importDefault(require("../thirdpartyemailpassword/recipe")); -const recipe_5 = __importDefault(require("../thirdpartypasswordless/recipe")); +const recipe_1 = __importDefault(require("../accountlinking/recipe")); +const recipe_2 = __importDefault(require("../emailpassword/recipe")); +const recipe_3 = __importDefault(require("../thirdparty/recipe")); +const recipe_4 = __importDefault(require("../passwordless/recipe")); +const recipe_5 = __importDefault(require("../thirdpartyemailpassword/recipe")); +const recipe_6 = __importDefault(require("../thirdpartypasswordless/recipe")); +const thirdparty_1 = __importDefault(require("../thirdparty")); +const passwordless_1 = __importDefault(require("../passwordless")); +const thirdpartypasswordless_1 = __importDefault(require("../thirdpartypasswordless")); function validateAndNormaliseUserInput(config) { if (config.apiKey.trim().length === 0) { throw new Error("apiKey provided to Dashboard recipe cannot be empty"); @@ -146,7 +149,7 @@ function isValidRecipeId(recipeId) { exports.isValidRecipeId = isValidRecipeId; function getUserForRecipeId(userId, recipeId) { return __awaiter(this, void 0, void 0, function* () { - let userResponse = yield supertokens_1.default.getInstanceOrThrowError()._getUserForRecipeId(userId, recipeId); + let userResponse = yield _getUserForRecipeId(userId, recipeId); let user = undefined; if (userResponse.user !== undefined) { user = Object.assign(Object.assign({}, userResponse.user), { firstName: "", lastName: "" }); @@ -158,44 +161,153 @@ function getUserForRecipeId(userId, recipeId) { }); } exports.getUserForRecipeId = getUserForRecipeId; +function _getUserForRecipeId(userId, recipeId) { + return __awaiter(this, void 0, void 0, function* () { + let user; + let recipe; + const globalUser = yield recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.getUser({ + userId, + userContext: {}, + }); + if (recipeId === recipe_2.default.RECIPE_ID) { + try { + // we detect if this recipe has been init or not.. + recipe_2.default.getInstanceOrThrowError(); + if (globalUser !== undefined) { + let loginMethod = globalUser.loginMethods.find( + (u) => u.recipeId === "emailpassword" && u.recipeUserId === userId + ); + if (loginMethod !== undefined) { + user = Object.assign({}, loginMethod); + recipe = "emailpassword"; + } + } + } catch (e) { + // No - op + } + if (user === undefined) { + try { + // we detect if this recipe has been init or not.. + recipe_5.default.getInstanceOrThrowError(); + if (globalUser !== undefined) { + let loginMethod = globalUser.loginMethods.find( + (u) => u.recipeId === "emailpassword" && u.recipeUserId === userId + ); + if (loginMethod !== undefined) { + user = Object.assign({}, loginMethod); + recipe = "thirdpartyemailpassword"; + } + } + } catch (e) { + // No - op + } + } + } else if (recipeId === recipe_3.default.RECIPE_ID) { + try { + const userResponse = yield thirdparty_1.default.getUserById(userId); + if (userResponse !== undefined) { + user = Object.assign(Object.assign({}, userResponse), { recipeId: "thirdparty" }); + recipe = "thirdparty"; + } + } catch (e) { + // No - op + } + if (user === undefined) { + try { + // we detect if this recipe has been init or not.. + recipe_5.default.getInstanceOrThrowError(); + if (globalUser !== undefined) { + let loginMethod = globalUser.loginMethods.find( + (u) => u.recipeId === "thirdparty" && u.recipeUserId === userId + ); + if (loginMethod !== undefined) { + user = Object.assign({}, loginMethod); + recipe = "thirdpartyemailpassword"; + } + } + } catch (e) { + // No - op + } + } + if (user === undefined) { + try { + const userResponse = yield thirdpartypasswordless_1.default.getUserById(userId); + if (userResponse !== undefined) { + user = Object.assign(Object.assign({}, userResponse), { recipeId: "thirdparty" }); + recipe = "thirdpartypasswordless"; + } + } catch (e) { + // No - op + } + } + } else if (recipeId === recipe_4.default.RECIPE_ID) { + try { + const userResponse = yield passwordless_1.default.getUserById({ + userId, + }); + if (userResponse !== undefined) { + user = Object.assign(Object.assign({}, userResponse), { recipeId: "passwordless" }); + recipe = "passwordless"; + } + } catch (e) { + // No - op + } + if (user === undefined) { + try { + const userResponse = yield thirdpartypasswordless_1.default.getUserById(userId); + if (userResponse !== undefined) { + user = Object.assign(Object.assign({}, userResponse), { recipeId: "passwordless" }); + recipe = "thirdpartypasswordless"; + } + } catch (e) { + // No - op + } + } + } + return { + user, + recipe, + }; + }); +} function isRecipeInitialised(recipeId) { let isRecipeInitialised = false; if (recipeId === "emailpassword") { try { - recipe_1.default.getInstanceOrThrowError(); + recipe_2.default.getInstanceOrThrowError(); isRecipeInitialised = true; } catch (_) {} if (!isRecipeInitialised) { try { - recipe_4.default.getInstanceOrThrowError(); + recipe_5.default.getInstanceOrThrowError(); isRecipeInitialised = true; } catch (_) {} } } else if (recipeId === "passwordless") { try { - recipe_3.default.getInstanceOrThrowError(); + recipe_4.default.getInstanceOrThrowError(); isRecipeInitialised = true; } catch (_) {} if (!isRecipeInitialised) { try { - recipe_5.default.getInstanceOrThrowError(); + recipe_6.default.getInstanceOrThrowError(); isRecipeInitialised = true; } catch (_) {} } } else if (recipeId === "thirdparty") { try { - recipe_2.default.getInstanceOrThrowError(); + recipe_3.default.getInstanceOrThrowError(); isRecipeInitialised = true; } catch (_) {} if (!isRecipeInitialised) { try { - recipe_4.default.getInstanceOrThrowError(); + recipe_5.default.getInstanceOrThrowError(); isRecipeInitialised = true; } catch (_) {} } if (!isRecipeInitialised) { try { - recipe_5.default.getInstanceOrThrowError(); + recipe_6.default.getInstanceOrThrowError(); isRecipeInitialised = true; } catch (_) {} } diff --git a/lib/build/recipe/emailpassword/api/implementation.js b/lib/build/recipe/emailpassword/api/implementation.js index 39061e053..ec58b94e9 100644 --- a/lib/build/recipe/emailpassword/api/implementation.js +++ b/lib/build/recipe/emailpassword/api/implementation.js @@ -38,74 +38,443 @@ var __importDefault = Object.defineProperty(exports, "__esModule", { value: true }); const logger_1 = require("../../../logger"); const session_1 = __importDefault(require("../../session")); +const __1 = require("../../../"); +const recipe_1 = __importDefault(require("../../accountlinking/recipe")); +const recipe_2 = __importDefault(require("../../emailverification/recipe")); +const accountLinkingClaim_1 = require("../../accountlinking/accountLinkingClaim"); +const accountlinking_1 = require("../../accountlinking"); function getAPIImplementation() { return { - linkAccountToExistingAccountPOST: function (_input) { + linkAccountToExistingAccountPOST: function ({ formFields, session, options, userContext }) { return __awaiter(this, void 0, void 0, function* () { - return { - status: "ACCOUNT_NOT_VERIFIED_ERROR", - isNotVerifiedAccountFromInputSession: false, - description: "", - }; + const email = formFields.filter((f) => f.id === "email")[0].value; + const password = formFields.filter((f) => f.id === "password")[0].value; + const createRecipeUserFunc = () => + __awaiter(this, void 0, void 0, function* () { + yield options.recipeImplementation.createNewRecipeUser({ + email, + password, + userContext, + }); + // we ignore the result from the above cause after this, function returns, + // the linkAccountsWithUserFromSession anyway does recursion.. + }); + const verifyCredentialsFunc = () => + __awaiter(this, void 0, void 0, function* () { + const signInResult = yield options.recipeImplementation.signIn({ + email, + password, + userContext, + }); + if (signInResult.status === "OK") { + return { status: "OK" }; + } else { + return { + status: "CUSTOM_RESPONSE", + resp: signInResult, + }; + } + }); + let accountLinkingInstance = yield recipe_1.default.getInstanceOrThrowError(); + let result = yield accountLinkingInstance.linkAccountsWithUserFromSession({ + session, + newUser: { + email, + recipeId: "emailpassword", + }, + createRecipeUserFunc, + verifyCredentialsFunc, + userContext, + }); + if (result.status === "CUSTOM_RESPONSE") { + return result.resp; + } else if (result.status === "NEW_ACCOUNT_NEEDS_TO_BE_VERIFIED_ERROR") { + // this will store in the db that these need to be linked, + // and after verification, it will link these accounts. + let toLinkResult = yield accountlinking_1.storeIntoAccountToLinkTable( + result.recipeUserId, + result.primaryUserId, + userContext + ); + if (toLinkResult.status === "RECIPE_USER_ID_ALREADY_LINKED_WITH_PRIMARY_USER_ID_ERROR") { + if (toLinkResult.primaryUserId === result.primaryUserId) { + // this is some sort of a race condition issue, so we just ignore it + // since we already linked to the session's account anyway... + return { + status: "OK", + wereAccountsAlreadyLinked: true, + }; + } else { + return { + status: "ACCOUNT_LINKING_NOT_ALLOWED_ERROR", + description: + "Input user is already linked to another account. Please try again or contact support.", + }; + } + } + // status: "OK" + yield session.fetchAndSetClaim(accountLinkingClaim_1.AccountLinkingClaim, userContext); + return { + status: "NEW_ACCOUNT_NEEDS_TO_BE_VERIFIED_ERROR", + description: "Before accounts can be linked, the new account must be verified", + }; + } + // status: "OK" | "ACCOUNT_LINKING_NOT_ALLOWED_ERROR" + return result; }); }, - emailExistsGET: function ({ email, options, userContext }) { + emailExistsGET: function ({ email, userContext }) { return __awaiter(this, void 0, void 0, function* () { - let user = yield options.recipeImplementation.getUserByEmail({ email, userContext }); + let usersWithSameEmail = yield __1.listUsersByAccountInfo( + { + email, + }, + userContext + ); + let exists = + usersWithSameEmail.find((user) => { + return ( + user.loginMethods.find((lM) => { + return lM.recipeId === "emailpassword" && lM.email === email; + }) !== undefined + ); + }) !== undefined; return { status: "OK", - exists: user !== undefined, + exists, }; }); }, generatePasswordResetTokenPOST: function ({ formFields, options, userContext }) { return __awaiter(this, void 0, void 0, function* () { - let email = formFields.filter((f) => f.id === "email")[0].value; - let user = yield options.recipeImplementation.getUserByEmail({ email, userContext }); - if (user === undefined) { - return { - status: "OK", - }; + const email = formFields.filter((f) => f.id === "email")[0].value; + // this function will be reused in different parts of the flow below.. + function generateAndSendPasswordResetToken(userId) { + return __awaiter(this, void 0, void 0, function* () { + // the user ID here can be primary or recipe level. + let response = yield options.recipeImplementation.createResetPasswordToken({ + userId, + email, + userContext, + }); + if (response.status === "UNKNOWN_USER_ID_ERROR") { + logger_1.logDebugMessage(`Password reset email not sent, unknown user id: ${userId}`); + return { + status: "OK", + }; + } + let passwordResetLink = + options.appInfo.websiteDomain.getAsStringDangerous() + + options.appInfo.websiteBasePath.getAsStringDangerous() + + "/reset-password?token=" + + response.token + + "&rid=" + + options.recipeId; + logger_1.logDebugMessage(`Sending password reset email to ${email}`); + yield options.emailDelivery.ingredientInterfaceImpl.sendEmail({ + type: "PASSWORD_RESET", + user: { + id: userId, + email, + }, + passwordResetLink, + userContext, + }); + return { + status: "OK", + }; + }); } - let response = yield options.recipeImplementation.createResetPasswordToken({ - userId: user.recipeUserId, - email: user.email, - userContext, + /** + * check if primaryUserId is linked with this email + */ + let users = yield __1.listUsersByAccountInfo({ + email, }); - if (response.status === "UNKNOWN_USER_ID_ERROR") { - logger_1.logDebugMessage(`Password reset email not sent, unknown user id: ${user.id}`); + // we find the recipe user ID of the email password account from the user's list + // for later use. + let emailPasswordAccount = undefined; + for (let i = 0; i < users.length; i++) { + let emailPasswordAccountTmp = users[i].loginMethods.find( + (l) => l.recipeId === "emailpassword" && l.email === email + ); + if (emailPasswordAccount !== undefined) { + emailPasswordAccount = emailPasswordAccountTmp; + break; + } + } + // we find the primary user ID from the user's list for later use. + let primaryUserAssociatedWithEmail = users.find((u) => u.isPrimaryUser); + // first we check if there even exists a primary user that has the input email + // if not, then we do the regular flow for password reset. + if (primaryUserAssociatedWithEmail === undefined) { + if (emailPasswordAccount === undefined) { + logger_1.logDebugMessage(`Password reset email not sent, unknown user email: ${email}`); + return { + status: "OK", + }; + } + return yield generateAndSendPasswordResetToken(emailPasswordAccount.recipeUserId); + } + // Now we need to check that if there exists any email password user at all + // for the input email. If not, then it implies that when the token is consumed, + // then we will create a new user - so we should only generate the token if + // the criteria for the new user is met. + if (emailPasswordAccount === undefined) { + // this means that there is no email password user that exists for the input email. + // So we check for the sign up condition and only go ahead if that condition is + // met. + let isSignUpAllowed = yield recipe_1.default.getInstanceOrThrowError().isSignUpAllowed({ + newUser: { + recipeId: "emailpassword", + email, + }, + userContext, + }); + if (isSignUpAllowed) { + // notice that we pass in the primary user ID here. This means that + // we will be creating a new email password account with the token + // is consumed and linking it to this primary user. + return yield generateAndSendPasswordResetToken(primaryUserAssociatedWithEmail.id); + } else { + logger_1.logDebugMessage( + `Password reset email not sent, isSignUpAllowed returned false for email: ${email}` + ); + return { + status: "OK", + }; + } + } + // At this point, we know that some email password user exists with this email + // and also some primary user ID exist. We now need to find out if they are linked + // together or not. If they are linked together, then we can just generate the token + // else we check for more security conditions (since we will be linking them post token generation) + let areTheTwoAccountsLinked = + primaryUserAssociatedWithEmail.loginMethods.find((lm) => { + return lm.recipeUserId === emailPasswordAccount.recipeUserId; + }) !== undefined; + if (areTheTwoAccountsLinked) { + return yield generateAndSendPasswordResetToken(emailPasswordAccount.recipeId); + } + // Here we know that the two accounts are NOT linked. We now need to check for an + // extra security measure here to make sure that the input email in the primary user + // is verified, and if not, we need to make sure that there is no other email / phone number + // associated with the primary user account. If there is, then we do not proceed. + /* + This security measure helps prevent the following attack: + An attacker has email A and they create an account using TP and it doesn't matter if A is verified or not. Now they create another account using EP with email A and verifies it. Both these accounts are linked. Now the attacker changes the email for EP recipe to B which makes the EP account unverified, but it's still linked. + + If the real owner of B tries to signup using EP, it will say that the account already exists so they may try to reset password which should be denied because then they will end up getting access to attacker's account and verify the EP account. + + The problem with this situation is if the EP account is verified, it will allow further sign-ups with email B which will also be linked to this primary account (that the attacker had created with email A). + + It is important to realize that the attacker had created another account with A because if they hadn't done that, then they wouldn't have access to this account after the real user resets the password which is why it is important to check there is another non-EP account linked to the primary such that the email is not the same as B. + + Exception to the above is that, if there is a third recipe account linked to the above two accounts and has B as verified, then we should allow reset password token generation because user has already proven that the owns the email B + */ + // But first, this only matters it the user cares about checking for email verification status.. + let shouldDoAccountLinkingResponse = yield recipe_1.default + .getInstanceOrThrowError() + .config.shouldDoAutomaticAccountLinking( + emailPasswordAccount, + primaryUserAssociatedWithEmail, + undefined, + userContext + ); + if (!shouldDoAccountLinkingResponse.shouldAutomaticallyLink) { + // here we will go ahead with the token generation cause + // even when the token is consumed, we will not be linking the accounts + // so no need to check for anything + return yield generateAndSendPasswordResetToken(emailPasswordAccount.recipeId); + } + if (!shouldDoAccountLinkingResponse.shouldRequireVerification) { + // the checks below are related to email verification, and if the user + // does not care about that, then we should just continue with token generation + return yield generateAndSendPasswordResetToken(emailPasswordAccount.recipeId); + } + // Now we start the required security checks. First we check if the primary user + // it has just one linked account. And if that's true, then we continue + // cause then there is no scope for account takeover + if (primaryUserAssociatedWithEmail.loginMethods.length === 1) { + return yield generateAndSendPasswordResetToken(emailPasswordAccount.recipeId); + } + // Next we check if there is any login method in which the input email is verified. + // If that is the case, then it's proven that the user owns the email and we can + // trust linking of the email password account. + let emailVerified = + primaryUserAssociatedWithEmail.loginMethods.find((lm) => { + return lm.email === email && lm.verified; + }) !== undefined; + if (emailVerified) { + return yield generateAndSendPasswordResetToken(emailPasswordAccount.recipeId); + } + // finally, we check if the primary user has any other email / phone number + // associated with this account - and if it does, then it means that + // there is a risk of account takeover, so we do not allow the token to be generated + let hasOtherEmailOrPhone = + primaryUserAssociatedWithEmail.loginMethods.find((lm) => { + return lm.email !== email || lm.phoneNumber !== undefined; + }) !== undefined; + if (hasOtherEmailOrPhone) { return { - status: "OK", + status: "PASSWORD_RESET_NOT_ALLOWED", + reason: + "Token generation was not done because of account take over risk. Please contact support.", }; + } else { + return yield generateAndSendPasswordResetToken(emailPasswordAccount.recipeId); } - let passwordResetLink = - options.appInfo.websiteDomain.getAsStringDangerous() + - options.appInfo.websiteBasePath.getAsStringDangerous() + - "/reset-password?token=" + - response.token + - "&rid=" + - options.recipeId; - logger_1.logDebugMessage(`Sending password reset email to ${email}`); - yield options.emailDelivery.ingredientInterfaceImpl.sendEmail({ - type: "PASSWORD_RESET", - user, - passwordResetLink, - userContext, - }); - return { - status: "OK", - }; }); }, passwordResetPOST: function ({ formFields, token, options, userContext }) { return __awaiter(this, void 0, void 0, function* () { + function markEmailAsVerified(userId, email) { + return __awaiter(this, void 0, void 0, function* () { + const emailVerificationInstance = recipe_2.default.getInstance(); + if (emailVerificationInstance) { + const tokenResponse = yield emailVerificationInstance.recipeInterfaceImpl.createEmailVerificationToken( + { + userId, + email, + userContext, + } + ); + if (tokenResponse.status === "OK") { + yield emailVerificationInstance.recipeInterfaceImpl.verifyEmailUsingToken({ + token: tokenResponse.token, + userContext, + }); + } + } + }); + } + function doUpdatePassword() { + return __awaiter(this, void 0, void 0, function* () { + let updateResponse = yield options.recipeImplementation.updateEmailOrPassword({ + userId: recipeUserIdForWhomTokenWasGenerated, + password: newPassword, + userContext, + }); + if ( + updateResponse.status === "EMAIL_ALREADY_EXISTS_ERROR" || + updateResponse.status === "EMAIL_CHANGE_NOT_ALLOWED_ERROR" + ) { + throw new Error("This should never come here because we are not updating the email"); + } else if (updateResponse.status === "UNKNOWN_USER_ID_ERROR") { + // This should happen only cause of a race condition where the user + // might be deleted before token creation and consumption. + return { + status: "RESET_PASSWORD_INVALID_TOKEN_ERROR", + }; + } else { + // status: "OK" + return { + status: "OK", + user: existingUser, + email: emailForWhomTokenWasGenerated, + }; + } + // TODO: we need to also handle password policy error. Note that this needs + // to happen in the api file (before this function is called) as well + // cause we don't want to consume the token unnecessarily. + }); + } let newPassword = formFields.filter((f) => f.id === "password")[0].value; - let response = yield options.recipeImplementation.resetPasswordUsingToken({ + let tokenConsumptionResponse = yield options.recipeImplementation.consumePasswordResetToken({ token, - newPassword, userContext, }); - return response; + if (tokenConsumptionResponse.status === "RESET_PASSWORD_INVALID_TOKEN_ERROR") { + return tokenConsumptionResponse; + } + let recipeUserIdForWhomTokenWasGenerated = tokenConsumptionResponse.userId; + let emailForWhomTokenWasGenerated = tokenConsumptionResponse.email; + let existingUser = yield __1.getUser(tokenConsumptionResponse.userId, userContext); + if (existingUser === undefined) { + // This should happen only cause of a race condition where the user + // might be deleted before token creation and consumption. + // Also note that this being undefined doesn't mean that the email password + // user does not exist, but it means that there is no reicpe or primary user + // for whom the token was generated. + return { + status: "RESET_PASSWORD_INVALID_TOKEN_ERROR", + }; + } + // We start by checking if the existingUser is a primary user or not. If it is, + // then we will try and create a new email password user and link it to the primary user (if required) + if (existingUser.isPrimaryUser) { + // If this user contains an email password account for whom the token was generated, + // then we update that user's password. + let emailPasswordUserIsLinkedToExistingUser = + existingUser.loginMethods.find((lm) => { + // we check based on user ID and not email because the only time + // the primary user ID is used for token generation is if the email password + // user did not exist - in which case the value of emailPasswordUserExists will + // resolve to false anyway, and that's what we want. + return lm.recipeUserId === recipeUserIdForWhomTokenWasGenerated; + }) !== undefined; + if (emailPasswordUserIsLinkedToExistingUser) { + return doUpdatePassword(); + } else { + // this means that the existingUser does not have an emailpassword user associated + // with it. It could now mean that no emailpassword user exists, or it could mean that + // the the ep user exists, but it's not linked to the current account. + // If no ep user doesn't exists, we will create one, and link it to the existing account. + // If ep user exists, then it means there is some race condition cause + // then the token should have been generated for that user instead of the primary user, + // and it shouldn't have come into this branch. So we can simply send a password reset + // invalid error and the user can try again. + // NOTE: We do not ask the dev if we should do account linking or not here + // cause we already have asked them this when generating an password reset token. + let createUserResponse = yield options.recipeImplementation.createNewRecipeUser({ + email: tokenConsumptionResponse.email, + password: newPassword, + userContext, + }); + if (createUserResponse.status === "EMAIL_ALREADY_EXISTS_ERROR") { + // this means that the user already existed and we can just return an invalid + // token (see the above comment) + return { + status: "RESET_PASSWORD_INVALID_TOKEN_ERROR", + }; + } else { + // we mark the email as verified because password reset also requires + // access to the email to work.. This has a good side effect that + // any other login method with the same email in existingAccount will also get marked + // as verified. + yield markEmailAsVerified(createUserResponse.user.id, tokenConsumptionResponse.email); + // Now we try and link the accounts. The function below will try and also + // create a primary user of the new account, and if it does that, it's OK.. + // But in most cases, it will end up linking to existing account since the + // email is shared. + let linkedToUserId = yield recipe_1.default + .getInstanceOrThrowError() + .createPrimaryUserIdOrLinkAccounts({ + recipeUserId: createUserResponse.user.id, + isVerified: true, + checkAccountsToLinkTableAsWell: true, + userContext, + }); + if (linkedToUserId !== existingUser.id) { + // this means that the account we just linked to + // was not the one we had expected to link it to. This can happen + // due to some race condition or the other.. Either way, this + // is not an issue and we can just return OK + } + return { + status: "OK", + email: tokenConsumptionResponse.email, + user: yield __1.getUser(linkedToUserId, userContext), // we refetch cause we want to return the user object with the updated login methods. + }; + } + } + } else { + // This means that the existing user is not a primary account, which implies that + // it must be a non linked email password account. In this case, we simply update the password. + // Linking to an existing account will be done after the user goes through the email + // verification flow once they log in (if applicable). + return doUpdatePassword(); + } }); }, signInPOST: function ({ formFields, options, userContext }) { @@ -116,12 +485,18 @@ function getAPIImplementation() { if (response.status === "WRONG_CREDENTIALS_ERROR") { return response; } - let user = response.user; + let emailPasswordRecipeUser = response.user.loginMethods.find( + (u) => u.recipeId === "emailpassword" && u.email === email + ); + if (emailPasswordRecipeUser === undefined) { + // this can happen cause of some race condition, but it's not a big deal. + throw new Error("Race condition error - please call this API again"); + } let session = yield session_1.default.createNewSession( options.req, options.res, - user.id, - user.recipeUserId, + response.user.id, + emailPasswordRecipeUser.recipeUserId, {}, {}, userContext @@ -129,7 +504,7 @@ function getAPIImplementation() { return { status: "OK", session, - user, + user: response.user, }; }); }, @@ -137,16 +512,41 @@ function getAPIImplementation() { return __awaiter(this, void 0, void 0, function* () { let email = formFields.filter((f) => f.id === "email")[0].value; let password = formFields.filter((f) => f.id === "password")[0].value; - let response = yield options.recipeImplementation.signUp({ email, password, userContext }); + let isSignUpAllowed = yield recipe_1.default.getInstanceOrThrowError().isSignUpAllowed({ + newUser: { + recipeId: "emailpassword", + email, + }, + userContext, + }); + if (!isSignUpAllowed) { + return { + status: "SIGNUP_NOT_ALLOWED", + reason: + "The input email is already associated with another account where it is not verified. Please verify the other account before trying again.", + }; + } + // this function also does account linking + let response = yield options.recipeImplementation.signUp({ + email, + password, + userContext, + }); if (response.status === "EMAIL_ALREADY_EXISTS_ERROR") { return response; } - let user = response.user; + let emailPasswordRecipeUser = response.user.loginMethods.find( + (u) => u.recipeId === "emailpassword" && u.email === email + ); + if (emailPasswordRecipeUser === undefined) { + // this can happen cause of some race condition, but it's not a big deal. + throw new Error("Race condition error - please call this API again"); + } let session = yield session_1.default.createNewSession( options.req, options.res, - user.id, - user.recipeUserId, + response.user.id, + emailPasswordRecipeUser.recipeUserId, {}, {}, userContext @@ -154,9 +554,7 @@ function getAPIImplementation() { return { status: "OK", session, - user, - createdNewUser: true, - createdNewRecipeUser: true, // TODO + user: response.user, }; }); }, diff --git a/lib/build/recipe/emailpassword/api/linkAccountToExistingAccount.d.ts b/lib/build/recipe/emailpassword/api/linkAccountToExistingAccount.d.ts new file mode 100644 index 000000000..5588e7dd1 --- /dev/null +++ b/lib/build/recipe/emailpassword/api/linkAccountToExistingAccount.d.ts @@ -0,0 +1,6 @@ +// @ts-nocheck +import { APIInterface, APIOptions } from ".."; +export default function linkAccountToExistingAccountAPI( + apiImplementation: APIInterface, + options: APIOptions +): Promise; diff --git a/lib/build/recipe/emailpassword/api/linkAccountToExistingAccount.js b/lib/build/recipe/emailpassword/api/linkAccountToExistingAccount.js new file mode 100644 index 000000000..643a21819 --- /dev/null +++ b/lib/build/recipe/emailpassword/api/linkAccountToExistingAccount.js @@ -0,0 +1,84 @@ +"use strict"; +/* 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. + */ +var __awaiter = + (this && this.__awaiter) || + function (thisArg, _arguments, P, generator) { + function adopt(value) { + return value instanceof P + ? value + : new P(function (resolve) { + resolve(value); + }); + } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { + try { + step(generator.next(value)); + } catch (e) { + reject(e); + } + } + function rejected(value) { + try { + step(generator["throw"](value)); + } catch (e) { + reject(e); + } + } + function step(result) { + result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); + } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); + }; +var __importDefault = + (this && this.__importDefault) || + function (mod) { + return mod && mod.__esModule ? mod : { default: mod }; + }; +Object.defineProperty(exports, "__esModule", { value: true }); +const utils_1 = require("../../../utils"); +const utils_2 = require("./utils"); +const utils_3 = require("../../../utils"); +const session_1 = __importDefault(require("../../session")); +function linkAccountToExistingAccountAPI(apiImplementation, options) { + return __awaiter(this, void 0, void 0, function* () { + if (apiImplementation.linkAccountToExistingAccountPOST === undefined) { + return false; + } + let formFields = yield utils_2.validateFormFieldsOrThrowError( + options.config.signUpFeature.formFields, + (yield options.req.getJSONBody()).formFields + ); + let userContext = utils_3.makeDefaultUserContextFromAPI(options.req); + const session = yield session_1.default.getSession( + options.req, + options.res, + { overrideGlobalClaimValidators: () => [] }, + userContext + ); + let result = yield apiImplementation.linkAccountToExistingAccountPOST({ + formFields, + session: session, + options, + userContext, + }); + // status: NEW_ACCOUNT_NEEDS_TO_BE_VERIFIED_ERROR | ACCOUNT_LINKING_NOT_ALLOWED_ERROR | WRONG_CREDENTIALS_ERROR | GENERAL_ERROR | "OK" + utils_1.send200Response(options.res, result); + return true; + }); +} +exports.default = linkAccountToExistingAccountAPI; diff --git a/lib/build/recipe/emailpassword/api/passwordReset.js b/lib/build/recipe/emailpassword/api/passwordReset.js index dc8c29d4f..305462d75 100644 --- a/lib/build/recipe/emailpassword/api/passwordReset.js +++ b/lib/build/recipe/emailpassword/api/passwordReset.js @@ -60,7 +60,10 @@ function passwordReset(apiImplementation, options) { if (apiImplementation.passwordResetPOST === undefined) { return false; } - // step 1 + // step 1: We need to do this here even though the update emailpassword recipe function would do this cause: + // - we want to throw this error before consuming the token, so that the user can try again + // - there is a case in the api impl where we create a new user, and we want to assign + // a password that meets the password policy. let formFields = yield utils_2.validateFormFieldsOrThrowError( options.config.resetPasswordUsingTokenFeature.formFieldsForPasswordResetForm, (yield options.req.getJSONBody()).formFields diff --git a/lib/build/recipe/emailpassword/constants.d.ts b/lib/build/recipe/emailpassword/constants.d.ts index 9b58b697a..d0dbede02 100644 --- a/lib/build/recipe/emailpassword/constants.d.ts +++ b/lib/build/recipe/emailpassword/constants.d.ts @@ -6,3 +6,4 @@ export declare const SIGN_IN_API = "/signin"; export declare const GENERATE_PASSWORD_RESET_TOKEN_API = "/user/password/reset/token"; export declare const PASSWORD_RESET_API = "/user/password/reset"; export declare const SIGNUP_EMAIL_EXISTS_API = "/signup/email/exists"; +export declare const LINK_ACCOUNT_TO_EXISTING_ACCOUNT_API = "/signup/link-account"; diff --git a/lib/build/recipe/emailpassword/constants.js b/lib/build/recipe/emailpassword/constants.js index 465baa718..a9f779e46 100644 --- a/lib/build/recipe/emailpassword/constants.js +++ b/lib/build/recipe/emailpassword/constants.js @@ -14,7 +14,7 @@ * under the License. */ Object.defineProperty(exports, "__esModule", { value: true }); -exports.SIGNUP_EMAIL_EXISTS_API = exports.PASSWORD_RESET_API = exports.GENERATE_PASSWORD_RESET_TOKEN_API = exports.SIGN_IN_API = exports.SIGN_UP_API = exports.FORM_FIELD_EMAIL_ID = exports.FORM_FIELD_PASSWORD_ID = void 0; +exports.LINK_ACCOUNT_TO_EXISTING_ACCOUNT_API = exports.SIGNUP_EMAIL_EXISTS_API = exports.PASSWORD_RESET_API = exports.GENERATE_PASSWORD_RESET_TOKEN_API = exports.SIGN_IN_API = exports.SIGN_UP_API = exports.FORM_FIELD_EMAIL_ID = exports.FORM_FIELD_PASSWORD_ID = void 0; exports.FORM_FIELD_PASSWORD_ID = "password"; exports.FORM_FIELD_EMAIL_ID = "email"; exports.SIGN_UP_API = "/signup"; @@ -22,3 +22,4 @@ exports.SIGN_IN_API = "/signin"; exports.GENERATE_PASSWORD_RESET_TOKEN_API = "/user/password/reset/token"; exports.PASSWORD_RESET_API = "/user/password/reset"; exports.SIGNUP_EMAIL_EXISTS_API = "/signup/email/exists"; +exports.LINK_ACCOUNT_TO_EXISTING_ACCOUNT_API = "/signup/link-account"; diff --git a/lib/build/recipe/emailpassword/emaildelivery/services/backwardCompatibility/index.d.ts b/lib/build/recipe/emailpassword/emaildelivery/services/backwardCompatibility/index.d.ts index 77620015d..9844769ff 100644 --- a/lib/build/recipe/emailpassword/emaildelivery/services/backwardCompatibility/index.d.ts +++ b/lib/build/recipe/emailpassword/emaildelivery/services/backwardCompatibility/index.d.ts @@ -1,25 +1,13 @@ // @ts-nocheck -import { TypeEmailPasswordEmailDeliveryInput, User, RecipeInterface } from "../../../types"; +import { TypeEmailPasswordEmailDeliveryInput, RecipeInterface } from "../../../types"; import { NormalisedAppinfo } from "../../../../../types"; import { EmailDeliveryInterface } from "../../../../../ingredients/emaildelivery/types"; export default class BackwardCompatibilityService implements EmailDeliveryInterface { - private recipeInterfaceImpl; private isInServerlessEnv; private appInfo; private resetPasswordUsingTokenFeature; - constructor( - recipeInterfaceImpl: RecipeInterface, - appInfo: NormalisedAppinfo, - isInServerlessEnv: boolean, - resetPasswordUsingTokenFeature?: { - createAndSendCustomEmail?: ( - user: User, - passwordResetURLWithToken: string, - userContext: any - ) => Promise; - } - ); + constructor(_: RecipeInterface, appInfo: NormalisedAppinfo, isInServerlessEnv: boolean); sendEmail: ( input: TypeEmailPasswordEmailDeliveryInput & { userContext: any; diff --git a/lib/build/recipe/emailpassword/emaildelivery/services/backwardCompatibility/index.js b/lib/build/recipe/emailpassword/emaildelivery/services/backwardCompatibility/index.js index 890df30ba..ffb8a6093 100644 --- a/lib/build/recipe/emailpassword/emaildelivery/services/backwardCompatibility/index.js +++ b/lib/build/recipe/emailpassword/emaildelivery/services/backwardCompatibility/index.js @@ -33,51 +33,33 @@ var __awaiter = Object.defineProperty(exports, "__esModule", { value: true }); const passwordResetFunctions_1 = require("../../../passwordResetFunctions"); class BackwardCompatibilityService { - constructor(recipeInterfaceImpl, appInfo, isInServerlessEnv, resetPasswordUsingTokenFeature) { + constructor(_, appInfo, isInServerlessEnv) { this.sendEmail = (input) => __awaiter(this, void 0, void 0, function* () { - let user = yield this.recipeInterfaceImpl.getUserById({ - userId: input.user.recipeUserId, - userContext: input.userContext, - }); - if (user === undefined) { - throw Error("this should never come here"); - } // we add this here cause the user may have overridden the sendEmail function // to change the input email and if we don't do this, the input email // will get reset by the getUserById call above. - user.email = input.user.email; try { if (!this.isInServerlessEnv) { this.resetPasswordUsingTokenFeature - .createAndSendCustomEmail(user, input.passwordResetLink, input.userContext) + .createAndSendCustomEmail(input.user, input.passwordResetLink, input.userContext) .catch((_) => {}); } else { // see https://github.com/supertokens/supertokens-node/pull/135 yield this.resetPasswordUsingTokenFeature.createAndSendCustomEmail( - user, + input.user, input.passwordResetLink, input.userContext ); } } catch (_) {} }); - this.recipeInterfaceImpl = recipeInterfaceImpl; this.isInServerlessEnv = isInServerlessEnv; this.appInfo = appInfo; { - let inputCreateAndSendCustomEmail = - resetPasswordUsingTokenFeature === null || resetPasswordUsingTokenFeature === void 0 - ? void 0 - : resetPasswordUsingTokenFeature.createAndSendCustomEmail; - this.resetPasswordUsingTokenFeature = - inputCreateAndSendCustomEmail !== undefined - ? { - createAndSendCustomEmail: inputCreateAndSendCustomEmail, - } - : { - createAndSendCustomEmail: passwordResetFunctions_1.createAndSendCustomEmail(this.appInfo), - }; + this.resetPasswordUsingTokenFeature = { + createAndSendCustomEmail: passwordResetFunctions_1.createAndSendCustomEmail(this.appInfo), + }; } } } diff --git a/lib/build/recipe/emailpassword/index.d.ts b/lib/build/recipe/emailpassword/index.d.ts index 51b059774..1e8837b73 100644 --- a/lib/build/recipe/emailpassword/index.d.ts +++ b/lib/build/recipe/emailpassword/index.d.ts @@ -1,7 +1,8 @@ // @ts-nocheck import Recipe from "./recipe"; import SuperTokensError from "./error"; -import { RecipeInterface, User, APIOptions, APIInterface, TypeEmailPasswordEmailDeliveryInput } from "./types"; +import { RecipeInterface, APIOptions, APIInterface, TypeEmailPasswordEmailDeliveryInput } from "./types"; +import { User } from "../../types"; export default class Wrapper { static init: typeof Recipe.init; static Error: typeof SuperTokensError; @@ -31,8 +32,6 @@ export default class Wrapper { status: "WRONG_CREDENTIALS_ERROR"; } >; - static getUserById(userId: string, userContext?: any): Promise; - static getUserByEmail(email: string, userContext?: any): Promise; /** * We do not make email optional here cause we want to * allow passing in primaryUserId. If we make email optional, @@ -57,9 +56,8 @@ export default class Wrapper { status: "UNKNOWN_USER_ID_ERROR"; } >; - static resetPasswordUsingToken( + static consumePasswordResetToken( token: string, - newPassword: string, userContext?: any ): Promise< | { @@ -76,9 +74,15 @@ export default class Wrapper { email?: string; password?: string; userContext?: any; - }): Promise<{ - status: "OK" | "UNKNOWN_USER_ID_ERROR" | "EMAIL_ALREADY_EXISTS_ERROR"; - }>; + }): Promise< + | { + status: "OK" | "UNKNOWN_USER_ID_ERROR" | "EMAIL_ALREADY_EXISTS_ERROR"; + } + | { + status: "EMAIL_CHANGE_NOT_ALLOWED_ERROR"; + reason: string; + } + >; static sendEmail( input: TypeEmailPasswordEmailDeliveryInput & { userContext?: any; @@ -89,10 +93,8 @@ export declare let init: typeof Recipe.init; export declare let Error: typeof SuperTokensError; export declare let signUp: typeof Wrapper.signUp; export declare let signIn: typeof Wrapper.signIn; -export declare let getUserById: typeof Wrapper.getUserById; -export declare let getUserByEmail: typeof Wrapper.getUserByEmail; export declare let createResetPasswordToken: typeof Wrapper.createResetPasswordToken; -export declare let resetPasswordUsingToken: typeof Wrapper.resetPasswordUsingToken; +export declare let consumePasswordResetToken: typeof Wrapper.consumePasswordResetToken; export declare let updateEmailOrPassword: typeof Wrapper.updateEmailOrPassword; export type { RecipeInterface, User, APIOptions, APIInterface }; export declare let sendEmail: typeof Wrapper.sendEmail; diff --git a/lib/build/recipe/emailpassword/index.js b/lib/build/recipe/emailpassword/index.js index 6e409b332..cf1da42f2 100644 --- a/lib/build/recipe/emailpassword/index.js +++ b/lib/build/recipe/emailpassword/index.js @@ -50,7 +50,7 @@ var __importDefault = return mod && mod.__esModule ? mod : { default: mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); -exports.sendEmail = exports.updateEmailOrPassword = exports.resetPasswordUsingToken = exports.createResetPasswordToken = exports.getUserByEmail = exports.getUserById = exports.signIn = exports.signUp = exports.Error = exports.init = void 0; +exports.sendEmail = exports.updateEmailOrPassword = exports.consumePasswordResetToken = exports.createResetPasswordToken = exports.signIn = exports.signUp = exports.Error = exports.init = void 0; const recipe_1 = __importDefault(require("./recipe")); const error_1 = __importDefault(require("./error")); class Wrapper { @@ -68,18 +68,6 @@ class Wrapper { userContext: userContext === undefined ? {} : userContext, }); } - static getUserById(userId, userContext) { - return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.getUserById({ - userId, - userContext: userContext === undefined ? {} : userContext, - }); - } - static getUserByEmail(email, userContext) { - return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.getUserByEmail({ - email, - userContext: userContext === undefined ? {} : userContext, - }); - } /** * We do not make email optional here cause we want to * allow passing in primaryUserId. If we make email optional, @@ -98,10 +86,9 @@ class Wrapper { userContext: userContext === undefined ? {} : userContext, }); } - static resetPasswordUsingToken(token, newPassword, userContext) { - return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.resetPasswordUsingToken({ + static consumePasswordResetToken(token, userContext) { + return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.consumePasswordResetToken({ token, - newPassword, userContext: userContext === undefined ? {} : userContext, }); } @@ -126,9 +113,7 @@ exports.init = Wrapper.init; exports.Error = Wrapper.Error; exports.signUp = Wrapper.signUp; exports.signIn = Wrapper.signIn; -exports.getUserById = Wrapper.getUserById; -exports.getUserByEmail = Wrapper.getUserByEmail; exports.createResetPasswordToken = Wrapper.createResetPasswordToken; -exports.resetPasswordUsingToken = Wrapper.resetPasswordUsingToken; +exports.consumePasswordResetToken = Wrapper.consumePasswordResetToken; exports.updateEmailOrPassword = Wrapper.updateEmailOrPassword; exports.sendEmail = Wrapper.sendEmail; diff --git a/lib/build/recipe/emailpassword/passwordResetFunctions.d.ts b/lib/build/recipe/emailpassword/passwordResetFunctions.d.ts index 56b10c948..008e7d923 100644 --- a/lib/build/recipe/emailpassword/passwordResetFunctions.d.ts +++ b/lib/build/recipe/emailpassword/passwordResetFunctions.d.ts @@ -1,6 +1,11 @@ // @ts-nocheck -import { User } from "./types"; import { NormalisedAppinfo } from "../../types"; export declare function createAndSendCustomEmail( appInfo: NormalisedAppinfo -): (user: User, passwordResetURLWithToken: string) => Promise; +): ( + user: { + id: string; + email: string; + }, + passwordResetURLWithToken: string +) => Promise; diff --git a/lib/build/recipe/emailpassword/recipe.js b/lib/build/recipe/emailpassword/recipe.js index e22893e5e..fb106bbce 100644 --- a/lib/build/recipe/emailpassword/recipe.js +++ b/lib/build/recipe/emailpassword/recipe.js @@ -68,6 +68,7 @@ const querier_1 = require("../../querier"); const supertokens_js_override_1 = __importDefault(require("supertokens-js-override")); const emaildelivery_1 = __importDefault(require("../../ingredients/emaildelivery")); const postSuperTokensInitCallbacks_1 = require("../../postSuperTokensInitCallbacks"); +const __1 = require("../../"); class Recipe extends recipeModule_1.default { constructor(recipeId, appInfo, isInServerlessEnv, config, ingredients) { super(recipeId, appInfo); @@ -106,6 +107,14 @@ class Recipe extends recipeModule_1.default { id: constants_1.SIGNUP_EMAIL_EXISTS_API, disabled: this.apiImpl.emailExistsGET === undefined, }, + { + method: "post", + pathWithoutApiBasePath: new normalisedURLPath_1.default( + constants_1.LINK_ACCOUNT_TO_EXISTING_ACCOUNT_API + ), + id: constants_1.LINK_ACCOUNT_TO_EXISTING_ACCOUNT_API, + disabled: this.apiImpl.linkAccountToExistingAccountPOST === undefined, + }, ]; }; this.handleAPIRequest = (id, req, res, _path, _method) => @@ -157,12 +166,21 @@ class Recipe extends recipeModule_1.default { // extra instance functions below............... this.getEmailForUserId = (userId, userContext) => __awaiter(this, void 0, void 0, function* () { - let userInfo = yield this.recipeInterfaceImpl.getUserById({ userId, userContext }); - if (userInfo !== undefined) { - return { - status: "OK", - email: userInfo.email, - }; + let user = yield __1.getUser(userId, userContext); + if (user !== undefined) { + let recipeLevelUser = user.loginMethods.find( + (u) => u.recipeId === "emailpassword" && u.recipeUserId === userId + ); + if (recipeLevelUser !== undefined) { + if (recipeLevelUser.email === undefined) { + // this check if only for types purposes. + throw new Error("Should never come here"); + } + return { + status: "OK", + email: recipeLevelUser.email, + }; + } } return { status: "UNKNOWN_USER_ID_ERROR", diff --git a/lib/build/recipe/emailpassword/recipeImplementation.js b/lib/build/recipe/emailpassword/recipeImplementation.js index 3820148ef..bb55e005e 100644 --- a/lib/build/recipe/emailpassword/recipeImplementation.js +++ b/lib/build/recipe/emailpassword/recipeImplementation.js @@ -36,116 +36,85 @@ var __importDefault = return mod && mod.__esModule ? mod : { default: mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); +const recipe_1 = __importDefault(require("../accountlinking/recipe")); const normalisedURLPath_1 = __importDefault(require("../../normalisedURLPath")); +const __1 = require("../.."); function getRecipeInterface(querier) { return { - signUp: function ({ email, password }) { + signUp: function ({ email, password, userContext }) { return __awaiter(this, void 0, void 0, function* () { - let response = yield querier.sendPostRequest(new normalisedURLPath_1.default("/recipe/signup"), { + // this function does not check if there is some primary user where the email + // of that primary user is unverified (isSignUpAllowed function logic) cause + // that is checked in the API layer before calling this function. + // This is the recipe function layer which can be + // called by the user manually as well if they want to. So we allow them to do that. + let response = yield this.createNewRecipeUser({ email, password, + userContext, }); - if (response.status === "OK") { + if (response.status === "EMAIL_ALREADY_EXISTS_ERROR") { return response; - } else { - return { - status: "EMAIL_ALREADY_EXISTS_ERROR", - }; } - }); - }, - signIn: function ({ email, password }) { - return __awaiter(this, void 0, void 0, function* () { - let response = yield querier.sendPostRequest(new normalisedURLPath_1.default("/recipe/signin"), { - email, - password, + let userId = yield recipe_1.default.getInstanceOrThrowError().createPrimaryUserIdOrLinkAccounts({ + // we can use index 0 cause this is a new recipe user + recipeUserId: response.user.loginMethods[0].recipeUserId, + checkAccountsToLinkTableAsWell: true, + isVerified: false, + userContext, }); - if (response.status === "OK") { - return response; - } else { - return { - status: "WRONG_CREDENTIALS_ERROR", - }; - } + return { + status: "OK", + user: yield __1.getUser(userId, userContext), + }; }); }, - getUserById: function ({ userId }) { + createNewRecipeUser: function (input) { return __awaiter(this, void 0, void 0, function* () { - let response = yield querier.sendGetRequest(new normalisedURLPath_1.default("/recipe/user"), { - userId, + return yield querier.sendPostRequest(new normalisedURLPath_1.default("/recipe/signup"), { + email: input.email, + password: input.password, }); - if (response.status === "OK") { - return Object.assign({}, response.user); - } else { - return undefined; - } }); }, - getUserByEmail: function ({ email }) { + signIn: function ({ email, password }) { return __awaiter(this, void 0, void 0, function* () { - let response = yield querier.sendGetRequest(new normalisedURLPath_1.default("/recipe/user"), { + return yield querier.sendPostRequest(new normalisedURLPath_1.default("/recipe/signin"), { email, + password, }); - if (response.status === "OK") { - return Object.assign({}, response.user); - } else { - return undefined; - } }); }, - createResetPasswordToken: function ({ userId }) { + createResetPasswordToken: function ({ userId, email }) { return __awaiter(this, void 0, void 0, function* () { - let response = yield querier.sendPostRequest( + // the input user ID can be a recipe or a primary user ID. + return yield querier.sendPostRequest( new normalisedURLPath_1.default("/recipe/user/password/reset/token"), { userId, + email, } ); - if (response.status === "OK") { - return { - status: "OK", - token: response.token, - }; - } else { - return { - status: "UNKNOWN_USER_ID_ERROR", - }; - } }); }, - resetPasswordUsingToken: function ({ token, newPassword }) { + consumePasswordResetToken: function ({ token }) { return __awaiter(this, void 0, void 0, function* () { - let response = yield querier.sendPostRequest( - new normalisedURLPath_1.default("/recipe/user/password/reset"), + return yield querier.sendPostRequest( + new normalisedURLPath_1.default("/recipe/user/password/reset/token/consume"), { - method: "token", token, - newPassword, } ); - return response; }); }, updateEmailOrPassword: function (input) { return __awaiter(this, void 0, void 0, function* () { - let response = yield querier.sendPutRequest(new normalisedURLPath_1.default("/recipe/user"), { + // the input can be primary or recipe level user id. + return yield querier.sendPutRequest(new normalisedURLPath_1.default("/recipe/user"), { userId: input.userId, email: input.email, password: input.password, }); - if (response.status === "OK") { - return { - status: "OK", - }; - } else if (response.status === "EMAIL_ALREADY_EXISTS_ERROR") { - return { - status: "EMAIL_ALREADY_EXISTS_ERROR", - }; - } else { - return { - status: "UNKNOWN_USER_ID_ERROR", - }; - } }); }, }; diff --git a/lib/build/recipe/emailpassword/types.d.ts b/lib/build/recipe/emailpassword/types.d.ts index b413e29b8..97451c8a0 100644 --- a/lib/build/recipe/emailpassword/types.d.ts +++ b/lib/build/recipe/emailpassword/types.d.ts @@ -7,7 +7,7 @@ import { TypeInputWithService as EmailDeliveryTypeInputWithService, } from "../../ingredients/emaildelivery/types"; import EmailDeliveryIngredient from "../../ingredients/emaildelivery"; -import { GeneralErrorResponse, NormalisedAppinfo } from "../../types"; +import { GeneralErrorResponse, NormalisedAppinfo, User } from "../../types"; export declare type TypeNormalisedInput = { signUpFeature: TypeNormalisedInputSignUp; signInFeature: TypeNormalisedInputSignIn; @@ -47,26 +47,13 @@ export declare type TypeNormalisedInputSignUp = { export declare type TypeNormalisedInputSignIn = { formFields: NormalisedFormField[]; }; -export declare type TypeInputResetPasswordUsingTokenFeature = { - /** - * @deprecated Please use emailDelivery config instead - */ - createAndSendCustomEmail?: (user: User, passwordResetURLWithToken: string, userContext: any) => Promise; -}; export declare type TypeNormalisedInputResetPasswordUsingTokenFeature = { formFieldsForGenerateTokenForm: NormalisedFormField[]; formFieldsForPasswordResetForm: NormalisedFormField[]; }; -export declare type User = { - id: string; - recipeUserId: string; - email: string; - timeJoined: number; -}; export declare type TypeInput = { signUpFeature?: TypeInputSignUp; emailDelivery?: EmailDeliveryTypeInput; - resetPasswordUsingTokenFeature?: TypeInputResetPasswordUsingTokenFeature; override?: { functions?: ( originalImplementation: RecipeInterface, @@ -89,6 +76,19 @@ export declare type RecipeInterface = { status: "EMAIL_ALREADY_EXISTS_ERROR"; } >; + createNewRecipeUser(input: { + email: string; + password: string; + userContext: any; + }): Promise< + | { + status: "OK"; + user: User; + } + | { + status: "EMAIL_ALREADY_EXISTS_ERROR"; + } + >; signIn(input: { email: string; password: string; @@ -102,18 +102,10 @@ export declare type RecipeInterface = { status: "WRONG_CREDENTIALS_ERROR"; } >; - getUserById(input: { userId: string; userContext: any }): Promise; - getUserByEmail(input: { email: string; userContext: any }): Promise; /** - * We do not make email optional here cause we want to - * allow passing in primaryUserId. If we make email optional, - * and if the user provides a primaryUserId, then it may result in two problems: - * - there is no recipeUserId = input primaryUserId, in this case, - * this function will throw an error - * - There is a recipe userId = input primaryUserId, but that recipe has no email, - * or has wrong email compared to what the user wanted to generate a reset token for. - * - * And we want to allow primaryUserId being passed in. + * We pass in the email as well to this function cause the input userId + * may not be associated with an emailpassword account. In this case, we + * need to know which email to use to create an emailpassword account later on. */ createResetPasswordToken(input: { userId: string; @@ -128,9 +120,8 @@ export declare type RecipeInterface = { status: "UNKNOWN_USER_ID_ERROR"; } >; - resetPasswordUsingToken(input: { + consumePasswordResetToken(input: { token: string; - newPassword: string; userContext: any; }): Promise< | { @@ -147,9 +138,15 @@ export declare type RecipeInterface = { email?: string; password?: string; userContext: any; - }): Promise<{ - status: "OK" | "UNKNOWN_USER_ID_ERROR" | "EMAIL_ALREADY_EXISTS_ERROR"; - }>; + }): Promise< + | { + status: "OK" | "UNKNOWN_USER_ID_ERROR" | "EMAIL_ALREADY_EXISTS_ERROR"; + } + | { + status: "EMAIL_CHANGE_NOT_ALLOWED_ERROR"; + reason: string; + } + >; }; export declare type APIOptions = { recipeImplementation: RecipeInterface; @@ -208,7 +205,7 @@ export declare type APIInterface = { | { status: "OK"; email: string; - userId: string; + user: User; } | { status: "RESET_PASSWORD_INVALID_TOKEN_ERROR"; @@ -248,7 +245,6 @@ export declare type APIInterface = { | { status: "OK"; user: User; - createdNewUser: boolean; session: SessionContainerInterface; } | { @@ -273,29 +269,14 @@ export declare type APIInterface = { }) => Promise< | { status: "OK"; - user: User; - createdNewRecipeUser: boolean; - session: SessionContainerInterface; wereAccountsAlreadyLinked: boolean; } | { - status: "RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"; - primaryUserId: string; + status: "NEW_ACCOUNT_NEEDS_TO_BE_VERIFIED_ERROR" | "ACCOUNT_LINKING_NOT_ALLOWED_ERROR"; description: string; } | { - status: "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"; - primaryUserId: string; - description: string; - } - | { - status: "ACCOUNT_LINKING_NOT_ALLOWED_ERROR"; - description: string; - } - | { - status: "ACCOUNT_NOT_VERIFIED_ERROR"; - isNotVerifiedAccountFromInputSession: boolean; - description: string; + status: "WRONG_CREDENTIALS_ERROR"; } | GeneralErrorResponse >); @@ -304,7 +285,6 @@ export declare type TypeEmailPasswordPasswordResetEmailDeliveryInput = { type: "PASSWORD_RESET"; user: { id: string; - recipeUserId: string; email: string; }; passwordResetLink: string; diff --git a/lib/build/recipe/emailpassword/utils.js b/lib/build/recipe/emailpassword/utils.js index e50c8f7bb..374770e87 100644 --- a/lib/build/recipe/emailpassword/utils.js +++ b/lib/build/recipe/emailpassword/utils.js @@ -75,19 +75,11 @@ function validateAndNormaliseUserInput(recipeInstance, appInfo, config) { ? void 0 : _a.service; /** - * following code is for backward compatibility. - * if user has not passed emailDelivery config, we - * use the createAndSendCustomEmail config. If the user - * has not passed even that config, we use the default + * If the user has not passed even that config, we use the default * createAndSendCustomEmail implementation which calls our supertokens API */ if (emailService === undefined) { - emailService = new backwardCompatibility_1.default( - recipeImpl, - appInfo, - isInServerlessEnv, - config === null || config === void 0 ? void 0 : config.resetPasswordUsingTokenFeature - ); + emailService = new backwardCompatibility_1.default(recipeImpl, appInfo, isInServerlessEnv); } return Object.assign(Object.assign({}, config === null || config === void 0 ? void 0 : config.emailDelivery), { /** diff --git a/lib/build/recipe/thirdpartyemailpassword/emaildelivery/services/backwardCompatibility/index.d.ts b/lib/build/recipe/thirdpartyemailpassword/emaildelivery/services/backwardCompatibility/index.d.ts index be35a1981..e3b6e87a8 100644 --- a/lib/build/recipe/thirdpartyemailpassword/emaildelivery/services/backwardCompatibility/index.d.ts +++ b/lib/build/recipe/thirdpartyemailpassword/emaildelivery/services/backwardCompatibility/index.d.ts @@ -1,5 +1,5 @@ // @ts-nocheck -import { TypeThirdPartyEmailPasswordEmailDeliveryInput, User } from "../../../types"; +import { TypeThirdPartyEmailPasswordEmailDeliveryInput } from "../../../types"; import { RecipeInterface as EmailPasswordRecipeInterface } from "../../../../emailpassword"; import { NormalisedAppinfo } from "../../../../../types"; import { EmailDeliveryInterface } from "../../../../../ingredients/emaildelivery/types"; @@ -9,14 +9,7 @@ export default class BackwardCompatibilityService constructor( emailPasswordRecipeInterfaceImpl: EmailPasswordRecipeInterface, appInfo: NormalisedAppinfo, - isInServerlessEnv: boolean, - resetPasswordUsingTokenFeature?: { - createAndSendCustomEmail?: ( - user: User, - passwordResetURLWithToken: string, - userContext: any - ) => Promise; - } + isInServerlessEnv: boolean ); sendEmail: ( input: TypeThirdPartyEmailPasswordEmailDeliveryInput & { diff --git a/lib/build/recipe/thirdpartyemailpassword/emaildelivery/services/backwardCompatibility/index.js b/lib/build/recipe/thirdpartyemailpassword/emaildelivery/services/backwardCompatibility/index.js index c4057c084..ee09e5c75 100644 --- a/lib/build/recipe/thirdpartyemailpassword/emaildelivery/services/backwardCompatibility/index.js +++ b/lib/build/recipe/thirdpartyemailpassword/emaildelivery/services/backwardCompatibility/index.js @@ -40,7 +40,7 @@ const backwardCompatibility_1 = __importDefault( require("../../../../emailpassword/emaildelivery/services/backwardCompatibility") ); class BackwardCompatibilityService { - constructor(emailPasswordRecipeInterfaceImpl, appInfo, isInServerlessEnv, resetPasswordUsingTokenFeature) { + constructor(emailPasswordRecipeInterfaceImpl, appInfo, isInServerlessEnv) { this.sendEmail = (input) => __awaiter(this, void 0, void 0, function* () { yield this.emailPasswordBackwardCompatibilityService.sendEmail(input); @@ -49,8 +49,7 @@ class BackwardCompatibilityService { this.emailPasswordBackwardCompatibilityService = new backwardCompatibility_1.default( emailPasswordRecipeInterfaceImpl, appInfo, - isInServerlessEnv, - resetPasswordUsingTokenFeature + isInServerlessEnv ); } } diff --git a/lib/build/recipe/thirdpartyemailpassword/index.d.ts b/lib/build/recipe/thirdpartyemailpassword/index.d.ts index d70cfdbe3..c9c985e29 100644 --- a/lib/build/recipe/thirdpartyemailpassword/index.d.ts +++ b/lib/build/recipe/thirdpartyemailpassword/index.d.ts @@ -29,7 +29,7 @@ export default class Wrapper { ): Promise< | { status: "OK"; - user: User; + user: import("../emailpassword").User; } | { status: "EMAIL_ALREADY_EXISTS_ERROR"; @@ -42,14 +42,12 @@ export default class Wrapper { ): Promise< | { status: "OK"; - user: User; + user: import("../emailpassword").User; } | { status: "WRONG_CREDENTIALS_ERROR"; } >; - static getUserById(userId: string, userContext?: any): Promise; - static getUsersByEmail(email: string, userContext?: any): Promise; static createResetPasswordToken( userId: string, email: string, @@ -63,9 +61,8 @@ export default class Wrapper { status: "UNKNOWN_USER_ID_ERROR"; } >; - static resetPasswordUsingToken( + static consumePasswordResetToken( token: string, - newPassword: string, userContext?: any ): Promise< | { @@ -82,9 +79,15 @@ export default class Wrapper { email?: string; password?: string; userContext?: any; - }): Promise<{ - status: "OK" | "UNKNOWN_USER_ID_ERROR" | "EMAIL_ALREADY_EXISTS_ERROR"; - }>; + }): Promise< + | { + status: "OK" | "UNKNOWN_USER_ID_ERROR" | "EMAIL_ALREADY_EXISTS_ERROR"; + } + | { + status: "EMAIL_CHANGE_NOT_ALLOWED_ERROR"; + reason: string; + } + >; static Google: typeof import("../thirdparty/providers/google").default; static Github: typeof import("../thirdparty/providers/github").default; static Facebook: typeof import("../thirdparty/providers/facebook").default; @@ -102,11 +105,9 @@ export declare let Error: typeof SuperTokensError; export declare let emailPasswordSignUp: typeof Wrapper.emailPasswordSignUp; export declare let emailPasswordSignIn: typeof Wrapper.emailPasswordSignIn; export declare let thirdPartySignInUp: typeof Wrapper.thirdPartySignInUp; -export declare let getUserById: typeof Wrapper.getUserById; export declare let getUserByThirdPartyInfo: typeof Wrapper.getUserByThirdPartyInfo; -export declare let getUsersByEmail: typeof Wrapper.getUsersByEmail; export declare let createResetPasswordToken: typeof Wrapper.createResetPasswordToken; -export declare let resetPasswordUsingToken: typeof Wrapper.resetPasswordUsingToken; +export declare let consumePasswordResetToken: typeof Wrapper.consumePasswordResetToken; export declare let updateEmailOrPassword: typeof Wrapper.updateEmailOrPassword; export declare let Google: typeof import("../thirdparty/providers/google").default; export declare let Github: typeof import("../thirdparty/providers/github").default; diff --git a/lib/build/recipe/thirdpartyemailpassword/index.js b/lib/build/recipe/thirdpartyemailpassword/index.js index f02484528..14d2fbb09 100644 --- a/lib/build/recipe/thirdpartyemailpassword/index.js +++ b/lib/build/recipe/thirdpartyemailpassword/index.js @@ -86,7 +86,7 @@ var __importDefault = return mod && mod.__esModule ? mod : { default: mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); -exports.sendEmail = exports.GoogleWorkspaces = exports.Discord = exports.Apple = exports.Facebook = exports.Github = exports.Google = exports.updateEmailOrPassword = exports.resetPasswordUsingToken = exports.createResetPasswordToken = exports.getUsersByEmail = exports.getUserByThirdPartyInfo = exports.getUserById = exports.thirdPartySignInUp = exports.emailPasswordSignIn = exports.emailPasswordSignUp = exports.Error = exports.init = void 0; +exports.sendEmail = exports.GoogleWorkspaces = exports.Discord = exports.Apple = exports.Facebook = exports.Github = exports.Google = exports.updateEmailOrPassword = exports.consumePasswordResetToken = exports.createResetPasswordToken = exports.getUserByThirdPartyInfo = exports.thirdPartySignInUp = exports.emailPasswordSignIn = exports.emailPasswordSignUp = exports.Error = exports.init = void 0; const recipe_1 = __importDefault(require("./recipe")); const error_1 = __importDefault(require("./error")); const thirdPartyProviders = __importStar(require("../thirdparty/providers")); @@ -120,12 +120,6 @@ class Wrapper { userContext, }); } - static getUserById(userId, userContext = {}) { - return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.getUserById({ userId, userContext }); - } - static getUsersByEmail(email, userContext = {}) { - return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.getUsersByEmail({ email, userContext }); - } static createResetPasswordToken(userId, email, userContext = {}) { return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.createResetPasswordToken({ userId, @@ -133,10 +127,9 @@ class Wrapper { userContext, }); } - static resetPasswordUsingToken(token, newPassword, userContext = {}) { - return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.resetPasswordUsingToken({ + static consumePasswordResetToken(token, userContext = {}) { + return recipe_1.default.getInstanceOrThrowError().recipeInterfaceImpl.consumePasswordResetToken({ token, - newPassword, userContext, }); } @@ -169,11 +162,9 @@ exports.Error = Wrapper.Error; exports.emailPasswordSignUp = Wrapper.emailPasswordSignUp; exports.emailPasswordSignIn = Wrapper.emailPasswordSignIn; exports.thirdPartySignInUp = Wrapper.thirdPartySignInUp; -exports.getUserById = Wrapper.getUserById; exports.getUserByThirdPartyInfo = Wrapper.getUserByThirdPartyInfo; -exports.getUsersByEmail = Wrapper.getUsersByEmail; exports.createResetPasswordToken = Wrapper.createResetPasswordToken; -exports.resetPasswordUsingToken = Wrapper.resetPasswordUsingToken; +exports.consumePasswordResetToken = Wrapper.consumePasswordResetToken; exports.updateEmailOrPassword = Wrapper.updateEmailOrPassword; exports.Google = Wrapper.Google; exports.Github = Wrapper.Github; diff --git a/lib/build/recipe/thirdpartyemailpassword/recipe.js b/lib/build/recipe/thirdpartyemailpassword/recipe.js index dceb0bd55..37614c57f 100644 --- a/lib/build/recipe/thirdpartyemailpassword/recipe.js +++ b/lib/build/recipe/thirdpartyemailpassword/recipe.js @@ -167,7 +167,6 @@ class Recipe extends recipeModule_1.default { signUpFeature: { formFields: this.config.signUpFeature.formFields, }, - resetPasswordUsingTokenFeature: this.config.resetPasswordUsingTokenFeature, }, { emailDelivery: this.emailDelivery, diff --git a/lib/build/recipe/thirdpartyemailpassword/recipeImplementation/emailPasswordRecipeImplementation.js b/lib/build/recipe/thirdpartyemailpassword/recipeImplementation/emailPasswordRecipeImplementation.js index 8d8772bdd..cbbd113c3 100644 --- a/lib/build/recipe/thirdpartyemailpassword/recipeImplementation/emailPasswordRecipeImplementation.js +++ b/lib/build/recipe/thirdpartyemailpassword/recipeImplementation/emailPasswordRecipeImplementation.js @@ -43,35 +43,19 @@ function getRecipeInterface(recipeInterface) { return recipeInterface.emailPasswordSignIn(input); }); }, - getUserById: function (input) { - return __awaiter(this, void 0, void 0, function* () { - let user = yield recipeInterface.getUserById(input); - if (user === undefined || user.thirdParty !== undefined) { - // either user is undefined or it's a thirdparty user. - return undefined; - } - return user; - }); - }, - getUserByEmail: function (input) { + createResetPasswordToken: function (input) { return __awaiter(this, void 0, void 0, function* () { - let result = yield recipeInterface.getUsersByEmail(input); - for (let i = 0; i < result.length; i++) { - if (result[i].thirdParty === undefined) { - return result[i]; - } - } - return undefined; + return recipeInterface.createResetPasswordToken(input); }); }, - createResetPasswordToken: function (input) { + consumePasswordResetToken: function (input) { return __awaiter(this, void 0, void 0, function* () { - return recipeInterface.createResetPasswordToken(input); + return recipeInterface.consumePasswordResetToken(input); }); }, - resetPasswordUsingToken: function (input) { + createNewRecipeUser: function (input) { return __awaiter(this, void 0, void 0, function* () { - return recipeInterface.resetPasswordUsingToken(input); + return recipeInterface.createNewEmailPasswordRecipeUser(input); }); }, updateEmailOrPassword: function (input) { diff --git a/lib/build/recipe/thirdpartyemailpassword/recipeImplementation/index.js b/lib/build/recipe/thirdpartyemailpassword/recipeImplementation/index.js index c66e7bb62..23f596161 100644 --- a/lib/build/recipe/thirdpartyemailpassword/recipeImplementation/index.js +++ b/lib/build/recipe/thirdpartyemailpassword/recipeImplementation/index.js @@ -40,6 +40,7 @@ const recipeImplementation_1 = __importDefault(require("../../emailpassword/reci const recipeImplementation_2 = __importDefault(require("../../thirdparty/recipeImplementation")); const emailPasswordRecipeImplementation_1 = __importDefault(require("./emailPasswordRecipeImplementation")); const thirdPartyRecipeImplementation_1 = __importDefault(require("./thirdPartyRecipeImplementation")); +const __1 = require("../../../"); function getRecipeInterface(emailPasswordQuerier, thirdPartyQuerier) { let originalEmailPasswordImplementation = recipeImplementation_1.default(emailPasswordQuerier); let originalThirdPartyImplementation; @@ -71,71 +72,50 @@ function getRecipeInterface(emailPasswordQuerier, thirdPartyQuerier) { ); }); }, - getUserById: function (input) { + getUserByThirdPartyInfo: function (input) { return __awaiter(this, void 0, void 0, function* () { - let user = yield originalEmailPasswordImplementation.getUserById.bind( - emailPasswordRecipeImplementation_1.default(this) - )(input); - if (user !== undefined) { - return user; - } if (originalThirdPartyImplementation === undefined) { return undefined; } - return yield originalThirdPartyImplementation.getUserById.bind( + return originalThirdPartyImplementation.getUserByThirdPartyInfo.bind( thirdPartyRecipeImplementation_1.default(this) )(input); }); }, - getUsersByEmail: function ({ email, userContext }) { + createResetPasswordToken: function (input) { return __awaiter(this, void 0, void 0, function* () { - let userFromEmailPass = yield originalEmailPasswordImplementation.getUserByEmail.bind( + return originalEmailPasswordImplementation.createResetPasswordToken.bind( emailPasswordRecipeImplementation_1.default(this) - )({ email, userContext }); - if (originalThirdPartyImplementation === undefined) { - return userFromEmailPass === undefined ? [] : [userFromEmailPass]; - } - let usersFromThirdParty = yield originalThirdPartyImplementation.getUsersByEmail.bind( - thirdPartyRecipeImplementation_1.default(this) - )({ email, userContext }); - if (userFromEmailPass !== undefined) { - return [...usersFromThirdParty, userFromEmailPass]; - } - return usersFromThirdParty; - }); - }, - getUserByThirdPartyInfo: function (input) { - return __awaiter(this, void 0, void 0, function* () { - if (originalThirdPartyImplementation === undefined) { - return undefined; - } - return originalThirdPartyImplementation.getUserByThirdPartyInfo.bind( - thirdPartyRecipeImplementation_1.default(this) )(input); }); }, - createResetPasswordToken: function (input) { + consumePasswordResetToken: function (input) { return __awaiter(this, void 0, void 0, function* () { - return originalEmailPasswordImplementation.createResetPasswordToken.bind( + return originalEmailPasswordImplementation.consumePasswordResetToken.bind( emailPasswordRecipeImplementation_1.default(this) )(input); }); }, - resetPasswordUsingToken: function (input) { + createNewEmailPasswordRecipeUser: function (input) { return __awaiter(this, void 0, void 0, function* () { - return originalEmailPasswordImplementation.resetPasswordUsingToken.bind( + return originalEmailPasswordImplementation.createNewRecipeUser.bind( emailPasswordRecipeImplementation_1.default(this) )(input); }); }, updateEmailOrPassword: function (input) { return __awaiter(this, void 0, void 0, function* () { - let user = yield this.getUserById({ userId: input.userId, userContext: input.userContext }); + let user = yield __1.getUser(input.userId, input.userContext); if (user === undefined) { return { status: "UNKNOWN_USER_ID_ERROR", }; - } else if (user.thirdParty !== undefined) { + } + let emailPasswordUserExists = + user.loginMethods.find((lM) => { + lM.recipeId === "emailpassword"; + }) !== undefined; + if (!emailPasswordUserExists) { throw new Error("Cannot update email or password of a user who signed up using third party login."); } return originalEmailPasswordImplementation.updateEmailOrPassword.bind( diff --git a/lib/build/recipe/thirdpartyemailpassword/recipeImplementation/thirdPartyRecipeImplementation.js b/lib/build/recipe/thirdpartyemailpassword/recipeImplementation/thirdPartyRecipeImplementation.js index 1b2784d4e..d3c4c248b 100644 --- a/lib/build/recipe/thirdpartyemailpassword/recipeImplementation/thirdPartyRecipeImplementation.js +++ b/lib/build/recipe/thirdpartyemailpassword/recipeImplementation/thirdPartyRecipeImplementation.js @@ -67,29 +67,14 @@ function getRecipeInterface(recipeInterface) { }; }); }, - getUserById: function (input) { + getUserById: function (_) { return __awaiter(this, void 0, void 0, function* () { - let user = yield recipeInterface.getUserById(input); - if (user === undefined || user.thirdParty === undefined) { - // either user is undefined or it's an email password user. - return undefined; - } - return { - email: user.email, - id: user.id, - recipeUserId: user.recipeUserId, - timeJoined: user.timeJoined, - thirdParty: user.thirdParty, - }; + throw new Error("This will be removed.."); }); }, - getUsersByEmail: function (input) { + getUsersByEmail: function (_) { return __awaiter(this, void 0, void 0, function* () { - let users = yield recipeInterface.getUsersByEmail(input); - // we filter out all non thirdparty users. - return users.filter((u) => { - return u.thirdParty !== undefined; - }); + throw new Error("This will be removed.."); }); }, }; diff --git a/lib/build/recipe/thirdpartyemailpassword/types.d.ts b/lib/build/recipe/thirdpartyemailpassword/types.d.ts index 398bf95e8..a65d20ba8 100644 --- a/lib/build/recipe/thirdpartyemailpassword/types.d.ts +++ b/lib/build/recipe/thirdpartyemailpassword/types.d.ts @@ -2,9 +2,7 @@ import { TypeProvider, APIOptions as ThirdPartyAPIOptionsOriginal } from "../thirdparty/types"; import { NormalisedFormField, - TypeFormField, TypeInputFormField, - TypeInputResetPasswordUsingTokenFeature, APIOptions as EmailPasswordAPIOptionsOriginal, TypeEmailPasswordEmailDeliveryInput, RecipeInterface as EPRecipeInterface, @@ -15,7 +13,7 @@ import { TypeInput as EmailDeliveryTypeInput, TypeInputWithService as EmailDeliveryTypeInputWithService, } from "../../ingredients/emaildelivery/types"; -import { GeneralErrorResponse } from "../../types"; +import { GeneralErrorResponse, User as GlobalUser } from "../../types"; export declare type User = { id: string; recipeUserId: string; @@ -26,17 +24,6 @@ export declare type User = { userId: string; }; }; -export declare type TypeContextEmailPasswordSignUp = { - loginType: "emailpassword"; - formFields: TypeFormField[]; -}; -export declare type TypeContextEmailPasswordSignIn = { - loginType: "emailpassword"; -}; -export declare type TypeContextThirdParty = { - loginType: "thirdparty"; - thirdPartyAuthCodeResponse: any; -}; export declare type TypeInputSignUp = { formFields?: TypeInputFormField[]; }; @@ -47,7 +34,6 @@ export declare type TypeInput = { signUpFeature?: TypeInputSignUp; providers?: TypeProvider[]; emailDelivery?: EmailDeliveryTypeInput; - resetPasswordUsingTokenFeature?: TypeInputResetPasswordUsingTokenFeature; override?: { functions?: ( originalImplementation: RecipeInterface, @@ -63,7 +49,6 @@ export declare type TypeNormalisedInput = { emailPasswordRecipeImpl: EPRecipeInterface, isInServerlessEnv: boolean ) => EmailDeliveryTypeInputWithService; - resetPasswordUsingTokenFeature?: TypeInputResetPasswordUsingTokenFeature; override: { functions: ( originalImplementation: RecipeInterface, @@ -73,8 +58,6 @@ export declare type TypeNormalisedInput = { }; }; export declare type RecipeInterface = { - getUserById(input: { userId: string; userContext: any }): Promise; - getUsersByEmail(input: { email: string; userContext: any }): Promise; getUserByThirdPartyInfo(input: { thirdPartyId: string; thirdPartyUserId: string; @@ -97,7 +80,20 @@ export declare type RecipeInterface = { }): Promise< | { status: "OK"; - user: User; + user: GlobalUser; + } + | { + status: "EMAIL_ALREADY_EXISTS_ERROR"; + } + >; + createNewEmailPasswordRecipeUser(input: { + email: string; + password: string; + userContext: any; + }): Promise< + | { + status: "OK"; + user: GlobalUser; } | { status: "EMAIL_ALREADY_EXISTS_ERROR"; @@ -110,7 +106,7 @@ export declare type RecipeInterface = { }): Promise< | { status: "OK"; - user: User; + user: GlobalUser; } | { status: "WRONG_CREDENTIALS_ERROR"; @@ -129,9 +125,8 @@ export declare type RecipeInterface = { status: "UNKNOWN_USER_ID_ERROR"; } >; - resetPasswordUsingToken(input: { + consumePasswordResetToken(input: { token: string; - newPassword: string; userContext: any; }): Promise< | { @@ -148,9 +143,15 @@ export declare type RecipeInterface = { email?: string; password?: string; userContext: any; - }): Promise<{ - status: "OK" | "UNKNOWN_USER_ID_ERROR" | "EMAIL_ALREADY_EXISTS_ERROR"; - }>; + }): Promise< + | { + status: "OK" | "UNKNOWN_USER_ID_ERROR" | "EMAIL_ALREADY_EXISTS_ERROR"; + } + | { + status: "EMAIL_CHANGE_NOT_ALLOWED_ERROR"; + reason: string; + } + >; }; export declare type EmailPasswordAPIOptions = EmailPasswordAPIOptionsOriginal; export declare type ThirdPartyAPIOptions = ThirdPartyAPIOptionsOriginal; @@ -255,7 +256,7 @@ export declare type APIInterface = { | { status: "OK"; email: string; - userId: string; + user: GlobalUser; } | { status: "RESET_PASSWORD_INVALID_TOKEN_ERROR"; @@ -308,29 +309,14 @@ export declare type APIInterface = { }) => Promise< | { status: "OK"; - user: User; - createdNewRecipeUser: boolean; - session: SessionContainerInterface; wereAccountsAlreadyLinked: boolean; } | { - status: "RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"; - primaryUserId: string; + status: "NEW_ACCOUNT_NEEDS_TO_BE_VERIFIED_ERROR" | "ACCOUNT_LINKING_NOT_ALLOWED_ERROR"; description: string; } | { - status: "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"; - primaryUserId: string; - description: string; - } - | { - status: "ACCOUNT_LINKING_NOT_ALLOWED_ERROR"; - description: string; - } - | { - status: "ACCOUNT_NOT_VERIFIED_ERROR"; - isNotVerifiedAccountFromInputSession: boolean; - description: string; + status: "WRONG_CREDENTIALS_ERROR"; } | GeneralErrorResponse >); @@ -346,7 +332,7 @@ export declare type APIInterface = { }) => Promise< | { status: "OK"; - user: User; + user: GlobalUser; session: SessionContainerInterface; } | { @@ -366,8 +352,7 @@ export declare type APIInterface = { }) => Promise< | { status: "OK"; - user: User; - createdNewUser: boolean; + user: GlobalUser; session: SessionContainerInterface; } | { diff --git a/lib/build/recipe/thirdpartyemailpassword/utils.js b/lib/build/recipe/thirdpartyemailpassword/utils.js index daa8a505c..9b36d7936 100644 --- a/lib/build/recipe/thirdpartyemailpassword/utils.js +++ b/lib/build/recipe/thirdpartyemailpassword/utils.js @@ -28,7 +28,6 @@ function validateAndNormaliseUserInput(recipeInstance, appInfo, config) { appInfo, config === undefined ? undefined : config.signUpFeature ); - let resetPasswordUsingTokenFeature = config === undefined ? undefined : config.resetPasswordUsingTokenFeature; let providers = config === undefined || config.providers === undefined ? [] : config.providers; let override = Object.assign( { @@ -51,12 +50,7 @@ function validateAndNormaliseUserInput(recipeInstance, appInfo, config) { * createAndSendCustomEmail implementation */ if (emailService === undefined) { - emailService = new backwardCompatibility_1.default( - emailPasswordRecipeImpl, - appInfo, - isInServerlessEnv, - config === null || config === void 0 ? void 0 : config.resetPasswordUsingTokenFeature - ); + emailService = new backwardCompatibility_1.default(emailPasswordRecipeImpl, appInfo, isInServerlessEnv); } return Object.assign(Object.assign({}, config === null || config === void 0 ? void 0 : config.emailDelivery), { /** @@ -78,7 +72,6 @@ function validateAndNormaliseUserInput(recipeInstance, appInfo, config) { getEmailDeliveryConfig, signUpFeature, providers, - resetPasswordUsingTokenFeature, }; } exports.validateAndNormaliseUserInput = validateAndNormaliseUserInput; diff --git a/lib/build/supertokens.d.ts b/lib/build/supertokens.d.ts index 6035bda5a..8bdf79f64 100644 --- a/lib/build/supertokens.d.ts +++ b/lib/build/supertokens.d.ts @@ -4,7 +4,6 @@ import RecipeModule from "./recipeModule"; import NormalisedURLPath from "./normalisedURLPath"; import { BaseRequest, BaseResponse } from "./framework"; import { TypeFramework } from "./framework/types"; -import { RecipeLevelUser } from "./recipe/accountlinking/types"; export default class SuperTokens { private static instance; framework: TypeFramework; @@ -73,17 +72,4 @@ export default class SuperTokens { }>; middleware: (request: BaseRequest, response: BaseResponse) => Promise; errorHandler: (err: any, request: BaseRequest, response: BaseResponse) => Promise; - _getUserForRecipeId: ( - userId: string, - recipeId: string - ) => Promise<{ - user: RecipeLevelUser | undefined; - recipe: - | "emailpassword" - | "thirdparty" - | "passwordless" - | "thirdpartyemailpassword" - | "thirdpartypasswordless" - | undefined; - }>; } diff --git a/lib/build/supertokens.js b/lib/build/supertokens.js index 6e0f0fa31..8b7d1c904 100644 --- a/lib/build/supertokens.js +++ b/lib/build/supertokens.js @@ -60,14 +60,6 @@ const error_1 = __importDefault(require("./error")); const logger_1 = require("./logger"); const postSuperTokensInitCallbacks_1 = require("./postSuperTokensInitCallbacks"); const recipe_1 = __importDefault(require("./recipe/accountlinking/recipe")); -const recipe_2 = __importDefault(require("./recipe/emailpassword/recipe")); -const recipe_3 = __importDefault(require("./recipe/thirdparty/recipe")); -const recipe_4 = __importDefault(require("./recipe/passwordless/recipe")); -const emailpassword_1 = __importDefault(require("./recipe/emailpassword")); -const thirdparty_1 = __importDefault(require("./recipe/thirdparty")); -const passwordless_1 = __importDefault(require("./recipe/passwordless")); -const thirdpartyemailpassword_1 = __importDefault(require("./recipe/thirdpartyemailpassword")); -const thirdpartypasswordless_1 = __importDefault(require("./recipe/thirdpartypasswordless")); class SuperTokens { constructor(config) { var _a, _b; @@ -308,93 +300,6 @@ class SuperTokens { } throw err; }); - // this is an internal use function, therefore it is prefixed with an `_` - this._getUserForRecipeId = (userId, recipeId) => - __awaiter(this, void 0, void 0, function* () { - let user; - let recipe; - if (recipeId === recipe_2.default.RECIPE_ID) { - try { - const userResponse = yield emailpassword_1.default.getUserById(userId); - if (userResponse !== undefined) { - user = Object.assign(Object.assign({}, userResponse), { recipeId: "emailpassword" }); - recipe = "emailpassword"; - } - } catch (e) { - // No - op - } - if (user === undefined) { - try { - const userResponse = yield thirdpartyemailpassword_1.default.getUserById(userId); - if (userResponse !== undefined) { - user = Object.assign(Object.assign({}, userResponse), { recipeId: "emailpassword" }); - recipe = "thirdpartyemailpassword"; - } - } catch (e) { - // No - op - } - } - } else if (recipeId === recipe_3.default.RECIPE_ID) { - try { - const userResponse = yield thirdparty_1.default.getUserById(userId); - if (userResponse !== undefined) { - user = Object.assign(Object.assign({}, userResponse), { recipeId: "thirdparty" }); - recipe = "thirdparty"; - } - } catch (e) { - // No - op - } - if (user === undefined) { - try { - const userResponse = yield thirdpartyemailpassword_1.default.getUserById(userId); - if (userResponse !== undefined) { - user = Object.assign(Object.assign({}, userResponse), { recipeId: "thirdparty" }); - recipe = "thirdpartyemailpassword"; - } - } catch (e) { - // No - op - } - } - if (user === undefined) { - try { - const userResponse = yield thirdpartypasswordless_1.default.getUserById(userId); - if (userResponse !== undefined) { - user = Object.assign(Object.assign({}, userResponse), { recipeId: "thirdparty" }); - recipe = "thirdpartypasswordless"; - } - } catch (e) { - // No - op - } - } - } else if (recipeId === recipe_4.default.RECIPE_ID) { - try { - const userResponse = yield passwordless_1.default.getUserById({ - userId, - }); - if (userResponse !== undefined) { - user = Object.assign(Object.assign({}, userResponse), { recipeId: "passwordless" }); - recipe = "passwordless"; - } - } catch (e) { - // No - op - } - if (user === undefined) { - try { - const userResponse = yield thirdpartypasswordless_1.default.getUserById(userId); - if (userResponse !== undefined) { - user = Object.assign(Object.assign({}, userResponse), { recipeId: "passwordless" }); - recipe = "thirdpartypasswordless"; - } - } catch (e) { - // No - op - } - } - } - return { - user, - recipe, - }; - }); logger_1.logDebugMessage("Started SuperTokens with debug logging (supertokens.init called)"); logger_1.logDebugMessage("appInfo: " + JSON.stringify(config.appInfo)); this.framework = config.framework !== undefined ? config.framework : "express"; diff --git a/lib/ts/recipe/accountlinking/accountLinkingClaim.ts b/lib/ts/recipe/accountlinking/accountLinkingClaim.ts new file mode 100644 index 000000000..005f3b35f --- /dev/null +++ b/lib/ts/recipe/accountlinking/accountLinkingClaim.ts @@ -0,0 +1,17 @@ +import { PrimitiveClaim } from "../session/claims"; + +/** + * We include "Class" in the class name, because it makes it easier to import the right thing (the instance) instead of this. + * */ +export class AccountLinkingClaimClass extends PrimitiveClaim { + constructor() { + super({ + key: "st-linking", + fetchValue(_, __, ___) { + return undefined; + }, + }); + } +} + +export const AccountLinkingClaim = new AccountLinkingClaimClass(); diff --git a/lib/ts/recipe/accountlinking/recipe.ts b/lib/ts/recipe/accountlinking/recipe.ts index cf5dddddc..7aa5d1175 100644 --- a/lib/ts/recipe/accountlinking/recipe.ts +++ b/lib/ts/recipe/accountlinking/recipe.ts @@ -380,24 +380,42 @@ export default class Recipe extends RecipeModule { return false; }; - linkAccountsWithUserFromSession = async ({ + linkAccountsWithUserFromSession = async ({ session, newUser, createRecipeUserFunc, + verifyCredentialsFunc, userContext, }: { session: SessionContainer; newUser: AccountInfoWithRecipeId; - createRecipeUserFunc: (newUser: AccountInfoWithRecipeId) => Promise; + createRecipeUserFunc: () => Promise; + verifyCredentialsFunc: () => Promise< + | { status: "OK" } + | { + status: "CUSTOM_RESPONSE"; + resp: T; + } + >; userContext: any; }): Promise< | { - status: "OK" | "NEW_ACCOUNT_NEEDS_TO_BE_VERIFIED_ERROR"; + status: "OK"; + wereAccountsAlreadyLinked: boolean; } | { status: "ACCOUNT_LINKING_NOT_ALLOWED_ERROR"; description: string; } + | { + status: "NEW_ACCOUNT_NEEDS_TO_BE_VERIFIED_ERROR"; + primaryUserId: string; + recipeUserId: string; + } + | { + status: "CUSTOM_RESPONSE"; + resp: T; + } > => { // In order to link the newUser to the session user, // we need to first make sure that the session user @@ -497,6 +515,7 @@ export default class Recipe extends RecipeModule { session, newUser, createRecipeUserFunc, + verifyCredentialsFunc, userContext, }); } else if ( @@ -543,29 +562,24 @@ export default class Recipe extends RecipeModule { userContext, }); - let newUserIsVerified = false; - const userObjThatHasSameAccountInfoAndRecipeIdAsNewUser = usersArrayThatHaveSameAccountInfoAsNewUser.find((u) => - u.loginMethods.find((lU) => { - let found = false; - if (lU.recipeId !== newUser.recipeId) { - return false; - } - if (newUser.recipeId === "thirdparty") { - if (lU.thirdParty === undefined) { + const userObjThatHasSameAccountInfoAndRecipeIdAsNewUser = usersArrayThatHaveSameAccountInfoAsNewUser.find( + (u) => + u.loginMethods.find((lU) => { + if (lU.recipeId !== newUser.recipeId) { return false; } - found = - lU.thirdParty.id === newUser.thirdParty!.id && - lU.thirdParty.userId === newUser.thirdParty!.userId; - } else { - found = lU.email === newUser.email || newUser.phoneNumber === newUser.phoneNumber; - } - if (!found) { - return false; - } - newUserIsVerified = lU.verified; - return true; - }) + if (newUser.recipeId === "thirdparty") { + if (lU.thirdParty === undefined) { + return false; + } + return ( + lU.thirdParty.id === newUser.thirdParty!.id && + lU.thirdParty.userId === newUser.thirdParty!.userId + ); + } else { + return lU.email === newUser.email || newUser.phoneNumber === newUser.phoneNumber; + } + }) !== undefined ); if (userObjThatHasSameAccountInfoAndRecipeIdAsNewUser === undefined) { @@ -586,15 +600,43 @@ export default class Recipe extends RecipeModule { } // we create the new recipe user - await createRecipeUserFunc(newUser); + await createRecipeUserFunc(); // now when we recurse, the new recipe user will be found and we can try linking again. return await this.linkAccountsWithUserFromSession({ session, newUser, createRecipeUserFunc, + verifyCredentialsFunc, userContext, }); + } else { + // since the user already exists, we should first verify the credentials + // before continuing to link the accounts. + let verifyResult = await verifyCredentialsFunc(); + if (verifyResult.status === "CUSTOM_RESPONSE") { + return verifyResult; + } + // this means that the verification was fine and we can continue.. + } + + // we check if the userObjThatHasSameAccountInfoAndRecipeIdAsNewUser is + // a primary user or not, and if it is, then it means that our newUser + // is already linked so we can return early. + + if (userObjThatHasSameAccountInfoAndRecipeIdAsNewUser.isPrimaryUser) { + if (userObjThatHasSameAccountInfoAndRecipeIdAsNewUser.id === existingUser.id) { + // this means that the accounts we want to link are already linked. + return { + status: "OK", + wereAccountsAlreadyLinked: true, + }; + } else { + return { + status: "ACCOUNT_LINKING_NOT_ALLOWED_ERROR", + description: "New user is already linked to another account", + }; + } } // now we check about the email verification of the new user. If it's verified, we proceed @@ -604,10 +646,17 @@ export default class Recipe extends RecipeModule { // this means that the existing user does not share anything in common with the new user // in terms of account info. So we check for email verification status.. - if (!newUserIsVerified && shouldDoAccountLinking.shouldRequireVerification) { + if ( + !userObjThatHasSameAccountInfoAndRecipeIdAsNewUser.loginMethods[0].verified && + shouldDoAccountLinking.shouldRequireVerification + ) { // we stop the flow and ask the user to verify this email first. + // the recipe ID is the userObjThatHasSameAccountInfoAndRecipeIdAsNewUser.id + // cause above we checked that userObjThatHasSameAccountInfoAndRecipeIdAsNewUser.isPrimaryUser is false. return { status: "NEW_ACCOUNT_NEEDS_TO_BE_VERIFIED_ERROR", + primaryUserId: existingUser.id, + recipeUserId: userObjThatHasSameAccountInfoAndRecipeIdAsNewUser.id, }; } } @@ -621,6 +670,7 @@ export default class Recipe extends RecipeModule { if (linkAccountResponse.status === "OK") { return { status: "OK", + wereAccountsAlreadyLinked: linkAccountResponse.accountsAlreadyLinked, }; } else if (linkAccountResponse.status === "RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR") { // this means that the the new user is already linked to some other primary user ID, diff --git a/lib/ts/recipe/accountlinking/recipeImplementation.ts b/lib/ts/recipe/accountlinking/recipeImplementation.ts index 6249719a7..6ad5fc7c5 100644 --- a/lib/ts/recipe/accountlinking/recipeImplementation.ts +++ b/lib/ts/recipe/accountlinking/recipeImplementation.ts @@ -36,6 +36,7 @@ export default function getRecipeImplementation(querier: Querier, config: TypeNo }); return result.userIdMapping; }, + getPrimaryUserIdsForRecipeUserIds: async function ( this: RecipeInterface, { @@ -51,6 +52,7 @@ export default function getRecipeImplementation(querier: Querier, config: TypeNo }); return result.userIdMapping; }, + getUsers: async function ( this: RecipeInterface, { @@ -83,6 +85,7 @@ export default function getRecipeImplementation(querier: Querier, config: TypeNo nextPaginationToken: response.nextPaginationToken, }; }, + canCreatePrimaryUserId: async function ( this: RecipeInterface, { @@ -103,14 +106,11 @@ export default function getRecipeImplementation(querier: Querier, config: TypeNo description: string; } > { - let result = await querier.sendGetRequest( - new NormalisedURLPath("/recipe/accountlinking/user/primary/check"), - { - recipeUserId, - } - ); - return result; + return await querier.sendGetRequest(new NormalisedURLPath("/recipe/accountlinking/user/primary/check"), { + recipeUserId, + }); }, + createPrimaryUser: async function ( this: RecipeInterface, { @@ -132,12 +132,11 @@ export default function getRecipeImplementation(querier: Querier, config: TypeNo description: string; } > { - let result = await querier.sendPostRequest(new NormalisedURLPath("/recipe/accountlinking/user/primary"), { + return await querier.sendPostRequest(new NormalisedURLPath("/recipe/accountlinking/user/primary"), { recipeUserId, }); - - return result; }, + canLinkAccounts: async function ( this: RecipeInterface, { @@ -170,6 +169,7 @@ export default function getRecipeImplementation(querier: Querier, config: TypeNo return result; }, + linkAccounts: async function ( this: RecipeInterface, { @@ -223,6 +223,7 @@ export default function getRecipeImplementation(querier: Querier, config: TypeNo return accountsLinkingResult; }, + unlinkAccounts: async function ( this: RecipeInterface, { @@ -242,67 +243,27 @@ export default function getRecipeImplementation(querier: Querier, config: TypeNo description: string; } > { - let recipeUserIdToPrimaryUserIdMapping = await this.getPrimaryUserIdsForRecipeUserIds({ - recipeUserIds: [recipeUserId], - userContext, - }); - let primaryUserId = recipeUserIdToPrimaryUserIdMapping[recipeUserId]; - if (primaryUserId === undefined) { - return { - status: "RECIPE_USER_NOT_FOUND_ERROR", - description: "No user exists with the provided recipeUserId", - }; - } - if (primaryUserId === null) { - return { - status: "PRIMARY_USER_NOT_FOUND_ERROR", - description: - "The input recipeUserId is not linked to any primary user, or is not a primary user itself", - }; - } - - if (primaryUserId === recipeUserId) { - let user = await this.getUser({ - userId: primaryUserId, - userContext, - }); - - if (user === undefined) { - // this can happen cause of some race condition.. - return this.unlinkAccounts({ - recipeUserId, - userContext, - }); - } - if (user.loginMethods.length > 1) { - // we delete the user here cause if we didn't - // do that, then it would result in the primary user ID having the same - // user ID as the recipe user ID, but they are not linked. So this is not allowed. - await this.deleteUser({ - userId: recipeUserId, - removeAllLinkedAccounts: false, - userContext, - }); - - return { - status: "OK", - wasRecipeUserDeleted: true, - }; - } - } let accountsUnlinkingResult = await querier.sendPostRequest( new NormalisedURLPath("/recipe/accountlinking/user/unlink"), { recipeUserId, - primaryUserId, } ); - if (accountsUnlinkingResult.status === "OK") { + if (accountsUnlinkingResult.status === "OK" && !accountsUnlinkingResult.wasRecipeUserDeleted) { + // we have the !accountsUnlinkingResult.wasRecipeUserDeleted check + // cause if the user was deleted, it means that it's user ID was the + // same as the primary user ID, AND that the primary user ID has more + // than one login method - so if we revoke the session in this case, + // it will revoke the session for all login methods as well (since recipeUserId == primaryUserID). + + // The reason we don't do this in the core is that if the user has overriden + // session recipe, it goes through their logic. await Session.revokeAllSessionsForUser(recipeUserId, userContext); } return accountsUnlinkingResult; }, + getUser: async function (this: RecipeInterface, { userId }: { userId: string }): Promise { let result = await querier.sendGetRequest(new NormalisedURLPath("/recipe/accountlinking/user"), { userId, @@ -312,6 +273,7 @@ export default function getRecipeImplementation(querier: Querier, config: TypeNo } return undefined; }, + listUsersByAccountInfo: async function ( this: RecipeInterface, { accountInfo }: { accountInfo: AccountInfo } @@ -321,6 +283,7 @@ export default function getRecipeImplementation(querier: Querier, config: TypeNo }); return result.users; }, + deleteUser: async function ( this: RecipeInterface, { @@ -333,12 +296,12 @@ export default function getRecipeImplementation(querier: Querier, config: TypeNo ): Promise<{ status: "OK"; }> { - let result = await querier.sendPostRequest(new NormalisedURLPath("/user/remove"), { + return await querier.sendPostRequest(new NormalisedURLPath("/user/remove"), { userId, removeAllLinkedAccounts, }); - return result; }, + fetchFromAccountToLinkTable: async function ({ recipeUserId, }: { @@ -349,6 +312,7 @@ export default function getRecipeImplementation(querier: Querier, config: TypeNo }); return result.user; }, + storeIntoAccountToLinkTable: async function ({ recipeUserId, primaryUserId, diff --git a/lib/ts/recipe/dashboard/api/userdetails/userGet.ts b/lib/ts/recipe/dashboard/api/userdetails/userGet.ts index ec17cb14e..372b797f3 100644 --- a/lib/ts/recipe/dashboard/api/userdetails/userGet.ts +++ b/lib/ts/recipe/dashboard/api/userdetails/userGet.ts @@ -1,4 +1,4 @@ -import { APIFunction, APIInterface, APIOptions, RecipeLevelUser } from "../../types"; +import { APIFunction, APIInterface, APIOptions, RecipeLevelUserWithFirstAndLastName } from "../../types"; import STError from "../../../../error"; import { getUserForRecipeId, isRecipeInitialised, isValidRecipeId } from "../../utils"; import UserMetaDataRecipe from "../../../usermetadata/recipe"; @@ -14,7 +14,7 @@ type Response = | { status: "OK"; recipeId: "emailpassword" | "thirdparty" | "passwordless"; - user: RecipeLevelUser; + user: RecipeLevelUserWithFirstAndLastName; }; export const userGet: APIFunction = async (_: APIInterface, options: APIOptions): Promise => { @@ -48,7 +48,7 @@ export const userGet: APIFunction = async (_: APIInterface, options: APIOptions) }; } - let user: RecipeLevelUser | undefined = (await getUserForRecipeId(userId, recipeId)).user; + let user: RecipeLevelUserWithFirstAndLastName | undefined = (await getUserForRecipeId(userId, recipeId)).user; if (user === undefined) { return { diff --git a/lib/ts/recipe/dashboard/api/userdetails/userPasswordPut.ts b/lib/ts/recipe/dashboard/api/userdetails/userPasswordPut.ts index 6c263a26e..4e2f8b11c 100644 --- a/lib/ts/recipe/dashboard/api/userdetails/userPasswordPut.ts +++ b/lib/ts/recipe/dashboard/api/userdetails/userPasswordPut.ts @@ -4,7 +4,6 @@ import EmailPasswordRecipe from "../../../emailpassword/recipe"; import EmailPassword from "../../../emailpassword"; import ThirdPartyEmailPasswordRecipe from "../../../thirdpartyemailpassword/recipe"; import ThirdPartyEmailPassword from "../../../thirdpartyemailpassword"; -import { FORM_FIELD_PASSWORD_ID } from "../../../emailpassword/constants"; type Response = | { @@ -21,7 +20,6 @@ type Response = export const userPasswordPut = async (_: APIInterface, options: APIOptions): Promise => { const requestBody = await options.req.getJSONBody(); const userId = requestBody.userId; - const email = requestBody.email; const newPassword = requestBody.newPassword; if (userId === undefined || typeof userId !== "string") { @@ -58,68 +56,54 @@ export const userPasswordPut = async (_: APIInterface, options: APIOptions): Pro } if (recipeToUse === "emailpassword") { - let passwordFormFields = EmailPasswordRecipe.getInstanceOrThrowError().config.signUpFeature.formFields.filter( - (field) => field.id === FORM_FIELD_PASSWORD_ID - ); - - let passwordValidationError = await passwordFormFields[0].validate(newPassword); - - if (passwordValidationError !== undefined) { - return { - status: "INVALID_PASSWORD_ERROR", - error: passwordValidationError, - }; - } - - const passwordResetToken = await EmailPassword.createResetPasswordToken(userId, email); + const updateResponse = await EmailPassword.updateEmailOrPassword({ + userId, + password: newPassword, + }); - if (passwordResetToken.status === "UNKNOWN_USER_ID_ERROR") { + if ( + updateResponse.status === "UNKNOWN_USER_ID_ERROR" || + updateResponse.status === "EMAIL_ALREADY_EXISTS_ERROR" || + updateResponse.status === "EMAIL_CHANGE_NOT_ALLOWED_ERROR" + ) { // Techincally it can but its an edge case so we assume that it wont throw new Error("Should never come here"); } - - const passwordResetResponse = await EmailPassword.resetPasswordUsingToken( - passwordResetToken.token, - newPassword - ); - - if (passwordResetResponse.status === "RESET_PASSWORD_INVALID_TOKEN_ERROR") { - throw new Error("Should never come here"); - } + // TODO: check for password policy error has well. + /** + * + * return { + status: "INVALID_PASSWORD_ERROR", + error: passwordValidationError, + }; + */ return { status: "OK", }; } - let passwordFormFields = ThirdPartyEmailPasswordRecipe.getInstanceOrThrowError().config.signUpFeature.formFields.filter( - (field) => field.id === FORM_FIELD_PASSWORD_ID - ); + const updateResponse = await ThirdPartyEmailPassword.updateEmailOrPassword({ + userId, + password: newPassword, + }); - let passwordValidationError = await passwordFormFields[0].validate(newPassword); - - if (passwordValidationError !== undefined) { - return { - status: "INVALID_PASSWORD_ERROR", - error: passwordValidationError, - }; - } - - const passwordResetToken = await ThirdPartyEmailPassword.createResetPasswordToken(userId, email); - - if (passwordResetToken.status === "UNKNOWN_USER_ID_ERROR") { + if ( + updateResponse.status === "UNKNOWN_USER_ID_ERROR" || + updateResponse.status === "EMAIL_ALREADY_EXISTS_ERROR" || + updateResponse.status === "EMAIL_CHANGE_NOT_ALLOWED_ERROR" + ) { // Techincally it can but its an edge case so we assume that it wont throw new Error("Should never come here"); } - - const passwordResetResponse = await ThirdPartyEmailPassword.resetPasswordUsingToken( - passwordResetToken.token, - newPassword - ); - - if (passwordResetResponse.status === "RESET_PASSWORD_INVALID_TOKEN_ERROR") { - throw new Error("Should never come here"); - } + // TODO: check for password policy error has well. + /** + * + * return { + status: "INVALID_PASSWORD_ERROR", + error: passwordValidationError, + }; + */ return { status: "OK", diff --git a/lib/ts/recipe/dashboard/types.ts b/lib/ts/recipe/dashboard/types.ts index 28dd4980f..ac1b7598b 100644 --- a/lib/ts/recipe/dashboard/types.ts +++ b/lib/ts/recipe/dashboard/types.ts @@ -72,6 +72,18 @@ export type RecipeLevelUser = { id: string; userId: string; }; +}; + +export type RecipeLevelUserWithFirstAndLastName = { + recipeId: "emailpassword" | "thirdparty" | "passwordless"; + timeJoined: number; + recipeUserId: string; + email?: string; + phoneNumber?: string; + thirdParty?: { + id: string; + userId: string; + }; firstName: string; lastName: string; }; diff --git a/lib/ts/recipe/dashboard/utils.ts b/lib/ts/recipe/dashboard/utils.ts index aed08ba25..55507a4e3 100644 --- a/lib/ts/recipe/dashboard/utils.ts +++ b/lib/ts/recipe/dashboard/utils.ts @@ -36,13 +36,17 @@ import { TypeInput, TypeNormalisedInput, RecipeLevelUser, + RecipeLevelUserWithFirstAndLastName, } from "./types"; -import Supertokens from "../../supertokens"; +import AccountLinking from "../accountlinking/recipe"; import EmailPasswordRecipe from "../emailpassword/recipe"; import ThirdPartyRecipe from "../thirdparty/recipe"; import PasswordlessRecipe from "../passwordless/recipe"; import ThirdPartyEmailPasswordRecipe from "../thirdpartyemailpassword/recipe"; import ThirdPartyPasswordlessRecipe from "../thirdpartypasswordless/recipe"; +import ThirdParty from "../thirdparty"; +import Passwordless from "../passwordless"; +import ThirdPartyPasswordless from "../thirdpartypasswordless"; export function validateAndNormaliseUserInput(config: TypeInput): TypeNormalisedInput { if (config.apiKey.trim().length === 0) { @@ -144,7 +148,7 @@ export async function getUserForRecipeId( userId: string, recipeId: string ): Promise<{ - user: RecipeLevelUser | undefined; + user: RecipeLevelUserWithFirstAndLastName | undefined; recipe: | "emailpassword" | "thirdparty" @@ -153,8 +157,8 @@ export async function getUserForRecipeId( | "thirdpartypasswordless" | undefined; }> { - let userResponse = await Supertokens.getInstanceOrThrowError()._getUserForRecipeId(userId, recipeId); - let user: RecipeLevelUser | undefined = undefined; + let userResponse = await _getUserForRecipeId(userId, recipeId); + let user: RecipeLevelUserWithFirstAndLastName | undefined = undefined; if (userResponse.user !== undefined) { user = { ...userResponse.user, @@ -168,6 +172,161 @@ export async function getUserForRecipeId( }; } +async function _getUserForRecipeId( + userId: string, + recipeId: string +): Promise<{ + user: RecipeLevelUser | undefined; + recipe: + | "emailpassword" + | "thirdparty" + | "passwordless" + | "thirdpartyemailpassword" + | "thirdpartypasswordless" + | undefined; +}> { + let user: RecipeLevelUser | undefined; + let recipe: + | "emailpassword" + | "thirdparty" + | "passwordless" + | "thirdpartyemailpassword" + | "thirdpartypasswordless" + | undefined; + + const globalUser = await AccountLinking.getInstanceOrThrowError().recipeInterfaceImpl.getUser({ + userId, + userContext: {}, + }); + + if (recipeId === EmailPasswordRecipe.RECIPE_ID) { + try { + // we detect if this recipe has been init or not.. + EmailPasswordRecipe.getInstanceOrThrowError(); + if (globalUser !== undefined) { + let loginMethod = globalUser.loginMethods.find( + (u) => u.recipeId === "emailpassword" && u.recipeUserId === userId + ); + if (loginMethod !== undefined) { + user = { + ...loginMethod, + }; + recipe = "emailpassword"; + } + } + } catch (e) { + // No - op + } + + if (user === undefined) { + try { + // we detect if this recipe has been init or not.. + ThirdPartyEmailPasswordRecipe.getInstanceOrThrowError(); + if (globalUser !== undefined) { + let loginMethod = globalUser.loginMethods.find( + (u) => u.recipeId === "emailpassword" && u.recipeUserId === userId + ); + if (loginMethod !== undefined) { + user = { + ...loginMethod, + }; + recipe = "thirdpartyemailpassword"; + } + } + } catch (e) { + // No - op + } + } + } else if (recipeId === ThirdPartyRecipe.RECIPE_ID) { + try { + const userResponse = await ThirdParty.getUserById(userId); + + if (userResponse !== undefined) { + user = { + ...userResponse, + recipeId: "thirdparty", + }; + recipe = "thirdparty"; + } + } catch (e) { + // No - op + } + + if (user === undefined) { + try { + // we detect if this recipe has been init or not.. + ThirdPartyEmailPasswordRecipe.getInstanceOrThrowError(); + if (globalUser !== undefined) { + let loginMethod = globalUser.loginMethods.find( + (u) => u.recipeId === "thirdparty" && u.recipeUserId === userId + ); + if (loginMethod !== undefined) { + user = { + ...loginMethod, + }; + recipe = "thirdpartyemailpassword"; + } + } + } catch (e) { + // No - op + } + } + + if (user === undefined) { + try { + const userResponse = await ThirdPartyPasswordless.getUserById(userId); + + if (userResponse !== undefined) { + user = { + ...userResponse, + recipeId: "thirdparty", + }; + recipe = "thirdpartypasswordless"; + } + } catch (e) { + // No - op + } + } + } else if (recipeId === PasswordlessRecipe.RECIPE_ID) { + try { + const userResponse = await Passwordless.getUserById({ + userId, + }); + + if (userResponse !== undefined) { + user = { + ...userResponse, + recipeId: "passwordless", + }; + recipe = "passwordless"; + } + } catch (e) { + // No - op + } + + if (user === undefined) { + try { + const userResponse = await ThirdPartyPasswordless.getUserById(userId); + + if (userResponse !== undefined) { + user = { + ...userResponse, + recipeId: "passwordless", + }; + recipe = "thirdpartypasswordless"; + } + } catch (e) { + // No - op + } + } + } + + return { + user, + recipe, + }; +} + export function isRecipeInitialised(recipeId: RecipeIdForUser): boolean { let isRecipeInitialised = false; diff --git a/lib/ts/recipe/emailpassword/api/implementation.ts b/lib/ts/recipe/emailpassword/api/implementation.ts index feb0bc9dd..404c70b06 100644 --- a/lib/ts/recipe/emailpassword/api/implementation.ts +++ b/lib/ts/recipe/emailpassword/api/implementation.ts @@ -3,10 +3,21 @@ import { logDebugMessage } from "../../../logger"; import Session from "../../session"; import { SessionContainerInterface } from "../../session/types"; import { GeneralErrorResponse } from "../../../types"; +import { listUsersByAccountInfo, getUser } from "../../../"; +import AccountLinking from "../../accountlinking/recipe"; +import EmailVerification from "../../emailverification/recipe"; +import { AccountLinkingClaim } from "../../accountlinking/accountLinkingClaim"; +import { storeIntoAccountToLinkTable } from "../../accountlinking"; +import { RecipeLevelUser } from "../../accountlinking/types"; export default function getAPIImplementation(): APIInterface { return { - linkAccountToExistingAccountPOST: async function (_input: { + linkAccountToExistingAccountPOST: async function ({ + formFields, + session, + options, + userContext, + }: { formFields: { id: string; value: string; @@ -17,41 +28,106 @@ export default function getAPIImplementation(): APIInterface { }): Promise< | { status: "OK"; - user: User; - createdNewRecipeUser: boolean; - session: SessionContainerInterface; wereAccountsAlreadyLinked: boolean; } | { - status: "RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"; - primaryUserId: string; - description: string; - } - | { - status: "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"; - primaryUserId: string; + status: "NEW_ACCOUNT_NEEDS_TO_BE_VERIFIED_ERROR" | "ACCOUNT_LINKING_NOT_ALLOWED_ERROR"; description: string; } | { - status: "ACCOUNT_LINKING_NOT_ALLOWED_ERROR"; - description: string; - } - | { - status: "ACCOUNT_NOT_VERIFIED_ERROR"; - isNotVerifiedAccountFromInputSession: boolean; - description: string; + status: "WRONG_CREDENTIALS_ERROR"; } | GeneralErrorResponse > { - return { - status: "ACCOUNT_NOT_VERIFIED_ERROR", - isNotVerifiedAccountFromInputSession: false, - description: "", + const email = formFields.filter((f) => f.id === "email")[0].value; + const password = formFields.filter((f) => f.id === "password")[0].value; + + const createRecipeUserFunc = async (): Promise => { + await options.recipeImplementation.createNewRecipeUser({ + email, + password, + userContext, + }); + // we ignore the result from the above cause after this, function returns, + // the linkAccountsWithUserFromSession anyway does recursion.. }; + + const verifyCredentialsFunc = async (): Promise< + | { status: "OK" } + | { + status: "CUSTOM_RESPONSE"; + resp: { + status: "WRONG_CREDENTIALS_ERROR"; + }; + } + > => { + const signInResult = await options.recipeImplementation.signIn({ + email, + password, + userContext, + }); + + if (signInResult.status === "OK") { + return { status: "OK" }; + } else { + return { + status: "CUSTOM_RESPONSE", + resp: signInResult, + }; + } + }; + + let accountLinkingInstance = await AccountLinking.getInstanceOrThrowError(); + let result = await accountLinkingInstance.linkAccountsWithUserFromSession<{ + status: "WRONG_CREDENTIALS_ERROR"; + }>({ + session, + newUser: { + email, + recipeId: "emailpassword", + }, + createRecipeUserFunc, + verifyCredentialsFunc, + userContext, + }); + if (result.status === "CUSTOM_RESPONSE") { + return result.resp; + } else if (result.status === "NEW_ACCOUNT_NEEDS_TO_BE_VERIFIED_ERROR") { + // this will store in the db that these need to be linked, + // and after verification, it will link these accounts. + let toLinkResult = await storeIntoAccountToLinkTable( + result.recipeUserId, + result.primaryUserId, + userContext + ); + if (toLinkResult.status === "RECIPE_USER_ID_ALREADY_LINKED_WITH_PRIMARY_USER_ID_ERROR") { + if (toLinkResult.primaryUserId === result.primaryUserId) { + // this is some sort of a race condition issue, so we just ignore it + // since we already linked to the session's account anyway... + return { + status: "OK", + wereAccountsAlreadyLinked: true, + }; + } else { + return { + status: "ACCOUNT_LINKING_NOT_ALLOWED_ERROR", + description: + "Input user is already linked to another account. Please try again or contact support.", + }; + } + } + // status: "OK" + await session.fetchAndSetClaim(AccountLinkingClaim, userContext); + return { + status: "NEW_ACCOUNT_NEEDS_TO_BE_VERIFIED_ERROR", + description: "Before accounts can be linked, the new account must be verified", + }; + } + // status: "OK" | "ACCOUNT_LINKING_NOT_ALLOWED_ERROR" + return result; }, emailExistsGET: async function ({ email, - options, userContext, }: { email: string; @@ -64,11 +140,25 @@ export default function getAPIImplementation(): APIInterface { } | GeneralErrorResponse > { - let user = await options.recipeImplementation.getUserByEmail({ email, userContext }); + let usersWithSameEmail = await listUsersByAccountInfo( + { + email, + }, + userContext + ); + + let exists = + usersWithSameEmail.find((user) => { + return ( + user.loginMethods.find((lM) => { + return lM.recipeId === "emailpassword" && lM.email === email; + }) !== undefined + ); + }) !== undefined; return { status: "OK", - exists: user !== undefined, + exists, }; }, generatePasswordResetTokenPOST: async function ({ @@ -89,46 +179,208 @@ export default function getAPIImplementation(): APIInterface { | { status: "PASSWORD_RESET_NOT_ALLOWED"; reason: string } | GeneralErrorResponse > { - let email = formFields.filter((f) => f.id === "email")[0].value; + const email = formFields.filter((f) => f.id === "email")[0].value; + + // this function will be reused in different parts of the flow below.. + async function generateAndSendPasswordResetToken( + userId: string + ): Promise< + | { + status: "OK"; + } + | { status: "PASSWORD_RESET_NOT_ALLOWED"; reason: string } + | GeneralErrorResponse + > { + // the user ID here can be primary or recipe level. + let response = await options.recipeImplementation.createResetPasswordToken({ + userId, + email, + userContext, + }); + if (response.status === "UNKNOWN_USER_ID_ERROR") { + logDebugMessage(`Password reset email not sent, unknown user id: ${userId}`); + return { + status: "OK", + }; + } + + let passwordResetLink = + options.appInfo.websiteDomain.getAsStringDangerous() + + options.appInfo.websiteBasePath.getAsStringDangerous() + + "/reset-password?token=" + + response.token + + "&rid=" + + options.recipeId; + + logDebugMessage(`Sending password reset email to ${email}`); + await options.emailDelivery.ingredientInterfaceImpl.sendEmail({ + type: "PASSWORD_RESET", + user: { + id: userId, + email, + }, + passwordResetLink, + userContext, + }); - let user = await options.recipeImplementation.getUserByEmail({ email, userContext }); - if (user === undefined) { return { status: "OK", }; } - let response = await options.recipeImplementation.createResetPasswordToken({ - userId: user.recipeUserId, - email: user.email, - userContext, + /** + * check if primaryUserId is linked with this email + */ + let users = await listUsersByAccountInfo({ + email, }); - if (response.status === "UNKNOWN_USER_ID_ERROR") { - logDebugMessage(`Password reset email not sent, unknown user id: ${user.id}`); - return { - status: "OK", - }; + + // we find the recipe user ID of the email password account from the user's list + // for later use. + let emailPasswordAccount: RecipeLevelUser | undefined = undefined; + for (let i = 0; i < users.length; i++) { + let emailPasswordAccountTmp = users[i].loginMethods.find( + (l) => l.recipeId === "emailpassword" && l.email === email + ); + if (emailPasswordAccount !== undefined) { + emailPasswordAccount = emailPasswordAccountTmp; + break; + } } - let passwordResetLink = - options.appInfo.websiteDomain.getAsStringDangerous() + - options.appInfo.websiteBasePath.getAsStringDangerous() + - "/reset-password?token=" + - response.token + - "&rid=" + - options.recipeId; - - logDebugMessage(`Sending password reset email to ${email}`); - await options.emailDelivery.ingredientInterfaceImpl.sendEmail({ - type: "PASSWORD_RESET", - user, - passwordResetLink, - userContext, - }); + // we find the primary user ID from the user's list for later use. + let primaryUserAssociatedWithEmail = users.find((u) => u.isPrimaryUser); - return { - status: "OK", - }; + // first we check if there even exists a primary user that has the input email + // if not, then we do the regular flow for password reset. + if (primaryUserAssociatedWithEmail === undefined) { + if (emailPasswordAccount === undefined) { + logDebugMessage(`Password reset email not sent, unknown user email: ${email}`); + return { + status: "OK", + }; + } + return await generateAndSendPasswordResetToken(emailPasswordAccount.recipeUserId); + } + + // Now we need to check that if there exists any email password user at all + // for the input email. If not, then it implies that when the token is consumed, + // then we will create a new user - so we should only generate the token if + // the criteria for the new user is met. + if (emailPasswordAccount === undefined) { + // this means that there is no email password user that exists for the input email. + // So we check for the sign up condition and only go ahead if that condition is + // met. + let isSignUpAllowed = await AccountLinking.getInstanceOrThrowError().isSignUpAllowed({ + newUser: { + recipeId: "emailpassword", + email, + }, + userContext, + }); + if (isSignUpAllowed) { + // notice that we pass in the primary user ID here. This means that + // we will be creating a new email password account with the token + // is consumed and linking it to this primary user. + return await generateAndSendPasswordResetToken(primaryUserAssociatedWithEmail.id); + } else { + logDebugMessage( + `Password reset email not sent, isSignUpAllowed returned false for email: ${email}` + ); + return { + status: "OK", + }; + } + } + + // At this point, we know that some email password user exists with this email + // and also some primary user ID exist. We now need to find out if they are linked + // together or not. If they are linked together, then we can just generate the token + // else we check for more security conditions (since we will be linking them post token generation) + + let areTheTwoAccountsLinked = + primaryUserAssociatedWithEmail.loginMethods.find((lm) => { + return lm.recipeUserId === emailPasswordAccount!.recipeUserId; + }) !== undefined; + + if (areTheTwoAccountsLinked) { + return await generateAndSendPasswordResetToken(emailPasswordAccount.recipeId); + } + + // Here we know that the two accounts are NOT linked. We now need to check for an + // extra security measure here to make sure that the input email in the primary user + // is verified, and if not, we need to make sure that there is no other email / phone number + // associated with the primary user account. If there is, then we do not proceed. + + /* + This security measure helps prevent the following attack: + An attacker has email A and they create an account using TP and it doesn't matter if A is verified or not. Now they create another account using EP with email A and verifies it. Both these accounts are linked. Now the attacker changes the email for EP recipe to B which makes the EP account unverified, but it's still linked. + + If the real owner of B tries to signup using EP, it will say that the account already exists so they may try to reset password which should be denied because then they will end up getting access to attacker's account and verify the EP account. + + The problem with this situation is if the EP account is verified, it will allow further sign-ups with email B which will also be linked to this primary account (that the attacker had created with email A). + + It is important to realize that the attacker had created another account with A because if they hadn't done that, then they wouldn't have access to this account after the real user resets the password which is why it is important to check there is another non-EP account linked to the primary such that the email is not the same as B. + + Exception to the above is that, if there is a third recipe account linked to the above two accounts and has B as verified, then we should allow reset password token generation because user has already proven that the owns the email B + */ + + // But first, this only matters it the user cares about checking for email verification status.. + + let shouldDoAccountLinkingResponse = await AccountLinking.getInstanceOrThrowError().config.shouldDoAutomaticAccountLinking( + emailPasswordAccount, + primaryUserAssociatedWithEmail, + undefined, + userContext + ); + + if (!shouldDoAccountLinkingResponse.shouldAutomaticallyLink) { + // here we will go ahead with the token generation cause + // even when the token is consumed, we will not be linking the accounts + // so no need to check for anything + return await generateAndSendPasswordResetToken(emailPasswordAccount.recipeId); + } + + if (!shouldDoAccountLinkingResponse.shouldRequireVerification) { + // the checks below are related to email verification, and if the user + // does not care about that, then we should just continue with token generation + return await generateAndSendPasswordResetToken(emailPasswordAccount.recipeId); + } + + // Now we start the required security checks. First we check if the primary user + // it has just one linked account. And if that's true, then we continue + // cause then there is no scope for account takeover + if (primaryUserAssociatedWithEmail.loginMethods.length === 1) { + return await generateAndSendPasswordResetToken(emailPasswordAccount.recipeId); + } + + // Next we check if there is any login method in which the input email is verified. + // If that is the case, then it's proven that the user owns the email and we can + // trust linking of the email password account. + let emailVerified = + primaryUserAssociatedWithEmail.loginMethods.find((lm) => { + return lm.email === email && lm.verified; + }) !== undefined; + + if (emailVerified) { + return await generateAndSendPasswordResetToken(emailPasswordAccount.recipeId); + } + + // finally, we check if the primary user has any other email / phone number + // associated with this account - and if it does, then it means that + // there is a risk of account takeover, so we do not allow the token to be generated + let hasOtherEmailOrPhone = + primaryUserAssociatedWithEmail.loginMethods.find((lm) => { + return lm.email !== email || lm.phoneNumber !== undefined; + }) !== undefined; + if (hasOtherEmailOrPhone) { + return { + status: "PASSWORD_RESET_NOT_ALLOWED", + reason: "Token generation was not done because of account take over risk. Please contact support.", + }; + } else { + return await generateAndSendPasswordResetToken(emailPasswordAccount.recipeId); + } }, passwordResetPOST: async function ({ formFields, @@ -146,21 +398,177 @@ export default function getAPIImplementation(): APIInterface { }): Promise< | { status: "OK"; - userId: string; + user: User; email: string; } | { status: "RESET_PASSWORD_INVALID_TOKEN_ERROR" } | GeneralErrorResponse > { + async function markEmailAsVerified(userId: string, email: string) { + const emailVerificationInstance = EmailVerification.getInstance(); + if (emailVerificationInstance) { + const tokenResponse = await emailVerificationInstance.recipeInterfaceImpl.createEmailVerificationToken( + { + userId, + email, + userContext, + } + ); + + if (tokenResponse.status === "OK") { + await emailVerificationInstance.recipeInterfaceImpl.verifyEmailUsingToken({ + token: tokenResponse.token, + userContext, + }); + } + } + } + + async function doUpdatePassword(): Promise< + | { + status: "OK"; + user: User; + email: string; + } + | { status: "RESET_PASSWORD_INVALID_TOKEN_ERROR" } + | GeneralErrorResponse + > { + let updateResponse = await options.recipeImplementation.updateEmailOrPassword({ + userId: recipeUserIdForWhomTokenWasGenerated, + password: newPassword, + userContext, + }); + if ( + updateResponse.status === "EMAIL_ALREADY_EXISTS_ERROR" || + updateResponse.status === "EMAIL_CHANGE_NOT_ALLOWED_ERROR" + ) { + throw new Error("This should never come here because we are not updating the email"); + } else if (updateResponse.status === "UNKNOWN_USER_ID_ERROR") { + // This should happen only cause of a race condition where the user + // might be deleted before token creation and consumption. + return { + status: "RESET_PASSWORD_INVALID_TOKEN_ERROR", + }; + } else { + // status: "OK" + return { + status: "OK", + user: existingUser!, + email: emailForWhomTokenWasGenerated, + }; + } + // TODO: we need to also handle password policy error. Note that this needs + // to happen in the api file (before this function is called) as well + // cause we don't want to consume the token unnecessarily. + } + let newPassword = formFields.filter((f) => f.id === "password")[0].value; - let response = await options.recipeImplementation.resetPasswordUsingToken({ + let tokenConsumptionResponse = await options.recipeImplementation.consumePasswordResetToken({ token, - newPassword, userContext, }); - return response; + if (tokenConsumptionResponse.status === "RESET_PASSWORD_INVALID_TOKEN_ERROR") { + return tokenConsumptionResponse; + } + + let recipeUserIdForWhomTokenWasGenerated = tokenConsumptionResponse.userId; + let emailForWhomTokenWasGenerated = tokenConsumptionResponse.email; + + let existingUser = await getUser(tokenConsumptionResponse.userId, userContext); + + if (existingUser === undefined) { + // This should happen only cause of a race condition where the user + // might be deleted before token creation and consumption. + // Also note that this being undefined doesn't mean that the email password + // user does not exist, but it means that there is no reicpe or primary user + // for whom the token was generated. + return { + status: "RESET_PASSWORD_INVALID_TOKEN_ERROR", + }; + } + + // We start by checking if the existingUser is a primary user or not. If it is, + // then we will try and create a new email password user and link it to the primary user (if required) + + if (existingUser.isPrimaryUser) { + // If this user contains an email password account for whom the token was generated, + // then we update that user's password. + let emailPasswordUserIsLinkedToExistingUser = + existingUser.loginMethods.find((lm) => { + // we check based on user ID and not email because the only time + // the primary user ID is used for token generation is if the email password + // user did not exist - in which case the value of emailPasswordUserExists will + // resolve to false anyway, and that's what we want. + return lm.recipeUserId === recipeUserIdForWhomTokenWasGenerated; + }) !== undefined; + + if (emailPasswordUserIsLinkedToExistingUser) { + return doUpdatePassword(); + } else { + // this means that the existingUser does not have an emailpassword user associated + // with it. It could now mean that no emailpassword user exists, or it could mean that + // the the ep user exists, but it's not linked to the current account. + // If no ep user doesn't exists, we will create one, and link it to the existing account. + // If ep user exists, then it means there is some race condition cause + // then the token should have been generated for that user instead of the primary user, + // and it shouldn't have come into this branch. So we can simply send a password reset + // invalid error and the user can try again. + + // NOTE: We do not ask the dev if we should do account linking or not here + // cause we already have asked them this when generating an password reset token. + + let createUserResponse = await options.recipeImplementation.createNewRecipeUser({ + email: tokenConsumptionResponse.email, + password: newPassword, + userContext, + }); + if (createUserResponse.status === "EMAIL_ALREADY_EXISTS_ERROR") { + // this means that the user already existed and we can just return an invalid + // token (see the above comment) + return { + status: "RESET_PASSWORD_INVALID_TOKEN_ERROR", + }; + } else { + // we mark the email as verified because password reset also requires + // access to the email to work.. This has a good side effect that + // any other login method with the same email in existingAccount will also get marked + // as verified. + await markEmailAsVerified(createUserResponse.user.id, tokenConsumptionResponse.email); + + // Now we try and link the accounts. The function below will try and also + // create a primary user of the new account, and if it does that, it's OK.. + // But in most cases, it will end up linking to existing account since the + // email is shared. + let linkedToUserId = await AccountLinking.getInstanceOrThrowError().createPrimaryUserIdOrLinkAccounts( + { + recipeUserId: createUserResponse.user.id, + isVerified: true, + checkAccountsToLinkTableAsWell: true, + userContext, + } + ); + if (linkedToUserId !== existingUser.id) { + // this means that the account we just linked to + // was not the one we had expected to link it to. This can happen + // due to some race condition or the other.. Either way, this + // is not an issue and we can just return OK + } + return { + status: "OK", + email: tokenConsumptionResponse.email, + user: (await getUser(linkedToUserId, userContext))!, // we refetch cause we want to return the user object with the updated login methods. + }; + } + } + } else { + // This means that the existing user is not a primary account, which implies that + // it must be a non linked email password account. In this case, we simply update the password. + // Linking to an existing account will be done after the user goes through the email + // verification flow once they log in (if applicable). + return doUpdatePassword(); + } }, signInPOST: async function ({ formFields, @@ -191,13 +599,21 @@ export default function getAPIImplementation(): APIInterface { if (response.status === "WRONG_CREDENTIALS_ERROR") { return response; } - let user = response.user; + + let emailPasswordRecipeUser = response.user.loginMethods.find( + (u) => u.recipeId === "emailpassword" && u.email === email + ); + + if (emailPasswordRecipeUser === undefined) { + // this can happen cause of some race condition, but it's not a big deal. + throw new Error("Race condition error - please call this API again"); + } let session = await Session.createNewSession( options.req, options.res, - user.id, - user.recipeUserId, + response.user.id, + emailPasswordRecipeUser.recipeUserId, {}, {}, userContext @@ -205,7 +621,7 @@ export default function getAPIImplementation(): APIInterface { return { status: "OK", session, - user, + user: response.user, }; }, signUpPOST: async function ({ @@ -224,8 +640,6 @@ export default function getAPIImplementation(): APIInterface { status: "OK"; session: SessionContainerInterface; user: User; - createdNewUser: boolean; - createdNewRecipeUser: boolean; } | { status: "EMAIL_ALREADY_EXISTS_ERROR"; @@ -239,17 +653,45 @@ export default function getAPIImplementation(): APIInterface { let email = formFields.filter((f) => f.id === "email")[0].value; let password = formFields.filter((f) => f.id === "password")[0].value; - let response = await options.recipeImplementation.signUp({ email, password, userContext }); + let isSignUpAllowed = await AccountLinking.getInstanceOrThrowError().isSignUpAllowed({ + newUser: { + recipeId: "emailpassword", + email, + }, + userContext, + }); + + if (!isSignUpAllowed) { + return { + status: "SIGNUP_NOT_ALLOWED", + reason: + "The input email is already associated with another account where it is not verified. Please verify the other account before trying again.", + }; + } + + // this function also does account linking + let response = await options.recipeImplementation.signUp({ + email, + password, + userContext, + }); if (response.status === "EMAIL_ALREADY_EXISTS_ERROR") { return response; } - let user = response.user; + let emailPasswordRecipeUser = response.user.loginMethods.find( + (u) => u.recipeId === "emailpassword" && u.email === email + ); + + if (emailPasswordRecipeUser === undefined) { + // this can happen cause of some race condition, but it's not a big deal. + throw new Error("Race condition error - please call this API again"); + } let session = await Session.createNewSession( options.req, options.res, - user.id, - user.recipeUserId, + response.user.id, + emailPasswordRecipeUser.recipeUserId, {}, {}, userContext @@ -257,9 +699,7 @@ export default function getAPIImplementation(): APIInterface { return { status: "OK", session, - user, - createdNewUser: true, // TODO - createdNewRecipeUser: true, // TODO + user: response.user, }; }, }; diff --git a/lib/ts/recipe/emailpassword/api/linkAccountToExistingAccount.ts b/lib/ts/recipe/emailpassword/api/linkAccountToExistingAccount.ts new file mode 100644 index 000000000..9967c1cd1 --- /dev/null +++ b/lib/ts/recipe/emailpassword/api/linkAccountToExistingAccount.ts @@ -0,0 +1,54 @@ +/* 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. + */ + +import { send200Response } from "../../../utils"; +import { validateFormFieldsOrThrowError } from "./utils"; +import { APIInterface, APIOptions } from ".."; +import { makeDefaultUserContextFromAPI } from "../../../utils"; +import Session from "../../session"; + +export default async function linkAccountToExistingAccountAPI( + apiImplementation: APIInterface, + options: APIOptions +): Promise { + if (apiImplementation.linkAccountToExistingAccountPOST === undefined) { + return false; + } + + let formFields: { + id: string; + value: string; + }[] = await validateFormFieldsOrThrowError( + options.config.signUpFeature.formFields, + (await options.req.getJSONBody()).formFields + ); + + let userContext = makeDefaultUserContextFromAPI(options.req); + const session = await Session.getSession( + options.req, + options.res, + { overrideGlobalClaimValidators: () => [] }, + userContext + ); + let result = await apiImplementation.linkAccountToExistingAccountPOST({ + formFields, + session: session, + options, + userContext, + }); + // status: NEW_ACCOUNT_NEEDS_TO_BE_VERIFIED_ERROR | ACCOUNT_LINKING_NOT_ALLOWED_ERROR | WRONG_CREDENTIALS_ERROR | GENERAL_ERROR | "OK" + send200Response(options.res, result); + return true; +} diff --git a/lib/ts/recipe/emailpassword/api/passwordReset.ts b/lib/ts/recipe/emailpassword/api/passwordReset.ts index f0ad8ca5b..70f5c6dd6 100644 --- a/lib/ts/recipe/emailpassword/api/passwordReset.ts +++ b/lib/ts/recipe/emailpassword/api/passwordReset.ts @@ -26,7 +26,10 @@ export default async function passwordReset(apiImplementation: APIInterface, opt return false; } - // step 1 + // step 1: We need to do this here even though the update emailpassword recipe function would do this cause: + // - we want to throw this error before consuming the token, so that the user can try again + // - there is a case in the api impl where we create a new user, and we want to assign + // a password that meets the password policy. let formFields: { id: string; value: string; diff --git a/lib/ts/recipe/emailpassword/constants.ts b/lib/ts/recipe/emailpassword/constants.ts index 730d815c7..98573f240 100644 --- a/lib/ts/recipe/emailpassword/constants.ts +++ b/lib/ts/recipe/emailpassword/constants.ts @@ -26,3 +26,5 @@ export const GENERATE_PASSWORD_RESET_TOKEN_API = "/user/password/reset/token"; export const PASSWORD_RESET_API = "/user/password/reset"; export const SIGNUP_EMAIL_EXISTS_API = "/signup/email/exists"; + +export const LINK_ACCOUNT_TO_EXISTING_ACCOUNT_API = "/signup/link-account"; diff --git a/lib/ts/recipe/emailpassword/emaildelivery/services/backwardCompatibility/index.ts b/lib/ts/recipe/emailpassword/emaildelivery/services/backwardCompatibility/index.ts index 060079c83..89e822288 100644 --- a/lib/ts/recipe/emailpassword/emaildelivery/services/backwardCompatibility/index.ts +++ b/lib/ts/recipe/emailpassword/emaildelivery/services/backwardCompatibility/index.ts @@ -12,69 +12,49 @@ * License for the specific language governing permissions and limitations * under the License. */ -import { TypeEmailPasswordEmailDeliveryInput, User, RecipeInterface } from "../../../types"; +import { TypeEmailPasswordEmailDeliveryInput, RecipeInterface } from "../../../types"; import { createAndSendCustomEmail as defaultCreateAndSendCustomEmail } from "../../../passwordResetFunctions"; import { NormalisedAppinfo } from "../../../../../types"; import { EmailDeliveryInterface } from "../../../../../ingredients/emaildelivery/types"; export default class BackwardCompatibilityService implements EmailDeliveryInterface { - private recipeInterfaceImpl: RecipeInterface; private isInServerlessEnv: boolean; private appInfo: NormalisedAppinfo; private resetPasswordUsingTokenFeature: { - createAndSendCustomEmail: (user: User, passwordResetURLWithToken: string, userContext: any) => Promise; + createAndSendCustomEmail: ( + user: { + id: string; + email: string; + }, + passwordResetURLWithToken: string, + userContext: any + ) => Promise; }; - constructor( - recipeInterfaceImpl: RecipeInterface, - appInfo: NormalisedAppinfo, - isInServerlessEnv: boolean, - resetPasswordUsingTokenFeature?: { - createAndSendCustomEmail?: ( - user: User, - passwordResetURLWithToken: string, - userContext: any - ) => Promise; - } - ) { - this.recipeInterfaceImpl = recipeInterfaceImpl; + constructor(_: RecipeInterface, appInfo: NormalisedAppinfo, isInServerlessEnv: boolean) { this.isInServerlessEnv = isInServerlessEnv; this.appInfo = appInfo; { - let inputCreateAndSendCustomEmail = resetPasswordUsingTokenFeature?.createAndSendCustomEmail; - this.resetPasswordUsingTokenFeature = - inputCreateAndSendCustomEmail !== undefined - ? { - createAndSendCustomEmail: inputCreateAndSendCustomEmail, - } - : { - createAndSendCustomEmail: defaultCreateAndSendCustomEmail(this.appInfo), - }; + this.resetPasswordUsingTokenFeature = { + createAndSendCustomEmail: defaultCreateAndSendCustomEmail(this.appInfo), + }; } } sendEmail = async (input: TypeEmailPasswordEmailDeliveryInput & { userContext: any }) => { - let user = await this.recipeInterfaceImpl.getUserById({ - userId: input.user.recipeUserId, - userContext: input.userContext, - }); - if (user === undefined) { - throw Error("this should never come here"); - } // we add this here cause the user may have overridden the sendEmail function // to change the input email and if we don't do this, the input email // will get reset by the getUserById call above. - user.email = input.user.email; try { if (!this.isInServerlessEnv) { this.resetPasswordUsingTokenFeature - .createAndSendCustomEmail(user, input.passwordResetLink, input.userContext) + .createAndSendCustomEmail(input.user, input.passwordResetLink, input.userContext) .catch((_) => {}); } else { // see https://github.com/supertokens/supertokens-node/pull/135 await this.resetPasswordUsingTokenFeature.createAndSendCustomEmail( - user, + input.user, input.passwordResetLink, input.userContext ); diff --git a/lib/ts/recipe/emailpassword/index.ts b/lib/ts/recipe/emailpassword/index.ts index 42276add4..89c98a471 100644 --- a/lib/ts/recipe/emailpassword/index.ts +++ b/lib/ts/recipe/emailpassword/index.ts @@ -15,7 +15,8 @@ import Recipe from "./recipe"; import SuperTokensError from "./error"; -import { RecipeInterface, User, APIOptions, APIInterface, TypeEmailPasswordEmailDeliveryInput } from "./types"; +import { RecipeInterface, APIOptions, APIInterface, TypeEmailPasswordEmailDeliveryInput } from "./types"; +import { User } from "../../types"; export default class Wrapper { static init = Recipe.init; @@ -38,20 +39,6 @@ export default class Wrapper { }); } - static getUserById(userId: string, userContext?: any) { - return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.getUserById({ - userId, - userContext: userContext === undefined ? {} : userContext, - }); - } - - static getUserByEmail(email: string, userContext?: any) { - return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.getUserByEmail({ - email, - userContext: userContext === undefined ? {} : userContext, - }); - } - /** * We do not make email optional here cause we want to * allow passing in primaryUserId. If we make email optional, @@ -71,10 +58,9 @@ export default class Wrapper { }); } - static resetPasswordUsingToken(token: string, newPassword: string, userContext?: any) { - return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.resetPasswordUsingToken({ + static consumePasswordResetToken(token: string, userContext?: any) { + return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.consumePasswordResetToken({ token, - newPassword, userContext: userContext === undefined ? {} : userContext, }); } @@ -103,13 +89,9 @@ export let signUp = Wrapper.signUp; export let signIn = Wrapper.signIn; -export let getUserById = Wrapper.getUserById; - -export let getUserByEmail = Wrapper.getUserByEmail; - export let createResetPasswordToken = Wrapper.createResetPasswordToken; -export let resetPasswordUsingToken = Wrapper.resetPasswordUsingToken; +export let consumePasswordResetToken = Wrapper.consumePasswordResetToken; export let updateEmailOrPassword = Wrapper.updateEmailOrPassword; diff --git a/lib/ts/recipe/emailpassword/passwordResetFunctions.ts b/lib/ts/recipe/emailpassword/passwordResetFunctions.ts index d5671a545..3f8d2d2d8 100644 --- a/lib/ts/recipe/emailpassword/passwordResetFunctions.ts +++ b/lib/ts/recipe/emailpassword/passwordResetFunctions.ts @@ -13,13 +13,18 @@ * under the License. */ -import { User } from "./types"; import { NormalisedAppinfo } from "../../types"; import axios, { AxiosError } from "axios"; import { logDebugMessage } from "../../logger"; export function createAndSendCustomEmail(appInfo: NormalisedAppinfo) { - return async (user: User, passwordResetURLWithToken: string) => { + return async ( + user: { + id: string; + email: string; + }, + passwordResetURLWithToken: string + ) => { // related issue: https://github.com/supertokens/supertokens-node/issues/38 if (process.env.TEST_MODE === "testing") { return; diff --git a/lib/ts/recipe/emailpassword/recipe.ts b/lib/ts/recipe/emailpassword/recipe.ts index 094ad0e4c..13ded1267 100644 --- a/lib/ts/recipe/emailpassword/recipe.ts +++ b/lib/ts/recipe/emailpassword/recipe.ts @@ -25,6 +25,7 @@ import { GENERATE_PASSWORD_RESET_TOKEN_API, PASSWORD_RESET_API, SIGNUP_EMAIL_EXISTS_API, + LINK_ACCOUNT_TO_EXISTING_ACCOUNT_API, } from "./constants"; import signUpAPI from "./api/signup"; import signInAPI from "./api/signin"; @@ -42,6 +43,7 @@ import EmailDeliveryIngredient from "../../ingredients/emaildelivery"; import { TypeEmailPasswordEmailDeliveryInput } from "./types"; import { PostSuperTokensInitCallbacks } from "../../postSuperTokensInitCallbacks"; import { GetEmailForUserIdFunc } from "../emailverification/types"; +import { getUser } from "../../"; export default class Recipe extends RecipeModule { private static instance: Recipe | undefined = undefined; @@ -158,6 +160,12 @@ export default class Recipe extends RecipeModule { id: SIGNUP_EMAIL_EXISTS_API, disabled: this.apiImpl.emailExistsGET === undefined, }, + { + method: "post", + pathWithoutApiBasePath: new NormalisedURLPath(LINK_ACCOUNT_TO_EXISTING_ACCOUNT_API), + id: LINK_ACCOUNT_TO_EXISTING_ACCOUNT_API, + disabled: this.apiImpl.linkAccountToExistingAccountPOST === undefined, + }, ]; }; @@ -217,12 +225,21 @@ export default class Recipe extends RecipeModule { // extra instance functions below............... getEmailForUserId: GetEmailForUserIdFunc = async (userId, userContext) => { - let userInfo = await this.recipeInterfaceImpl.getUserById({ userId, userContext }); - if (userInfo !== undefined) { - return { - status: "OK", - email: userInfo.email, - }; + let user = await getUser(userId, userContext); + if (user !== undefined) { + let recipeLevelUser = user.loginMethods.find( + (u) => u.recipeId === "emailpassword" && u.recipeUserId === userId + ); + if (recipeLevelUser !== undefined) { + if (recipeLevelUser.email === undefined) { + // this check if only for types purposes. + throw new Error("Should never come here"); + } + return { + status: "OK", + email: recipeLevelUser.email, + }; + } } return { status: "UNKNOWN_USER_ID_ERROR", diff --git a/lib/ts/recipe/emailpassword/recipeImplementation.ts b/lib/ts/recipe/emailpassword/recipeImplementation.ts index 7ee518b0e..fbb4a74c0 100644 --- a/lib/ts/recipe/emailpassword/recipeImplementation.ts +++ b/lib/ts/recipe/emailpassword/recipeImplementation.ts @@ -1,27 +1,67 @@ -import { RecipeInterface, User } from "./types"; +import { RecipeInterface } from "./types"; +import AccountLinking from "../accountlinking/recipe"; import { Querier } from "../../querier"; import NormalisedURLPath from "../../normalisedURLPath"; +import { getUser } from "../.."; +import { User } from "../../types"; export default function getRecipeInterface(querier: Querier): RecipeInterface { return { - signUp: async function ({ - email, - password, - }: { - email: string; - password: string; - }): Promise<{ status: "OK"; user: User } | { status: "EMAIL_ALREADY_EXISTS_ERROR" }> { - let response = await querier.sendPostRequest(new NormalisedURLPath("/recipe/signup"), { + signUp: async function ( + this: RecipeInterface, + { + email, + password, + userContext, + }: { + email: string; + password: string; + userContext: any; + } + ): Promise<{ status: "OK"; user: User } | { status: "EMAIL_ALREADY_EXISTS_ERROR" }> { + // this function does not check if there is some primary user where the email + // of that primary user is unverified (isSignUpAllowed function logic) cause + // that is checked in the API layer before calling this function. + // This is the recipe function layer which can be + // called by the user manually as well if they want to. So we allow them to do that. + let response = await this.createNewRecipeUser({ email, password, + userContext, }); - if (response.status === "OK") { + if (response.status === "EMAIL_ALREADY_EXISTS_ERROR") { return response; - } else { - return { - status: "EMAIL_ALREADY_EXISTS_ERROR", - }; } + + let userId = await AccountLinking.getInstanceOrThrowError().createPrimaryUserIdOrLinkAccounts({ + // we can use index 0 cause this is a new recipe user + recipeUserId: response.user.loginMethods[0].recipeUserId, + checkAccountsToLinkTableAsWell: true, + isVerified: false, + userContext, + }); + + return { + status: "OK", + user: (await getUser(userId, userContext))!, + }; + }, + + createNewRecipeUser: async function (input: { + email: string; + password: string; + userContext: any; + }): Promise< + | { + status: "OK"; + user: User; + } + | { status: "EMAIL_ALREADY_EXISTS_ERROR" } + > { + return await querier.sendPostRequest(new NormalisedURLPath("/recipe/signup"), { + email: input.email, + password: input.password, + }); }, signIn: async function ({ @@ -31,71 +71,30 @@ export default function getRecipeInterface(querier: Querier): RecipeInterface { email: string; password: string; }): Promise<{ status: "OK"; user: User } | { status: "WRONG_CREDENTIALS_ERROR" }> { - let response = await querier.sendPostRequest(new NormalisedURLPath("/recipe/signin"), { + return await querier.sendPostRequest(new NormalisedURLPath("/recipe/signin"), { email, password, }); - if (response.status === "OK") { - return response; - } else { - return { - status: "WRONG_CREDENTIALS_ERROR", - }; - } - }, - - getUserById: async function ({ userId }: { userId: string }): Promise { - let response = await querier.sendGetRequest(new NormalisedURLPath("/recipe/user"), { - userId, - }); - if (response.status === "OK") { - return { - ...response.user, - }; - } else { - return undefined; - } - }, - - getUserByEmail: async function ({ email }: { email: string }): Promise { - let response = await querier.sendGetRequest(new NormalisedURLPath("/recipe/user"), { - email, - }); - if (response.status === "OK") { - return { - ...response.user, - }; - } else { - return undefined; - } }, createResetPasswordToken: async function ({ userId, + email, }: { userId: string; + email: string; }): Promise<{ status: "OK"; token: string } | { status: "UNKNOWN_USER_ID_ERROR" }> { - let response = await querier.sendPostRequest(new NormalisedURLPath("/recipe/user/password/reset/token"), { + // the input user ID can be a recipe or a primary user ID. + return await querier.sendPostRequest(new NormalisedURLPath("/recipe/user/password/reset/token"), { userId, + email, }); - if (response.status === "OK") { - return { - status: "OK", - token: response.token, - }; - } else { - return { - status: "UNKNOWN_USER_ID_ERROR", - }; - } }, - resetPasswordUsingToken: async function ({ + consumePasswordResetToken: async function ({ token, - newPassword, }: { token: string; - newPassword: string; }): Promise< | { status: "OK"; @@ -104,37 +103,30 @@ export default function getRecipeInterface(querier: Querier): RecipeInterface { } | { status: "RESET_PASSWORD_INVALID_TOKEN_ERROR" } > { - let response = await querier.sendPostRequest(new NormalisedURLPath("/recipe/user/password/reset"), { - method: "token", + return await querier.sendPostRequest(new NormalisedURLPath("/recipe/user/password/reset/token/consume"), { token, - newPassword, }); - return response; }, updateEmailOrPassword: async function (input: { userId: string; email?: string; password?: string; - }): Promise<{ status: "OK" | "UNKNOWN_USER_ID_ERROR" | "EMAIL_ALREADY_EXISTS_ERROR" }> { - let response = await querier.sendPutRequest(new NormalisedURLPath("/recipe/user"), { + }): Promise< + | { + status: "OK" | "UNKNOWN_USER_ID_ERROR" | "EMAIL_ALREADY_EXISTS_ERROR"; + } + | { + status: "EMAIL_CHANGE_NOT_ALLOWED_ERROR"; + reason: string; + } + > { + // the input can be primary or recipe level user id. + return await querier.sendPutRequest(new NormalisedURLPath("/recipe/user"), { userId: input.userId, email: input.email, password: input.password, }); - if (response.status === "OK") { - return { - status: "OK", - }; - } else if (response.status === "EMAIL_ALREADY_EXISTS_ERROR") { - return { - status: "EMAIL_ALREADY_EXISTS_ERROR", - }; - } else { - return { - status: "UNKNOWN_USER_ID_ERROR", - }; - } }, }; } diff --git a/lib/ts/recipe/emailpassword/types.ts b/lib/ts/recipe/emailpassword/types.ts index 0510e7ffc..0fcba1e68 100644 --- a/lib/ts/recipe/emailpassword/types.ts +++ b/lib/ts/recipe/emailpassword/types.ts @@ -21,7 +21,7 @@ import { TypeInputWithService as EmailDeliveryTypeInputWithService, } from "../../ingredients/emaildelivery/types"; import EmailDeliveryIngredient from "../../ingredients/emaildelivery"; -import { GeneralErrorResponse, NormalisedAppinfo } from "../../types"; +import { GeneralErrorResponse, NormalisedAppinfo, User } from "../../types"; export type TypeNormalisedInput = { signUpFeature: TypeNormalisedInputSignUp; @@ -66,29 +66,14 @@ export type TypeNormalisedInputSignIn = { formFields: NormalisedFormField[]; }; -export type TypeInputResetPasswordUsingTokenFeature = { - /** - * @deprecated Please use emailDelivery config instead - */ - createAndSendCustomEmail?: (user: User, passwordResetURLWithToken: string, userContext: any) => Promise; -}; - export type TypeNormalisedInputResetPasswordUsingTokenFeature = { formFieldsForGenerateTokenForm: NormalisedFormField[]; formFieldsForPasswordResetForm: NormalisedFormField[]; }; -export type User = { - id: string; - recipeUserId: string; - email: string; - timeJoined: number; -}; - export type TypeInput = { signUpFeature?: TypeInputSignUp; emailDelivery?: EmailDeliveryTypeInput; - resetPasswordUsingTokenFeature?: TypeInputResetPasswordUsingTokenFeature; override?: { functions?: ( originalImplementation: RecipeInterface, @@ -103,7 +88,26 @@ export type RecipeInterface = { email: string; password: string; userContext: any; - }): Promise<{ status: "OK"; user: User } | { status: "EMAIL_ALREADY_EXISTS_ERROR" }>; + }): Promise< + | { + status: "OK"; + user: User; + } + | { status: "EMAIL_ALREADY_EXISTS_ERROR" } + >; + + // this function is meant only for creating the recipe in the core and nothing else. + createNewRecipeUser(input: { + email: string; + password: string; + userContext: any; + }): Promise< + | { + status: "OK"; + user: User; + } + | { status: "EMAIL_ALREADY_EXISTS_ERROR" } + >; signIn(input: { email: string; @@ -111,20 +115,10 @@ export type RecipeInterface = { userContext: any; }): Promise<{ status: "OK"; user: User } | { status: "WRONG_CREDENTIALS_ERROR" }>; - getUserById(input: { userId: string; userContext: any }): Promise; - - getUserByEmail(input: { email: string; userContext: any }): Promise; - /** - * We do not make email optional here cause we want to - * allow passing in primaryUserId. If we make email optional, - * and if the user provides a primaryUserId, then it may result in two problems: - * - there is no recipeUserId = input primaryUserId, in this case, - * this function will throw an error - * - There is a recipe userId = input primaryUserId, but that recipe has no email, - * or has wrong email compared to what the user wanted to generate a reset token for. - * - * And we want to allow primaryUserId being passed in. + * We pass in the email as well to this function cause the input userId + * may not be associated with an emailpassword account. In this case, we + * need to know which email to use to create an emailpassword account later on. */ createResetPasswordToken(input: { userId: string; // the id can be either recipeUserId or primaryUserId @@ -132,9 +126,8 @@ export type RecipeInterface = { userContext: any; }): Promise<{ status: "OK"; token: string } | { status: "UNKNOWN_USER_ID_ERROR" }>; - resetPasswordUsingToken(input: { + consumePasswordResetToken(input: { token: string; - newPassword: string; userContext: any; }): Promise< | { @@ -150,9 +143,15 @@ export type RecipeInterface = { email?: string; password?: string; userContext: any; - }): Promise<{ - status: "OK" | "UNKNOWN_USER_ID_ERROR" | "EMAIL_ALREADY_EXISTS_ERROR"; - }>; + }): Promise< + | { + status: "OK" | "UNKNOWN_USER_ID_ERROR" | "EMAIL_ALREADY_EXISTS_ERROR"; + } + | { + status: "EMAIL_CHANGE_NOT_ALLOWED_ERROR"; + reason: string; + } + >; }; export type APIOptions = { @@ -215,7 +214,7 @@ export type APIInterface = { | { status: "OK"; email: string; - userId: string; + user: User; } | { status: "RESET_PASSWORD_INVALID_TOKEN_ERROR"; @@ -257,7 +256,6 @@ export type APIInterface = { | { status: "OK"; user: User; - createdNewUser: boolean; session: SessionContainerInterface; } | { @@ -283,29 +281,14 @@ export type APIInterface = { }) => Promise< | { status: "OK"; - user: User; - createdNewRecipeUser: boolean; - session: SessionContainerInterface; wereAccountsAlreadyLinked: boolean; } | { - status: "RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"; - primaryUserId: string; - description: string; - } - | { - status: "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"; - primaryUserId: string; + status: "NEW_ACCOUNT_NEEDS_TO_BE_VERIFIED_ERROR" | "ACCOUNT_LINKING_NOT_ALLOWED_ERROR"; description: string; } | { - status: "ACCOUNT_LINKING_NOT_ALLOWED_ERROR"; - description: string; - } - | { - status: "ACCOUNT_NOT_VERIFIED_ERROR"; - isNotVerifiedAccountFromInputSession: boolean; - description: string; + status: "WRONG_CREDENTIALS_ERROR"; } | GeneralErrorResponse >); @@ -315,7 +298,6 @@ export type TypeEmailPasswordPasswordResetEmailDeliveryInput = { type: "PASSWORD_RESET"; user: { id: string; - recipeUserId: string; email: string; }; passwordResetLink: string; diff --git a/lib/ts/recipe/emailpassword/utils.ts b/lib/ts/recipe/emailpassword/utils.ts index 27cade323..9cd12c4a0 100644 --- a/lib/ts/recipe/emailpassword/utils.ts +++ b/lib/ts/recipe/emailpassword/utils.ts @@ -53,19 +53,11 @@ export function validateAndNormaliseUserInput( function getEmailDeliveryConfig(recipeImpl: RecipeInterface, isInServerlessEnv: boolean) { let emailService = config?.emailDelivery?.service; /** - * following code is for backward compatibility. - * if user has not passed emailDelivery config, we - * use the createAndSendCustomEmail config. If the user - * has not passed even that config, we use the default + * If the user has not passed even that config, we use the default * createAndSendCustomEmail implementation which calls our supertokens API */ if (emailService === undefined) { - emailService = new BackwardCompatibilityService( - recipeImpl, - appInfo, - isInServerlessEnv, - config?.resetPasswordUsingTokenFeature - ); + emailService = new BackwardCompatibilityService(recipeImpl, appInfo, isInServerlessEnv); } return { ...config?.emailDelivery, diff --git a/lib/ts/recipe/thirdpartyemailpassword/emaildelivery/services/backwardCompatibility/index.ts b/lib/ts/recipe/thirdpartyemailpassword/emaildelivery/services/backwardCompatibility/index.ts index e6c6f07b1..c585bcb58 100644 --- a/lib/ts/recipe/thirdpartyemailpassword/emaildelivery/services/backwardCompatibility/index.ts +++ b/lib/ts/recipe/thirdpartyemailpassword/emaildelivery/services/backwardCompatibility/index.ts @@ -12,7 +12,7 @@ * License for the specific language governing permissions and limitations * under the License. */ -import { TypeThirdPartyEmailPasswordEmailDeliveryInput, User } from "../../../types"; +import { TypeThirdPartyEmailPasswordEmailDeliveryInput } from "../../../types"; import { RecipeInterface as EmailPasswordRecipeInterface } from "../../../../emailpassword"; import { NormalisedAppinfo } from "../../../../../types"; import EmailPasswordBackwardCompatibilityService from "../../../../emailpassword/emaildelivery/services/backwardCompatibility"; @@ -25,21 +25,13 @@ export default class BackwardCompatibilityService constructor( emailPasswordRecipeInterfaceImpl: EmailPasswordRecipeInterface, appInfo: NormalisedAppinfo, - isInServerlessEnv: boolean, - resetPasswordUsingTokenFeature?: { - createAndSendCustomEmail?: ( - user: User, - passwordResetURLWithToken: string, - userContext: any - ) => Promise; - } + isInServerlessEnv: boolean ) { { this.emailPasswordBackwardCompatibilityService = new EmailPasswordBackwardCompatibilityService( emailPasswordRecipeInterfaceImpl, appInfo, - isInServerlessEnv, - resetPasswordUsingTokenFeature + isInServerlessEnv ); } } diff --git a/lib/ts/recipe/thirdpartyemailpassword/index.ts b/lib/ts/recipe/thirdpartyemailpassword/index.ts index 17cc46039..87b4ab4d7 100644 --- a/lib/ts/recipe/thirdpartyemailpassword/index.ts +++ b/lib/ts/recipe/thirdpartyemailpassword/index.ts @@ -58,14 +58,6 @@ export default class Wrapper { }); } - static getUserById(userId: string, userContext: any = {}) { - return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.getUserById({ userId, userContext }); - } - - static getUsersByEmail(email: string, userContext: any = {}) { - return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.getUsersByEmail({ email, userContext }); - } - static createResetPasswordToken(userId: string, email: string, userContext: any = {}) { return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.createResetPasswordToken({ userId, @@ -74,10 +66,9 @@ export default class Wrapper { }); } - static resetPasswordUsingToken(token: string, newPassword: string, userContext: any = {}) { - return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.resetPasswordUsingToken({ + static consumePasswordResetToken(token: string, userContext: any = {}) { + return Recipe.getInstanceOrThrowError().recipeInterfaceImpl.consumePasswordResetToken({ token, - newPassword, userContext, }); } @@ -123,15 +114,11 @@ export let emailPasswordSignIn = Wrapper.emailPasswordSignIn; export let thirdPartySignInUp = Wrapper.thirdPartySignInUp; -export let getUserById = Wrapper.getUserById; - export let getUserByThirdPartyInfo = Wrapper.getUserByThirdPartyInfo; -export let getUsersByEmail = Wrapper.getUsersByEmail; - export let createResetPasswordToken = Wrapper.createResetPasswordToken; -export let resetPasswordUsingToken = Wrapper.resetPasswordUsingToken; +export let consumePasswordResetToken = Wrapper.consumePasswordResetToken; export let updateEmailOrPassword = Wrapper.updateEmailOrPassword; diff --git a/lib/ts/recipe/thirdpartyemailpassword/recipe.ts b/lib/ts/recipe/thirdpartyemailpassword/recipe.ts index d796181de..38779aa7a 100644 --- a/lib/ts/recipe/thirdpartyemailpassword/recipe.ts +++ b/lib/ts/recipe/thirdpartyemailpassword/recipe.ts @@ -118,7 +118,6 @@ export default class Recipe extends RecipeModule { signUpFeature: { formFields: this.config.signUpFeature.formFields, }, - resetPasswordUsingTokenFeature: this.config.resetPasswordUsingTokenFeature, }, { emailDelivery: this.emailDelivery, diff --git a/lib/ts/recipe/thirdpartyemailpassword/recipeImplementation/emailPasswordRecipeImplementation.ts b/lib/ts/recipe/thirdpartyemailpassword/recipeImplementation/emailPasswordRecipeImplementation.ts index fa68edf9b..e9b7afa3b 100644 --- a/lib/ts/recipe/thirdpartyemailpassword/recipeImplementation/emailPasswordRecipeImplementation.ts +++ b/lib/ts/recipe/thirdpartyemailpassword/recipeImplementation/emailPasswordRecipeImplementation.ts @@ -1,4 +1,5 @@ -import { RecipeInterface, User } from "../../emailpassword/types"; +import { RecipeInterface } from "../../emailpassword/types"; +import { User } from "../../../types"; import { RecipeInterface as ThirdPartyEmailPasswordRecipeInterface } from "../types"; export default function getRecipeInterface(recipeInterface: ThirdPartyEmailPasswordRecipeInterface): RecipeInterface { @@ -19,25 +20,6 @@ export default function getRecipeInterface(recipeInterface: ThirdPartyEmailPassw return recipeInterface.emailPasswordSignIn(input); }, - getUserById: async function (input: { userId: string; userContext: any }): Promise { - let user = await recipeInterface.getUserById(input); - if (user === undefined || user.thirdParty !== undefined) { - // either user is undefined or it's a thirdparty user. - return undefined; - } - return user; - }, - - getUserByEmail: async function (input: { email: string; userContext: any }): Promise { - let result = await recipeInterface.getUsersByEmail(input); - for (let i = 0; i < result.length; i++) { - if (result[i].thirdParty === undefined) { - return result[i]; - } - } - return undefined; - }, - createResetPasswordToken: async function (input: { userId: string; email: string; @@ -46,8 +28,22 @@ export default function getRecipeInterface(recipeInterface: ThirdPartyEmailPassw return recipeInterface.createResetPasswordToken(input); }, - resetPasswordUsingToken: async function (input: { token: string; newPassword: string; userContext: any }) { - return recipeInterface.resetPasswordUsingToken(input); + consumePasswordResetToken: async function (input: { token: string; userContext: any }) { + return recipeInterface.consumePasswordResetToken(input); + }, + + createNewRecipeUser: async function (input: { + email: string; + password: string; + userContext: any; + }): Promise< + | { + status: "OK"; + user: User; + } + | { status: "EMAIL_ALREADY_EXISTS_ERROR" } + > { + return recipeInterface.createNewEmailPasswordRecipeUser(input); }, updateEmailOrPassword: async function (input: { @@ -55,7 +51,15 @@ export default function getRecipeInterface(recipeInterface: ThirdPartyEmailPassw email?: string; password?: string; userContext: any; - }): Promise<{ status: "OK" | "UNKNOWN_USER_ID_ERROR" | "EMAIL_ALREADY_EXISTS_ERROR" }> { + }): Promise< + | { + status: "OK" | "UNKNOWN_USER_ID_ERROR" | "EMAIL_ALREADY_EXISTS_ERROR"; + } + | { + status: "EMAIL_CHANGE_NOT_ALLOWED_ERROR"; + reason: string; + } + > { return recipeInterface.updateEmailOrPassword(input); }, }; diff --git a/lib/ts/recipe/thirdpartyemailpassword/recipeImplementation/index.ts b/lib/ts/recipe/thirdpartyemailpassword/recipeImplementation/index.ts index d61f91a64..913cafa6f 100644 --- a/lib/ts/recipe/thirdpartyemailpassword/recipeImplementation/index.ts +++ b/lib/ts/recipe/thirdpartyemailpassword/recipeImplementation/index.ts @@ -6,6 +6,8 @@ import { RecipeInterface as ThirdPartyRecipeInterface } from "../../thirdparty"; import { Querier } from "../../../querier"; import DerivedEP from "./emailPasswordRecipeImplementation"; import DerivedTP from "./thirdPartyRecipeImplementation"; +import { User as GlobalUser } from "../../../types"; +import { getUser } from "../../../"; export default function getRecipeInterface( emailPasswordQuerier: Querier, @@ -22,7 +24,7 @@ export default function getRecipeInterface( email: string; password: string; userContext: any; - }): Promise<{ status: "OK"; user: User } | { status: "EMAIL_ALREADY_EXISTS_ERROR" }> { + }): Promise<{ status: "OK"; user: GlobalUser } | { status: "EMAIL_ALREADY_EXISTS_ERROR" }> { return await originalEmailPasswordImplementation.signUp.bind(DerivedEP(this))(input); }, @@ -30,7 +32,7 @@ export default function getRecipeInterface( email: string; password: string; userContext: any; - }): Promise<{ status: "OK"; user: User } | { status: "WRONG_CREDENTIALS_ERROR" }> { + }): Promise<{ status: "OK"; user: GlobalUser } | { status: "WRONG_CREDENTIALS_ERROR" }> { return originalEmailPasswordImplementation.signIn.bind(DerivedEP(this))(input); }, @@ -46,37 +48,6 @@ export default function getRecipeInterface( return originalThirdPartyImplementation.signInUp.bind(DerivedTP(this))(input); }, - getUserById: async function (input: { userId: string; userContext: any }): Promise { - let user: User | undefined = await originalEmailPasswordImplementation.getUserById.bind(DerivedEP(this))( - input - ); - if (user !== undefined) { - return user; - } - if (originalThirdPartyImplementation === undefined) { - return undefined; - } - return await originalThirdPartyImplementation.getUserById.bind(DerivedTP(this))(input); - }, - - getUsersByEmail: async function ({ email, userContext }: { email: string; userContext: any }): Promise { - let userFromEmailPass: User | undefined = await originalEmailPasswordImplementation.getUserByEmail.bind( - DerivedEP(this) - )({ email, userContext }); - - if (originalThirdPartyImplementation === undefined) { - return userFromEmailPass === undefined ? [] : [userFromEmailPass]; - } - let usersFromThirdParty: User[] = await originalThirdPartyImplementation.getUsersByEmail.bind( - DerivedTP(this) - )({ email, userContext }); - - if (userFromEmailPass !== undefined) { - return [...usersFromThirdParty, userFromEmailPass]; - } - return usersFromThirdParty; - }, - getUserByThirdPartyInfo: async function (input: { thirdPartyId: string; thirdPartyUserId: string; @@ -96,8 +67,22 @@ export default function getRecipeInterface( return originalEmailPasswordImplementation.createResetPasswordToken.bind(DerivedEP(this))(input); }, - resetPasswordUsingToken: async function (input: { token: string; newPassword: string; userContext: any }) { - return originalEmailPasswordImplementation.resetPasswordUsingToken.bind(DerivedEP(this))(input); + consumePasswordResetToken: async function (input: { token: string; userContext: any }) { + return originalEmailPasswordImplementation.consumePasswordResetToken.bind(DerivedEP(this))(input); + }, + + createNewEmailPasswordRecipeUser: async function (input: { + email: string; + password: string; + userContext: any; + }): Promise< + | { + status: "OK"; + user: GlobalUser; + } + | { status: "EMAIL_ALREADY_EXISTS_ERROR" } + > { + return originalEmailPasswordImplementation.createNewRecipeUser.bind(DerivedEP(this))(input); }, updateEmailOrPassword: async function ( @@ -108,15 +93,27 @@ export default function getRecipeInterface( password?: string; userContext: any; } - ): Promise<{ - status: "OK" | "UNKNOWN_USER_ID_ERROR" | "EMAIL_ALREADY_EXISTS_ERROR"; - }> { - let user = await this.getUserById({ userId: input.userId, userContext: input.userContext }); + ): Promise< + | { + status: "OK" | "UNKNOWN_USER_ID_ERROR" | "EMAIL_ALREADY_EXISTS_ERROR"; + } + | { + status: "EMAIL_CHANGE_NOT_ALLOWED_ERROR"; + reason: string; + } + > { + let user = await getUser(input.userId, input.userContext); if (user === undefined) { return { status: "UNKNOWN_USER_ID_ERROR", }; - } else if (user.thirdParty !== undefined) { + } + let emailPasswordUserExists = + user.loginMethods.find((lM) => { + lM.recipeId === "emailpassword"; + }) !== undefined; + + if (!emailPasswordUserExists) { throw new Error("Cannot update email or password of a user who signed up using third party login."); } return originalEmailPasswordImplementation.updateEmailOrPassword.bind(DerivedEP(this))(input); diff --git a/lib/ts/recipe/thirdpartyemailpassword/recipeImplementation/thirdPartyRecipeImplementation.ts b/lib/ts/recipe/thirdpartyemailpassword/recipeImplementation/thirdPartyRecipeImplementation.ts index 75a8f40e7..176b0b316 100644 --- a/lib/ts/recipe/thirdpartyemailpassword/recipeImplementation/thirdPartyRecipeImplementation.ts +++ b/lib/ts/recipe/thirdpartyemailpassword/recipeImplementation/thirdPartyRecipeImplementation.ts @@ -44,28 +44,12 @@ export default function getRecipeInterface(recipeInterface: ThirdPartyEmailPassw }; }, - getUserById: async function (input: { userId: string; userContext: any }): Promise { - let user = await recipeInterface.getUserById(input); - if (user === undefined || user.thirdParty === undefined) { - // either user is undefined or it's an email password user. - return undefined; - } - return { - email: user.email, - id: user.id, - recipeUserId: user.recipeUserId, - timeJoined: user.timeJoined, - thirdParty: user.thirdParty, - }; + getUserById: async function (_: { userId: string; userContext: any }): Promise { + throw new Error("This will be removed.."); }, - getUsersByEmail: async function (input: { email: string; userContext: any }): Promise { - let users = await recipeInterface.getUsersByEmail(input); - - // we filter out all non thirdparty users. - return users.filter((u) => { - return u.thirdParty !== undefined; - }) as User[]; + getUsersByEmail: async function (_: { email: string; userContext: any }): Promise { + throw new Error("This will be removed.."); }, }; } diff --git a/lib/ts/recipe/thirdpartyemailpassword/types.ts b/lib/ts/recipe/thirdpartyemailpassword/types.ts index 68e2fa290..70d5fd12a 100644 --- a/lib/ts/recipe/thirdpartyemailpassword/types.ts +++ b/lib/ts/recipe/thirdpartyemailpassword/types.ts @@ -15,9 +15,7 @@ import { TypeProvider, APIOptions as ThirdPartyAPIOptionsOriginal } from "../thirdparty/types"; import { NormalisedFormField, - TypeFormField, TypeInputFormField, - TypeInputResetPasswordUsingTokenFeature, APIOptions as EmailPasswordAPIOptionsOriginal, TypeEmailPasswordEmailDeliveryInput, RecipeInterface as EPRecipeInterface, @@ -28,7 +26,7 @@ import { TypeInput as EmailDeliveryTypeInput, TypeInputWithService as EmailDeliveryTypeInputWithService, } from "../../ingredients/emaildelivery/types"; -import { GeneralErrorResponse } from "../../types"; +import { GeneralErrorResponse, User as GlobalUser } from "../../types"; export type User = { id: string; @@ -41,20 +39,6 @@ export type User = { }; }; -export type TypeContextEmailPasswordSignUp = { - loginType: "emailpassword"; - formFields: TypeFormField[]; -}; - -export type TypeContextEmailPasswordSignIn = { - loginType: "emailpassword"; -}; - -export type TypeContextThirdParty = { - loginType: "thirdparty"; - thirdPartyAuthCodeResponse: any; -}; - export type TypeInputSignUp = { formFields?: TypeInputFormField[]; }; @@ -67,7 +51,6 @@ export type TypeInput = { signUpFeature?: TypeInputSignUp; providers?: TypeProvider[]; emailDelivery?: EmailDeliveryTypeInput; - resetPasswordUsingTokenFeature?: TypeInputResetPasswordUsingTokenFeature; override?: { functions?: ( originalImplementation: RecipeInterface, @@ -84,7 +67,6 @@ export type TypeNormalisedInput = { emailPasswordRecipeImpl: EPRecipeInterface, isInServerlessEnv: boolean ) => EmailDeliveryTypeInputWithService; - resetPasswordUsingTokenFeature?: TypeInputResetPasswordUsingTokenFeature; override: { functions: ( originalImplementation: RecipeInterface, @@ -95,10 +77,6 @@ export type TypeNormalisedInput = { }; export type RecipeInterface = { - getUserById(input: { userId: string; userContext: any }): Promise; - - getUsersByEmail(input: { email: string; userContext: any }): Promise; - getUserByThirdPartyInfo(input: { thirdPartyId: string; thirdPartyUserId: string; @@ -116,13 +94,25 @@ export type RecipeInterface = { email: string; password: string; userContext: any; - }): Promise<{ status: "OK"; user: User } | { status: "EMAIL_ALREADY_EXISTS_ERROR" }>; + }): Promise<{ status: "OK"; user: GlobalUser } | { status: "EMAIL_ALREADY_EXISTS_ERROR" }>; + + createNewEmailPasswordRecipeUser(input: { + email: string; + password: string; + userContext: any; + }): Promise< + | { + status: "OK"; + user: GlobalUser; + } + | { status: "EMAIL_ALREADY_EXISTS_ERROR" } + >; emailPasswordSignIn(input: { email: string; password: string; userContext: any; - }): Promise<{ status: "OK"; user: User } | { status: "WRONG_CREDENTIALS_ERROR" }>; + }): Promise<{ status: "OK"; user: GlobalUser } | { status: "WRONG_CREDENTIALS_ERROR" }>; createResetPasswordToken(input: { userId: string; @@ -130,9 +120,8 @@ export type RecipeInterface = { userContext: any; }): Promise<{ status: "OK"; token: string } | { status: "UNKNOWN_USER_ID_ERROR" }>; - resetPasswordUsingToken(input: { + consumePasswordResetToken(input: { token: string; - newPassword: string; userContext: any; }): Promise< | { @@ -148,9 +137,15 @@ export type RecipeInterface = { email?: string; password?: string; userContext: any; - }): Promise<{ - status: "OK" | "UNKNOWN_USER_ID_ERROR" | "EMAIL_ALREADY_EXISTS_ERROR"; - }>; + }): Promise< + | { + status: "OK" | "UNKNOWN_USER_ID_ERROR" | "EMAIL_ALREADY_EXISTS_ERROR"; + } + | { + status: "EMAIL_CHANGE_NOT_ALLOWED_ERROR"; + reason: string; + } + >; }; export type EmailPasswordAPIOptions = EmailPasswordAPIOptionsOriginal; @@ -261,7 +256,7 @@ export type APIInterface = { | { status: "OK"; email: string; - userId: string; + user: GlobalUser; } | { status: "RESET_PASSWORD_INVALID_TOKEN_ERROR"; @@ -316,29 +311,14 @@ export type APIInterface = { }) => Promise< | { status: "OK"; - user: User; - createdNewRecipeUser: boolean; - session: SessionContainerInterface; wereAccountsAlreadyLinked: boolean; } | { - status: "RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"; - primaryUserId: string; - description: string; - } - | { - status: "ACCOUNT_INFO_ALREADY_ASSOCIATED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR"; - primaryUserId: string; - description: string; - } - | { - status: "ACCOUNT_LINKING_NOT_ALLOWED_ERROR"; + status: "NEW_ACCOUNT_NEEDS_TO_BE_VERIFIED_ERROR" | "ACCOUNT_LINKING_NOT_ALLOWED_ERROR"; description: string; } | { - status: "ACCOUNT_NOT_VERIFIED_ERROR"; - isNotVerifiedAccountFromInputSession: boolean; - description: string; + status: "WRONG_CREDENTIALS_ERROR"; } | GeneralErrorResponse >); @@ -354,7 +334,7 @@ export type APIInterface = { }) => Promise< | { status: "OK"; - user: User; + user: GlobalUser; session: SessionContainerInterface; } | { @@ -375,8 +355,7 @@ export type APIInterface = { }) => Promise< | { status: "OK"; - user: User; - createdNewUser: boolean; + user: GlobalUser; session: SessionContainerInterface; } | { diff --git a/lib/ts/recipe/thirdpartyemailpassword/utils.ts b/lib/ts/recipe/thirdpartyemailpassword/utils.ts index 2c1577768..b27c6239d 100644 --- a/lib/ts/recipe/thirdpartyemailpassword/utils.ts +++ b/lib/ts/recipe/thirdpartyemailpassword/utils.ts @@ -33,8 +33,6 @@ export function validateAndNormaliseUserInput( config === undefined ? undefined : config.signUpFeature ); - let resetPasswordUsingTokenFeature = config === undefined ? undefined : config.resetPasswordUsingTokenFeature; - let providers = config === undefined || config.providers === undefined ? [] : config.providers; let override = { @@ -53,12 +51,7 @@ export function validateAndNormaliseUserInput( * createAndSendCustomEmail implementation */ if (emailService === undefined) { - emailService = new BackwardCompatibilityService( - emailPasswordRecipeImpl, - appInfo, - isInServerlessEnv, - config?.resetPasswordUsingTokenFeature - ); + emailService = new BackwardCompatibilityService(emailPasswordRecipeImpl, appInfo, isInServerlessEnv); } return { ...config?.emailDelivery, @@ -82,7 +75,6 @@ export function validateAndNormaliseUserInput( getEmailDeliveryConfig, signUpFeature, providers, - resetPasswordUsingTokenFeature, }; } diff --git a/lib/ts/supertokens.ts b/lib/ts/supertokens.ts index 3e6243fab..19fbb59dc 100644 --- a/lib/ts/supertokens.ts +++ b/lib/ts/supertokens.ts @@ -33,15 +33,6 @@ import STError from "./error"; import { logDebugMessage } from "./logger"; import { PostSuperTokensInitCallbacks } from "./postSuperTokensInitCallbacks"; import AccountLinking from "./recipe/accountlinking/recipe"; -import { RecipeLevelUser } from "./recipe/accountlinking/types"; -import EmailPasswordRecipe from "./recipe/emailpassword/recipe"; -import ThirdPartyRecipe from "./recipe/thirdparty/recipe"; -import PasswordlessRecipe from "./recipe/passwordless/recipe"; -import EmailPassword from "./recipe/emailpassword"; -import ThirdParty from "./recipe/thirdparty"; -import Passwordless from "./recipe/passwordless"; -import ThirdPartyEmailPassword from "./recipe/thirdpartyemailpassword"; -import ThirdPartyPasswordless from "./recipe/thirdpartypasswordless"; export default class SuperTokens { private static instance: SuperTokens | undefined; @@ -405,143 +396,4 @@ export default class SuperTokens { } throw err; }; - - // this is an internal use function, therefore it is prefixed with an `_` - _getUserForRecipeId = async ( - userId: string, - recipeId: string - ): Promise<{ - user: RecipeLevelUser | undefined; - recipe: - | "emailpassword" - | "thirdparty" - | "passwordless" - | "thirdpartyemailpassword" - | "thirdpartypasswordless" - | undefined; - }> => { - let user: RecipeLevelUser | undefined; - let recipe: - | "emailpassword" - | "thirdparty" - | "passwordless" - | "thirdpartyemailpassword" - | "thirdpartypasswordless" - | undefined; - - if (recipeId === EmailPasswordRecipe.RECIPE_ID) { - try { - const userResponse = await EmailPassword.getUserById(userId); - - if (userResponse !== undefined) { - user = { - ...userResponse, - recipeId: "emailpassword", - }; - recipe = "emailpassword"; - } - } catch (e) { - // No - op - } - - if (user === undefined) { - try { - const userResponse = await ThirdPartyEmailPassword.getUserById(userId); - - if (userResponse !== undefined) { - user = { - ...userResponse, - recipeId: "emailpassword", - }; - recipe = "thirdpartyemailpassword"; - } - } catch (e) { - // No - op - } - } - } else if (recipeId === ThirdPartyRecipe.RECIPE_ID) { - try { - const userResponse = await ThirdParty.getUserById(userId); - - if (userResponse !== undefined) { - user = { - ...userResponse, - recipeId: "thirdparty", - }; - recipe = "thirdparty"; - } - } catch (e) { - // No - op - } - - if (user === undefined) { - try { - const userResponse = await ThirdPartyEmailPassword.getUserById(userId); - - if (userResponse !== undefined) { - user = { - ...userResponse, - recipeId: "thirdparty", - }; - recipe = "thirdpartyemailpassword"; - } - } catch (e) { - // No - op - } - } - - if (user === undefined) { - try { - const userResponse = await ThirdPartyPasswordless.getUserById(userId); - - if (userResponse !== undefined) { - user = { - ...userResponse, - recipeId: "thirdparty", - }; - recipe = "thirdpartypasswordless"; - } - } catch (e) { - // No - op - } - } - } else if (recipeId === PasswordlessRecipe.RECIPE_ID) { - try { - const userResponse = await Passwordless.getUserById({ - userId, - }); - - if (userResponse !== undefined) { - user = { - ...userResponse, - recipeId: "passwordless", - }; - recipe = "passwordless"; - } - } catch (e) { - // No - op - } - - if (user === undefined) { - try { - const userResponse = await ThirdPartyPasswordless.getUserById(userId); - - if (userResponse !== undefined) { - user = { - ...userResponse, - recipeId: "passwordless", - }; - recipe = "thirdpartypasswordless"; - } - } catch (e) { - // No - op - } - } - } - - return { - user, - recipe, - }; - }; } diff --git a/test/with-typescript/index.ts b/test/with-typescript/index.ts index 3235a335d..4762e932d 100644 --- a/test/with-typescript/index.ts +++ b/test/with-typescript/index.ts @@ -1064,10 +1064,14 @@ Supertokens.init({ // we check if the email exists in SuperTokens. If not, // then the sign in should be handled by you. if ( - (await supertokensImpl.getUserByEmail({ - email: input.email, - userContext: input.userContext, - })) === undefined + ( + await Supertokens.listUsersByAccountInfo( + { + email: input.email, + }, + input.userContext + ) + ).length === 0 ) { // TODO: sign in from your db // example return value if credentials don't match @@ -1082,24 +1086,6 @@ Supertokens.init({ // all new users are created in SuperTokens; return supertokensImpl.signUp(input); }, - getUserByEmail: async (input) => { - let superTokensUser = await supertokensImpl.getUserByEmail(input); - if (superTokensUser === undefined) { - let email = input.email; - // TODO: fetch and return user info from your database... - } else { - return superTokensUser; - } - }, - getUserById: async (input) => { - let superTokensUser = await supertokensImpl.getUserById(input); - if (superTokensUser === undefined) { - let userId = input.userId; - // TODO: fetch and return user info from your database... - } else { - return superTokensUser; - } - }, }; }, apis: (oI) => { @@ -1279,7 +1265,6 @@ ThirdPartyEmailPassword.sendEmail({ user: { email: "", id: "", - recipeUserId: "", }, }); ThirdPartyEmailPassword.sendEmail({ @@ -1288,7 +1273,6 @@ ThirdPartyEmailPassword.sendEmail({ user: { email: "", id: "", - recipeUserId: "", }, userContext: {}, });