From 9fb35867d726af68f0b341b0cc9b172ea695e87f Mon Sep 17 00:00:00 2001 From: Rishabh Poddar Date: Wed, 1 Mar 2023 13:43:06 +0530 Subject: [PATCH] account linking: fixes bugs and refactors (#498) * fixes bugs and refactors * small refactors and fixes * small refactor * small type changes * review changes --- lib/build/recipe/accountlinking/index.d.ts | 4 +- lib/build/recipe/accountlinking/index.js | 8 +- lib/build/recipe/accountlinking/recipe.d.ts | 18 +- lib/build/recipe/accountlinking/recipe.js | 463 ++++++++++------- .../accountlinking/recipeImplementation.js | 45 +- lib/build/recipe/accountlinking/types.d.ts | 3 +- lib/build/recipe/dashboard/types.d.ts | 1 - lib/ts/recipe/accountlinking/index.ts | 6 +- lib/ts/recipe/accountlinking/recipe.ts | 481 +++++++++++------- .../accountlinking/recipeImplementation.ts | 53 +- lib/ts/recipe/accountlinking/types.ts | 3 +- lib/ts/recipe/dashboard/types.ts | 1 - 12 files changed, 673 insertions(+), 413 deletions(-) diff --git a/lib/build/recipe/accountlinking/index.d.ts b/lib/build/recipe/accountlinking/index.d.ts index 16e13235f..e0370758c 100644 --- a/lib/build/recipe/accountlinking/index.d.ts +++ b/lib/build/recipe/accountlinking/index.d.ts @@ -9,7 +9,7 @@ export default class Wrapper { ): Promise<{ [primaryUserId: string]: string[]; }>; - static getPrimaryUserIdsforRecipeUserIds( + static getPrimaryUserIdsForRecipeUserIds( recipeUserIds: string[], userContext?: any ): Promise<{ @@ -127,7 +127,7 @@ export default class Wrapper { } export declare const init: typeof Recipe.init; export declare const getRecipeUserIdsForPrimaryUserIds: typeof Wrapper.getRecipeUserIdsForPrimaryUserIds; -export declare const getPrimaryUserIdsforRecipeUserIds: typeof Wrapper.getPrimaryUserIdsforRecipeUserIds; +export declare const getPrimaryUserIdsForRecipeUserIds: typeof Wrapper.getPrimaryUserIdsForRecipeUserIds; export declare const addNewRecipeUserIdWithoutPrimaryUserId: typeof Wrapper.addNewRecipeUserIdWithoutPrimaryUserId; export declare const canCreatePrimaryUserId: typeof Wrapper.canCreatePrimaryUserId; export declare const createPrimaryUser: typeof Wrapper.createPrimaryUser; diff --git a/lib/build/recipe/accountlinking/index.js b/lib/build/recipe/accountlinking/index.js index 4002928ef..0cc4e1b7d 100644 --- a/lib/build/recipe/accountlinking/index.js +++ b/lib/build/recipe/accountlinking/index.js @@ -50,7 +50,7 @@ var __importDefault = return mod && mod.__esModule ? mod : { default: mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); -exports.storeIntoAccountToLinkTable = exports.fetchFromAccountToLinkTable = exports.unlinkAccounts = exports.linkAccounts = exports.canLinkAccounts = exports.createPrimaryUser = exports.canCreatePrimaryUserId = exports.addNewRecipeUserIdWithoutPrimaryUserId = exports.getPrimaryUserIdsforRecipeUserIds = exports.getRecipeUserIdsForPrimaryUserIds = exports.init = void 0; +exports.storeIntoAccountToLinkTable = exports.fetchFromAccountToLinkTable = exports.unlinkAccounts = exports.linkAccounts = exports.canLinkAccounts = exports.createPrimaryUser = exports.canCreatePrimaryUserId = exports.addNewRecipeUserIdWithoutPrimaryUserId = exports.getPrimaryUserIdsForRecipeUserIds = exports.getRecipeUserIdsForPrimaryUserIds = exports.init = void 0; const recipe_1 = __importDefault(require("./recipe")); class Wrapper { static getRecipeUserIdsForPrimaryUserIds(primaryUserIds, userContext) { @@ -63,11 +63,11 @@ class Wrapper { }); }); } - static getPrimaryUserIdsforRecipeUserIds(recipeUserIds, userContext) { + static getPrimaryUserIdsForRecipeUserIds(recipeUserIds, userContext) { return __awaiter(this, void 0, void 0, function* () { return yield recipe_1.default .getInstanceOrThrowError() - .recipeInterfaceImpl.getPrimaryUserIdsforRecipeUserIds({ + .recipeInterfaceImpl.getPrimaryUserIdsForRecipeUserIds({ recipeUserIds, userContext: userContext === undefined ? {} : userContext, }); @@ -151,7 +151,7 @@ exports.default = Wrapper; Wrapper.init = recipe_1.default.init; exports.init = Wrapper.init; exports.getRecipeUserIdsForPrimaryUserIds = Wrapper.getRecipeUserIdsForPrimaryUserIds; -exports.getPrimaryUserIdsforRecipeUserIds = Wrapper.getPrimaryUserIdsforRecipeUserIds; +exports.getPrimaryUserIdsForRecipeUserIds = Wrapper.getPrimaryUserIdsForRecipeUserIds; exports.addNewRecipeUserIdWithoutPrimaryUserId = Wrapper.addNewRecipeUserIdWithoutPrimaryUserId; exports.canCreatePrimaryUserId = Wrapper.canCreatePrimaryUserId; exports.createPrimaryUser = Wrapper.createPrimaryUser; diff --git a/lib/build/recipe/accountlinking/recipe.d.ts b/lib/build/recipe/accountlinking/recipe.d.ts index d266a4e4c..4e09c8b71 100644 --- a/lib/build/recipe/accountlinking/recipe.d.ts +++ b/lib/build/recipe/accountlinking/recipe.d.ts @@ -74,25 +74,29 @@ export default class Recipe extends RecipeModule { }) => Promise; accountLinkPostSignInViaSession: ({ session, - info, - infoVerified, + newUser, + newUserVerified, userContext, }: { session: SessionContainer; - info: AccountInfoAndEmailWithRecipeId; - infoVerified: boolean; + newUser: AccountInfoAndEmailWithRecipeId; + newUserVerified: boolean; userContext: any; }) => Promise< | { createRecipeUser: true; - updateVerificationClaim: boolean; + updateAccountLinkingClaim: "ADD_CLAIM" | "NO_CHANGE"; } | ({ createRecipeUser: false; } & ( | { accountsLinked: true; - updateVerificationClaim: boolean; + updateAccountLinkingClaim: "REMOVE_CLAIM"; + } + | { + accountsLinked: false; + updateAccountLinkingClaim: "ADD_CLAIM"; } | { accountsLinked: false; @@ -117,7 +121,7 @@ export default class Recipe extends RecipeModule { recipeUserId: string; userContext: any; }) => Promise; - createPrimaryUserIdOrLinkAccounts: ({ + createPrimaryUserIdOrLinkAccountsAfterEmailVerification: ({ recipeUserId, session, userContext, diff --git a/lib/build/recipe/accountlinking/recipe.js b/lib/build/recipe/accountlinking/recipe.js index 0104a8e38..e9d637996 100644 --- a/lib/build/recipe/accountlinking/recipe.js +++ b/lib/build/recipe/accountlinking/recipe.js @@ -53,7 +53,6 @@ Object.defineProperty(exports, "__esModule", { value: true }); const recipeModule_1 = __importDefault(require("../../recipeModule")); const utils_1 = require("./utils"); const __1 = require("../.."); -const supertokens_1 = __importDefault(require("../../supertokens")); const supertokens_js_override_1 = __importDefault(require("supertokens-js-override")); const recipeImplementation_1 = __importDefault(require("./recipeImplementation")); const querier_1 = require("../../querier"); @@ -167,11 +166,11 @@ class Recipe extends recipeModule_1.default { this.markEmailAsVerified = ({ email, recipeUserId, userContext }) => __awaiter(this, void 0, void 0, function* () { const emailVerificationInstance = recipe_1.default.getInstance(); - if (emailVerificationInstance) { + if (emailVerificationInstance !== undefined) { const tokenResponse = yield emailVerificationInstance.recipeInterfaceImpl.createEmailVerificationToken( { userId: recipeUserId, - email: email, + email, userContext, } ); @@ -282,104 +281,142 @@ class Recipe extends recipeModule_1.default { return primaryUser.id; } }); - this.accountLinkPostSignInViaSession = ({ session, info, infoVerified, userContext }) => + this.accountLinkPostSignInViaSession = ({ session, newUser, newUserVerified, userContext }) => __awaiter(this, void 0, void 0, function* () { let userId = session.getUserId(); - let user = yield this.recipeInterfaceImpl.getUser({ + let existingUser = yield this.recipeInterfaceImpl.getUser({ userId, userContext, }); - if (user === undefined) { - throw Error("this should not be thrown"); + if (existingUser === undefined) { + // this can come here if the user ID in the session belongs to a user + // that is not recognized by SuperTokens. In this case, we + // disallow this kind of operation. + return { + createRecipeUser: false, + accountsLinked: false, + reason: "ACCOUNT_LINKING_NOT_ALLOWED_ERROR", + }; } /** * checking if the user with existing session * is a primary user or not */ - if (!user.isPrimaryUser) { - let shouldDoAccountLinking = yield this.config.shouldDoAutomaticAccountLinking( - info, + if (!existingUser.isPrimaryUser) { + // first we check if the newUser should be a candidate for account linking + const shouldDoAccountLinkingOfNewUser = yield this.config.shouldDoAutomaticAccountLinking( + newUser, undefined, session, userContext ); - if (!shouldDoAccountLinking.shouldAutomaticallyLink) { + if (!shouldDoAccountLinkingOfNewUser.shouldAutomaticallyLink) { return { createRecipeUser: false, accountsLinked: false, reason: "ACCOUNT_LINKING_NOT_ALLOWED_ERROR", }; } - let recipeId = user.loginMethods[0].recipeId; - let recipeUser = yield supertokens_1.default - .getInstanceOrThrowError() - ._getUserForRecipeId(user.id, recipeId); - if (recipeUser.user === undefined) { - throw Error( - "This error should never be thrown. Check for bug in `getUserForRecipeId` function" - ); - } - shouldDoAccountLinking = yield this.config.shouldDoAutomaticAccountLinking( - recipeUser.user, + // Now we ask the user if the existing login method can be linked to anything + // (since it's not a primary user) + // here we can use the index of 0 cause the existingUser is not a primary user, + // therefore it will only have one login method in the loginMethods' array. + let existingUserAccountInfoAndEmailWithRecipeId = { + recipeId: existingUser.loginMethods[0].recipeId, + email: existingUser.loginMethods[0].email, + phoneNumber: existingUser.loginMethods[0].phoneNumber, + thirdParty: existingUser.loginMethods[0].thirdParty, + }; + const shouldDoAccountLinkingOfExistingUser = yield this.config.shouldDoAutomaticAccountLinking( + existingUserAccountInfoAndEmailWithRecipeId, undefined, session, userContext ); - if (!shouldDoAccountLinking.shouldAutomaticallyLink) { + if (!shouldDoAccountLinkingOfExistingUser.shouldAutomaticallyLink) { return { createRecipeUser: false, accountsLinked: false, reason: "ACCOUNT_LINKING_NOT_ALLOWED_ERROR", }; } - if (shouldDoAccountLinking.shouldRequireVerification) { - if (!user.loginMethods[0].verified) { - return { - createRecipeUser: false, - accountsLinked: false, - reason: "EXISTING_ACCOUNT_NEEDS_TO_BE_VERIFIED_ERROR", - }; - } + if ( + shouldDoAccountLinkingOfExistingUser.shouldRequireVerification && + !existingUser.loginMethods[0].verified + ) { + return { + createRecipeUser: false, + accountsLinked: false, + reason: "EXISTING_ACCOUNT_NEEDS_TO_BE_VERIFIED_ERROR", + }; } /** * checking if primary user can be created for the existing recipe user */ - let canCreatePrimaryUser = yield this.recipeInterfaceImpl.canCreatePrimaryUserId({ - recipeUserId: user.id, + let canCreatePrimaryUserResult = yield this.recipeInterfaceImpl.canCreatePrimaryUserId({ + recipeUserId: existingUser.loginMethods[0].recipeUserId, userContext, }); - if (canCreatePrimaryUser.status !== "OK") { + if (canCreatePrimaryUserResult.status !== "OK") { + // TODO: we need to think about the implications of the different + // reasons here - which is possible? and which is not? return { createRecipeUser: false, accountsLinked: false, - reason: canCreatePrimaryUser.status, - primaryUserId: canCreatePrimaryUser.primaryUserId, + reason: canCreatePrimaryUserResult.status, + primaryUserId: canCreatePrimaryUserResult.primaryUserId, }; } /** * creating primary user for the recipe user */ let createPrimaryUserResult = yield this.recipeInterfaceImpl.createPrimaryUser({ - recipeUserId: user.id, + recipeUserId: existingUser.loginMethods[0].recipeUserId, userContext, }); - if (createPrimaryUserResult.status !== "OK") { + if ( + createPrimaryUserResult.status === + "RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" + ) { + // this can happen if there is a race condition in which the + // existing user becomes a primary user ID by the time the code + // execution comes into this block. So we call the function once again. + return yield this.accountLinkPostSignInViaSession({ + session, + newUser, + newUserVerified, + userContext, + }); + } else if ( + createPrimaryUserResult.status === + "ACCOUNT_INFO_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" + ) { + /* this can come here if in the following example: + - User creates a primary account (P1) using email R + - User2 created another account (A2) with email R (but this is not yet linked to P1 cause maybe A2 is not a candidate for account linking) + - Now User2 is logged in with A2 account, and they are trying to link with another account. + - In this case, existingUser (A2 account), cannot become a primary user + - So we are in a stuck state, and must ask the end user to contact support*/ return { createRecipeUser: false, accountsLinked: false, reason: createPrimaryUserResult.status, primaryUserId: createPrimaryUserResult.primaryUserId, }; + } else if (createPrimaryUserResult.status === "OK") { + // this if condition is not needed, but for some reason TS complains if it's not there. + existingUser = createPrimaryUserResult.user; } - user = createPrimaryUserResult.user; + // at this point, the existingUser is a primary user. So we can + // go ahead and attempt account linking for the new user and existingUser. } /** * checking if account linking is allowed for given primary user * and new login info */ - let shouldDoAccountLinking = yield this.config.shouldDoAutomaticAccountLinking( - info, - user, + const shouldDoAccountLinking = yield this.config.shouldDoAutomaticAccountLinking( + newUser, + existingUser, session, userContext ); @@ -394,44 +431,56 @@ class Recipe extends recipeModule_1.default { * checking if a recipe user already exists for the given * login info */ - let identifier; - if (info.email !== undefined) { - identifier = { - email: info.email, + let accountInfo; + if (newUser.email !== undefined) { + accountInfo = { + email: newUser.email, }; - } - if (info.phoneNumber !== undefined) { - identifier = { - phoneNumber: info.phoneNumber, + } else if (newUser.phoneNumber !== undefined) { + accountInfo = { + phoneNumber: newUser.phoneNumber, + }; + } else if (newUser.thirdParty !== undefined) { + accountInfo = { + thirdPartyId: newUser.thirdParty.id, + thirdPartyUserId: newUser.thirdParty.userId, }; } else { throw Error("this error should never be thrown"); } - let existingRecipeUsersForInputInfo = yield this.recipeInterfaceImpl.listUsersByAccountInfo({ - accountInfo: identifier, + /** + * We try and find if there is an existing recipe user for the same login method + * and same identifying info as the newUser object. + */ + let usersArrayThatHaveSameAccountInfoAsNewUser = yield this.recipeInterfaceImpl.listUsersByAccountInfo({ + accountInfo, userContext, }); - let recipeUser = existingRecipeUsersForInputInfo.find((u) => - u.loginMethods.find((lU) => { - if (lU.recipeId === info.recipeId) { - return false; - } - if (info.recipeId === "thirdparty") { - if (info.thirdParty !== undefined) { + const userObjThatHasSameAccountInfoAndRecipeIdAsNewUser = usersArrayThatHaveSameAccountInfoAsNewUser.find( + (u) => + u.loginMethods.find((lU) => { + if (lU.recipeId !== newUser.recipeId) { + return false; + } + if (newUser.recipeId === "thirdparty") { if (lU.thirdParty === undefined) { return false; } return ( - lU.thirdParty.id === info.thirdParty.id && - lU.thirdParty.userId === info.thirdParty.userId + lU.thirdParty.id === newUser.thirdParty.id && + lU.thirdParty.userId === newUser.thirdParty.userId ); } - return false; - } - return lU.email === info.email || info.phoneNumber === info.phoneNumber; - }) + return lU.email === newUser.email || newUser.phoneNumber === newUser.phoneNumber; + }) ); - if (recipeUser === undefined) { + if (userObjThatHasSameAccountInfoAndRecipeIdAsNewUser === undefined) { + /* + Before proceeding to linking accounts, we need to create the recipe user ID associated + with newUser. In order to do that in a secure way, we need to check if the accountInfo + of the newUser is the same as of the existingUser - if it is, then we can go ahead, else + we will have to check about the verification status of the newUser's accountInfo + */ /** * if recipe user doesn't exists, we check if * any of the identifying info associated with @@ -442,44 +491,43 @@ class Recipe extends recipeModule_1.default { * so the recipe will call back this function when the * recipe user is created */ - let identitiesForPrimaryUser = this.transformUserInfoIntoVerifiedAndUnverifiedBucket(user); - if (info.email !== undefined) { + let identitiesForExistingUser = this.transformUserInfoIntoVerifiedAndUnverifiedBucket(existingUser); + if (newUser.email !== undefined) { let result = - identitiesForPrimaryUser.verified.emails.includes(info.email) || - identitiesForPrimaryUser.unverified.emails.includes(info.email); + identitiesForExistingUser.verified.emails.includes(newUser.email) || + identitiesForExistingUser.unverified.emails.includes(newUser.email); if (result) { return { createRecipeUser: true, - updateVerificationClaim: false, + updateAccountLinkingClaim: "NO_CHANGE", }; } } - if (info.phoneNumber !== undefined) { + if (newUser.phoneNumber !== undefined) { let result = - identitiesForPrimaryUser.verified.phoneNumbers.includes(info.phoneNumber) || - identitiesForPrimaryUser.unverified.phoneNumbers.includes(info.phoneNumber); + identitiesForExistingUser.verified.phoneNumbers.includes(newUser.phoneNumber) || + identitiesForExistingUser.unverified.phoneNumbers.includes(newUser.phoneNumber); if (result) { return { createRecipeUser: true, - updateVerificationClaim: false, + updateAccountLinkingClaim: "NO_CHANGE", }; } } /** * checking if there already exists any other primary - * user which is associated with the identifying info - * for the given input + * user which is associated with the account info + * for the newUser */ - if (existingRecipeUsersForInputInfo !== undefined) { - let primaryUserIfExists = existingRecipeUsersForInputInfo.find((u) => u.isPrimaryUser); - if (primaryUserIfExists !== undefined) { - return { - createRecipeUser: false, - accountsLinked: false, - reason: "ACCOUNT_INFO_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR", - primaryUserId: primaryUserIfExists.id, - }; - } + const primaryUserIfExists = usersArrayThatHaveSameAccountInfoAsNewUser.find((u) => u.isPrimaryUser); + if (primaryUserIfExists !== undefined) { + // TODO: as per the lucid chart diagram, there should be an assert here? + return { + createRecipeUser: false, + accountsLinked: false, + reason: "ACCOUNT_INFO_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR", + primaryUserId: primaryUserIfExists.id, + }; } /** * if the existing info is not verified, we do want @@ -487,44 +535,59 @@ class Recipe extends recipeModule_1.default { * to again callback this function for any further * linking part. Instead, we want the recipe to * update the session claim so it can be known that - * the new account needs to be verified. so, return + * the new account needs to be verified. So, return * createRecipeUser as true to let the recipe know * that a recipe user needs to be created and set * updateVerificationClaim to true so the recipe will * not call back this function and update the session * claim instead */ - if (!infoVerified) { - if (shouldDoAccountLinking.shouldRequireVerification) { - return { - createRecipeUser: true, - updateVerificationClaim: true, - }; - } + if (!newUserVerified && shouldDoAccountLinking.shouldRequireVerification) { + return { + createRecipeUser: true, + updateAccountLinkingClaim: "ADD_CLAIM", + }; } return { createRecipeUser: true, - updateVerificationClaim: false, + updateAccountLinkingClaim: "NO_CHANGE", }; } /** - * checking if th primary user (associated with session) + * checking if the primary user (associated with session) * and recipe user (associated with login info) can be * linked */ let canLinkAccounts = yield this.recipeInterfaceImpl.canLinkAccounts({ - recipeUserId: recipeUser.id, - primaryUserId: user.id, + recipeUserId: userObjThatHasSameAccountInfoAndRecipeIdAsNewUser.id, + primaryUserId: existingUser.id, userContext, }); if (canLinkAccounts.status === "ACCOUNTS_ALREADY_LINKED_ERROR") { return { createRecipeUser: false, accountsLinked: true, - updateVerificationClaim: false, + updateAccountLinkingClaim: "REMOVE_CLAIM", }; } - if (canLinkAccounts.status !== "OK") { + if ( + canLinkAccounts.status === "ACCOUNT_INFO_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" || + canLinkAccounts.status === "RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" + ) { + /* ACCOUNT_INFO_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR can be possible if + - existingUser has email E1 + - you try and link an account with email E2 + - there already exists another primary account with email E2 + - so linking of existingUser and new account would fail + - this is a stuck state cause the user will have to contact support or login to their other account.*/ + /** + * RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR can be possible if: + - existingUser has email E1 + - you try and link an account with email E2 + - there already exists another primary account with email E2 that is linked to new account (input to this function) + - so linking of existingUser and new account would fail + - this is a stuck state cause the user will have to contact support or login to their other account. + */ return { createRecipeUser: false, accountsLinked: false, @@ -532,77 +595,82 @@ class Recipe extends recipeModule_1.default { primaryUserId: canLinkAccounts.primaryUserId, }; } - let identitiesForPrimaryUser = this.transformUserInfoIntoVerifiedAndUnverifiedBucket(user); - let recipeUserIdentifyingInfoIsAssociatedWithPrimaryUser = false; - let emailIdentityVerifiedForPrimaryUser = false; - let phoneNumberIdentityVerifiedForPrimaryUser = false; - if (info.email !== undefined) { - recipeUserIdentifyingInfoIsAssociatedWithPrimaryUser = - identitiesForPrimaryUser.verified.emails.includes(info.email) || - identitiesForPrimaryUser.unverified.emails.includes(info.email); - emailIdentityVerifiedForPrimaryUser = identitiesForPrimaryUser.verified.emails.includes(info.email); - } - if (!recipeUserIdentifyingInfoIsAssociatedWithPrimaryUser && info.phoneNumber !== undefined) { - recipeUserIdentifyingInfoIsAssociatedWithPrimaryUser = - identitiesForPrimaryUser.verified.phoneNumbers.includes(info.phoneNumber) || - identitiesForPrimaryUser.unverified.phoneNumbers.includes(info.phoneNumber); - phoneNumberIdentityVerifiedForPrimaryUser = identitiesForPrimaryUser.verified.phoneNumbers.includes( - info.phoneNumber + let accountInfoForExistingUser = this.transformUserInfoIntoVerifiedAndUnverifiedBucket(existingUser); + let newUserAccountInfoIsAssociatedWithExistingUser = false; + let newUsersEmailAlreadyVerifiedInExistingUser = false; + if (newUser.email !== undefined) { + newUserAccountInfoIsAssociatedWithExistingUser = + accountInfoForExistingUser.verified.emails.includes(newUser.email) || + accountInfoForExistingUser.unverified.emails.includes(newUser.email); + newUsersEmailAlreadyVerifiedInExistingUser = accountInfoForExistingUser.verified.emails.includes( + newUser.email ); + } else if (newUser.phoneNumber !== undefined) { + newUserAccountInfoIsAssociatedWithExistingUser = + accountInfoForExistingUser.verified.phoneNumbers.includes(newUser.phoneNumber) || + accountInfoForExistingUser.unverified.phoneNumbers.includes(newUser.phoneNumber); } - if (recipeUserIdentifyingInfoIsAssociatedWithPrimaryUser) { + if (newUserAccountInfoIsAssociatedWithExistingUser) { /** - * let's Ly belongs to P1 such that Ly equal to Lx. - * if LY verified, mark Lx as verified. If Lx is verfied, - * then mark all Ly as verified + * let Ly belong to P1 such that Ly equal to Lx. + * if LY verified or if Lx is verfied, + * then mark all Ly and Lx as verified */ - if (info.email !== undefined && (emailIdentityVerifiedForPrimaryUser || infoVerified)) { - let recipeUserIdsForEmailVerificationUpdate = user.loginMethods - .filter((u) => u.email === info.email && !u.verified) - .map((l) => l.email); - if (!infoVerified) { - recipeUserIdsForEmailVerificationUpdate.push(recipeUser.id); + if ( + newUser.email !== undefined && + (newUsersEmailAlreadyVerifiedInExistingUser || newUserVerified) + ) { + let recipeUserIdsForEmailVerificationUpdate = existingUser.loginMethods + .filter((u) => u.email === newUser.email && !u.verified) + .map((l) => l.recipeUserId); + if (!newUserVerified) { + recipeUserIdsForEmailVerificationUpdate.push( + userObjThatHasSameAccountInfoAndRecipeIdAsNewUser.id + ); } recipeUserIdsForEmailVerificationUpdate = Array.from( new Set(recipeUserIdsForEmailVerificationUpdate) ); for (let i = 0; i < recipeUserIdsForEmailVerificationUpdate.length; i++) { - let rUserId = recipeUserIdsForEmailVerificationUpdate[i]; - if (rUserId !== undefined) { - yield this.markEmailAsVerified({ - email: info.email, - recipeUserId: rUserId, - userContext, - }); - } + const recipeUserId = recipeUserIdsForEmailVerificationUpdate[i]; + yield this.markEmailAsVerified({ + email: newUser.email, + recipeUserId, + userContext, + }); } - } else if ( - info.phoneNumber !== undefined && - (phoneNumberIdentityVerifiedForPrimaryUser || infoVerified) - ) { - // DISCUSS: should we consider this scenario. phoneNumber will always be verified } } else { - if (shouldDoAccountLinking.shouldRequireVerification) { - if (!infoVerified) { - return { - createRecipeUser: false, - accountsLinked: false, - reason: "NEW_ACCOUNT_NEEDS_TO_BE_VERIFIED_ERROR", - }; - } + if (shouldDoAccountLinking.shouldRequireVerification && !newUserVerified) { + return { + createRecipeUser: false, + accountsLinked: false, + updateAccountLinkingClaim: "ADD_CLAIM", + }; } } - yield this.recipeInterfaceImpl.linkAccounts({ - recipeUserId: recipeUser.id, - primaryUserId: user.id, + const linkAccountResponse = yield this.recipeInterfaceImpl.linkAccounts({ + recipeUserId: userObjThatHasSameAccountInfoAndRecipeIdAsNewUser.id, + primaryUserId: existingUser.id, userContext, }); - return { - createRecipeUser: false, - accountsLinked: true, - updateVerificationClaim: true, - }; + if ( + linkAccountResponse.status === "OK" || + linkAccountResponse.status === "ACCOUNTS_ALREADY_LINKED_ERROR" + ) { + return { + createRecipeUser: false, + accountsLinked: true, + updateAccountLinkingClaim: "REMOVE_CLAIM", + }; + } else { + return { + createRecipeUser: false, + accountsLinked: false, + primaryUserId: linkAccountResponse.primaryUserId, + reason: linkAccountResponse.status, + }; + } }); this.getPrimaryUserIdThatCanBeLinkedToRecipeUserId = ({ recipeUserId, userContext }) => __awaiter(this, void 0, void 0, function* () { @@ -617,29 +685,31 @@ class Recipe extends recipeModule_1.default { if (pUser !== undefined && pUser.isPrimaryUser) { return pUser; } - let identifier; + let accountInfo; let loginMethodInfo = user.loginMethods[0]; // this is a recipe user so there will be only one item in the array if (loginMethodInfo.email !== undefined) { - identifier = { + accountInfo = { email: loginMethodInfo.email, }; } else if (loginMethodInfo.phoneNumber !== undefined) { - identifier = { + accountInfo = { phoneNumber: loginMethodInfo.phoneNumber, }; + } else if (loginMethodInfo.thirdParty !== undefined) { + accountInfo = { + thirdPartyId: loginMethodInfo.thirdParty.id, + thirdPartyUserId: loginMethodInfo.thirdParty.userId, + }; } else { throw Error("this error should never be thrown"); } let users = yield this.recipeInterfaceImpl.listUsersByAccountInfo({ - accountInfo: identifier, + accountInfo, userContext, }); - if (users === undefined || users.length === 0) { - return undefined; - } return users.find((u) => u.isPrimaryUser); }); - this.createPrimaryUserIdOrLinkAccounts = ({ recipeUserId, session, userContext }) => + this.createPrimaryUserIdOrLinkAccountsAfterEmailVerification = ({ recipeUserId, session, userContext }) => __awaiter(this, void 0, void 0, function* () { let primaryUser = yield this.getPrimaryUserIdThatCanBeLinkedToRecipeUserId({ recipeUserId, @@ -647,34 +717,71 @@ class Recipe extends recipeModule_1.default { }); if (primaryUser === undefined) { let user = yield __1.getUser(recipeUserId, userContext); - if (user === undefined || user.isPrimaryUser) { + if (user === undefined) { throw Error("this error should never be thrown"); } + if (user.isPrimaryUser) { + // this can come here cause of a race condition. + yield this.createPrimaryUserIdOrLinkAccountsAfterEmailVerification({ + recipeUserId, + session, + userContext, + }); + return; + } let shouldDoAccountLinking = yield this.config.shouldDoAutomaticAccountLinking( - Object.assign({}, user.loginMethods[0]), + { + recipeId: user.loginMethods[0].recipeId, + email: user.loginMethods[0].email, + phoneNumber: user.loginMethods[0].phoneNumber, + thirdParty: user.loginMethods[0].thirdParty, + }, undefined, session, userContext ); if (shouldDoAccountLinking.shouldAutomaticallyLink) { - yield this.recipeInterfaceImpl.createPrimaryUser({ + let response = yield this.recipeInterfaceImpl.createPrimaryUser({ recipeUserId: recipeUserId, userContext, }); - // TODO: remove session claim + if (response.status === "OK") { + // TODO: remove session claim + } else { + // it can come here cause of a race condition.. + yield this.createPrimaryUserIdOrLinkAccountsAfterEmailVerification({ + recipeUserId, + session, + userContext, + }); + } } } else { /** * recipeUser already linked with primaryUser */ - let recipeUser = primaryUser.loginMethods.find((u) => u.id === recipeUserId); + let recipeUser = primaryUser.loginMethods.find((u) => u.recipeUserId === recipeUserId); if (recipeUser === undefined) { let user = yield __1.getUser(recipeUserId, userContext); - if (user === undefined || user.isPrimaryUser) { + if (user === undefined) { throw Error("this error should never be thrown"); } + if (user.isPrimaryUser) { + // this can come here cause of a race condition. + yield this.createPrimaryUserIdOrLinkAccountsAfterEmailVerification({ + recipeUserId, + session, + userContext, + }); + return; + } let shouldDoAccountLinking = yield this.config.shouldDoAutomaticAccountLinking( - Object.assign({}, user.loginMethods[0]), + { + recipeId: user.loginMethods[0].recipeId, + email: user.loginMethods[0].email, + phoneNumber: user.loginMethods[0].phoneNumber, + thirdParty: user.loginMethods[0].thirdParty, + }, primaryUser, session, userContext @@ -685,10 +792,18 @@ class Recipe extends recipeModule_1.default { primaryUserId: primaryUser.id, userContext, }); - if (linkAccountsResult.status === "OK") { - // TODO: remove session claim if session claim exists - // else create a new session + let primaryUserId = primaryUser.id; + if ( + linkAccountsResult.status === + "ACCOUNT_INFO_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" || + linkAccountsResult.status === + "RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" + ) { + primaryUserId = linkAccountsResult.primaryUserId; } + console.log(primaryUserId); // TODO: remove this + // TODO: remove session claim if session claim exists + // else create a new session } } } diff --git a/lib/build/recipe/accountlinking/recipeImplementation.js b/lib/build/recipe/accountlinking/recipeImplementation.js index a2df264ac..ca280eb50 100644 --- a/lib/build/recipe/accountlinking/recipeImplementation.js +++ b/lib/build/recipe/accountlinking/recipeImplementation.js @@ -52,7 +52,6 @@ var __importDefault = Object.defineProperty(exports, "__esModule", { value: true }); const normalisedURLPath_1 = __importDefault(require("../../normalisedURLPath")); const session_1 = __importDefault(require("../session")); -const supertokens_1 = __importDefault(require("../../supertokens")); function getRecipeImplementation(querier, config) { return { getRecipeUserIdsForPrimaryUserIds: function ({ primaryUserIds }) { @@ -66,7 +65,7 @@ function getRecipeImplementation(querier, config) { return result.userIdMapping; }); }, - getPrimaryUserIdsforRecipeUserIds: function ({ recipeUserIds }) { + getPrimaryUserIdsForRecipeUserIds: function ({ recipeUserIds }) { return __awaiter(this, void 0, void 0, function* () { let result = yield querier.sendGetRequest( new normalisedURLPath_1.default("/recipe/accountlinking/users"), @@ -111,7 +110,7 @@ function getRecipeImplementation(querier, config) { * This is to know if the existing recipeUserId * is already associated with a primaryUserId */ - let recipeUserIdToPrimaryUserIdMapping = yield this.getPrimaryUserIdsforRecipeUserIds({ + let recipeUserIdToPrimaryUserIdMapping = yield this.getPrimaryUserIdsForRecipeUserIds({ recipeUserIds: [recipeUserId], userContext, }); @@ -119,7 +118,11 @@ function getRecipeImplementation(querier, config) { * checking if primaryUserId exists for the recipeUserId */ let primaryUserId = recipeUserIdToPrimaryUserIdMapping[recipeUserId]; - if (primaryUserId !== undefined && primaryUserId !== null) { + if (primaryUserId === undefined) { + // this means that the recipeUserId doesn't exist + throw new Error("The input recipeUserId does not exist"); + } + if (primaryUserId !== null) { return { status: "RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR", primaryUserId, @@ -141,7 +144,7 @@ function getRecipeImplementation(querier, config) { * precautionary check */ if (user === undefined) { - throw Error("this error should not be thrown"); + throw new Error("The input recipeUserId does not exist"); } /** * for all the identifying info associated with the recipeUser, @@ -163,6 +166,12 @@ function getRecipeImplementation(querier, config) { phoneNumber: loginMethod.phoneNumber, }); } + if (loginMethod.thirdParty !== undefined) { + infos.push({ + thirdPartyId: loginMethod.thirdParty.id, + thirdPartyUserId: loginMethod.thirdParty.userId, + }); + } for (let j = 0; j < infos.length; j++) { let info = infos[j]; let usersList = yield this.listUsersByAccountInfo({ @@ -318,13 +327,18 @@ function getRecipeImplementation(querier, config) { if (loginMethodInfo === undefined) { throw Error("this error should never be thrown"); } - let recipeUser = yield supertokens_1.default - .getInstanceOrThrowError() - ._getUserForRecipeId(loginMethodInfo.recipeUserId, loginMethodInfo.recipeId); - if (recipeUser.user === undefined) { - throw Error("this error should never be thrown"); - } - yield config.onAccountLinked(user, recipeUser.user, userContext); + yield config.onAccountLinked( + user, + { + recipeId: loginMethodInfo.recipeId, + recipeUserId: loginMethodInfo.recipeUserId, + timeJoined: loginMethodInfo.timeJoined, + email: loginMethodInfo.email, + phoneNumber: loginMethodInfo.phoneNumber, + thirdParty: loginMethodInfo.thirdParty, + }, + userContext + ); } else { throw Error(`error thrown from core while linking accounts: ${accountsLinkingResult.status}`); } @@ -333,12 +347,15 @@ function getRecipeImplementation(querier, config) { }, unlinkAccounts: function ({ recipeUserId, userContext }) { return __awaiter(this, void 0, void 0, function* () { - let recipeUserIdToPrimaryUserIdMapping = yield this.getPrimaryUserIdsforRecipeUserIds({ + let recipeUserIdToPrimaryUserIdMapping = yield this.getPrimaryUserIdsForRecipeUserIds({ recipeUserIds: [recipeUserId], userContext, }); let primaryUserId = recipeUserIdToPrimaryUserIdMapping[recipeUserId]; - if (primaryUserId === undefined || primaryUserId === null) { + if (primaryUserId === undefined) { + throw new Error("input recipeUserId does not exist"); + } + if (primaryUserId === null) { throw Error("recipeUserId is not associated with any primaryUserId"); } /** diff --git a/lib/build/recipe/accountlinking/types.d.ts b/lib/build/recipe/accountlinking/types.d.ts index 0a36d20d4..441535b32 100644 --- a/lib/build/recipe/accountlinking/types.d.ts +++ b/lib/build/recipe/accountlinking/types.d.ts @@ -55,7 +55,7 @@ export declare type RecipeInterface = { }) => Promise<{ [primaryUserId: string]: string[]; }>; - getPrimaryUserIdsforRecipeUserIds: (input: { + getPrimaryUserIdsForRecipeUserIds: (input: { recipeUserIds: string[]; userContext: any; }) => Promise<{ @@ -189,7 +189,6 @@ export declare type RecipeInterface = { }; export declare type RecipeLevelUser = { recipeId: "emailpassword" | "thirdparty" | "passwordless"; - id: string; timeJoined: number; recipeUserId: string; email?: string; diff --git a/lib/build/recipe/dashboard/types.d.ts b/lib/build/recipe/dashboard/types.d.ts index af1b0c65c..b35894316 100644 --- a/lib/build/recipe/dashboard/types.d.ts +++ b/lib/build/recipe/dashboard/types.d.ts @@ -42,7 +42,6 @@ export declare type APIFunction = (apiImplementation: APIInterface, options: API export declare type RecipeIdForUser = "emailpassword" | "thirdparty" | "passwordless"; export declare type RecipeLevelUser = { recipeId: "emailpassword" | "thirdparty" | "passwordless"; - id: string; timeJoined: number; recipeUserId: string; email?: string; diff --git a/lib/ts/recipe/accountlinking/index.ts b/lib/ts/recipe/accountlinking/index.ts index 92b91e83c..984f2e970 100644 --- a/lib/ts/recipe/accountlinking/index.ts +++ b/lib/ts/recipe/accountlinking/index.ts @@ -25,8 +25,8 @@ export default class Wrapper { }); } - static async getPrimaryUserIdsforRecipeUserIds(recipeUserIds: string[], userContext?: any) { - return await Recipe.getInstanceOrThrowError().recipeInterfaceImpl.getPrimaryUserIdsforRecipeUserIds({ + static async getPrimaryUserIdsForRecipeUserIds(recipeUserIds: string[], userContext?: any) { + return await Recipe.getInstanceOrThrowError().recipeInterfaceImpl.getPrimaryUserIdsForRecipeUserIds({ recipeUserIds, userContext: userContext === undefined ? {} : userContext, }); @@ -103,7 +103,7 @@ export default class Wrapper { export const init = Wrapper.init; export const getRecipeUserIdsForPrimaryUserIds = Wrapper.getRecipeUserIdsForPrimaryUserIds; -export const getPrimaryUserIdsforRecipeUserIds = Wrapper.getPrimaryUserIdsforRecipeUserIds; +export const getPrimaryUserIdsForRecipeUserIds = Wrapper.getPrimaryUserIdsForRecipeUserIds; export const addNewRecipeUserIdWithoutPrimaryUserId = Wrapper.addNewRecipeUserIdWithoutPrimaryUserId; export const canCreatePrimaryUserId = Wrapper.canCreatePrimaryUserId; export const createPrimaryUser = Wrapper.createPrimaryUser; diff --git a/lib/ts/recipe/accountlinking/recipe.ts b/lib/ts/recipe/accountlinking/recipe.ts index f202eab79..424ee5be7 100644 --- a/lib/ts/recipe/accountlinking/recipe.ts +++ b/lib/ts/recipe/accountlinking/recipe.ts @@ -28,7 +28,6 @@ import type { } from "./types"; import { validateAndNormaliseUserInput } from "./utils"; import { getUser } from "../.."; -import SuperTokens from "../../supertokens"; import OverrideableBuilder from "supertokens-js-override"; import RecipeImplementation from "./recipeImplementation"; import { Querier } from "../../querier"; @@ -248,10 +247,10 @@ export default class Recipe extends RecipeModule { userContext: any; }): Promise => { const emailVerificationInstance = EmailVerification.getInstance(); - if (emailVerificationInstance) { + if (emailVerificationInstance !== undefined) { const tokenResponse = await emailVerificationInstance.recipeInterfaceImpl.createEmailVerificationToken({ userId: recipeUserId, - email: email, + email, userContext, }); @@ -377,25 +376,32 @@ export default class Recipe extends RecipeModule { accountLinkPostSignInViaSession = async ({ session, - info, - infoVerified, + newUser, + newUserVerified, userContext, }: { session: SessionContainer; - info: AccountInfoAndEmailWithRecipeId; - infoVerified: boolean; + newUser: AccountInfoAndEmailWithRecipeId; + newUserVerified: boolean; userContext: any; }): Promise< | { createRecipeUser: true; - updateVerificationClaim: boolean; + + // if the value of updateAccountLinkingClaim is NO_CHANGE, it also means that the + // consumer of this function should call this function again. + updateAccountLinkingClaim: "ADD_CLAIM" | "NO_CHANGE"; } | ({ createRecipeUser: false; } & ( | { accountsLinked: true; - updateVerificationClaim: boolean; + updateAccountLinkingClaim: "REMOVE_CLAIM"; + } + | { + accountsLinked: false; + updateAccountLinkingClaim: "ADD_CLAIM"; } | { accountsLinked: false; @@ -414,25 +420,33 @@ export default class Recipe extends RecipeModule { )) > => { let userId = session.getUserId(); - let user = await this.recipeInterfaceImpl.getUser({ + let existingUser = await this.recipeInterfaceImpl.getUser({ userId, userContext, }); - if (user === undefined) { - throw Error("this should not be thrown"); + if (existingUser === undefined) { + // this can come here if the user ID in the session belongs to a user + // that is not recognized by SuperTokens. In this case, we + // disallow this kind of operation. + return { + createRecipeUser: false, + accountsLinked: false, + reason: "ACCOUNT_LINKING_NOT_ALLOWED_ERROR", + }; } /** * checking if the user with existing session * is a primary user or not */ - if (!user.isPrimaryUser) { - let shouldDoAccountLinking = await this.config.shouldDoAutomaticAccountLinking( - info, + if (!existingUser.isPrimaryUser) { + // first we check if the newUser should be a candidate for account linking + const shouldDoAccountLinkingOfNewUser = await this.config.shouldDoAutomaticAccountLinking( + newUser, undefined, session, userContext ); - if (!shouldDoAccountLinking.shouldAutomaticallyLink) { + if (!shouldDoAccountLinkingOfNewUser.shouldAutomaticallyLink) { return { createRecipeUser: false, accountsLinked: false, @@ -440,75 +454,108 @@ export default class Recipe extends RecipeModule { }; } - let recipeId = user.loginMethods[0].recipeId; - let recipeUser = await SuperTokens.getInstanceOrThrowError()._getUserForRecipeId(user.id, recipeId); + // Now we ask the user if the existing login method can be linked to anything + // (since it's not a primary user) - if (recipeUser.user === undefined) { - throw Error("This error should never be thrown. Check for bug in `getUserForRecipeId` function"); - } + // here we can use the index of 0 cause the existingUser is not a primary user, + // therefore it will only have one login method in the loginMethods' array. + let existingUserAccountInfoAndEmailWithRecipeId: AccountInfoAndEmailWithRecipeId = { + recipeId: existingUser.loginMethods[0].recipeId, + email: existingUser.loginMethods[0].email, + phoneNumber: existingUser.loginMethods[0].phoneNumber, + thirdParty: existingUser.loginMethods[0].thirdParty, + }; - shouldDoAccountLinking = await this.config.shouldDoAutomaticAccountLinking( - recipeUser.user, + const shouldDoAccountLinkingOfExistingUser = await this.config.shouldDoAutomaticAccountLinking( + existingUserAccountInfoAndEmailWithRecipeId, undefined, session, userContext ); - if (!shouldDoAccountLinking.shouldAutomaticallyLink) { + if (!shouldDoAccountLinkingOfExistingUser.shouldAutomaticallyLink) { return { createRecipeUser: false, accountsLinked: false, reason: "ACCOUNT_LINKING_NOT_ALLOWED_ERROR", }; } - if (shouldDoAccountLinking.shouldRequireVerification) { - if (!user.loginMethods[0].verified) { - return { - createRecipeUser: false, - accountsLinked: false, - reason: "EXISTING_ACCOUNT_NEEDS_TO_BE_VERIFIED_ERROR", - }; - } + if ( + shouldDoAccountLinkingOfExistingUser.shouldRequireVerification && + !existingUser.loginMethods[0].verified + ) { + return { + createRecipeUser: false, + accountsLinked: false, + reason: "EXISTING_ACCOUNT_NEEDS_TO_BE_VERIFIED_ERROR", + }; } /** * checking if primary user can be created for the existing recipe user */ - let canCreatePrimaryUser = await this.recipeInterfaceImpl.canCreatePrimaryUserId({ - recipeUserId: user.id, + let canCreatePrimaryUserResult = await this.recipeInterfaceImpl.canCreatePrimaryUserId({ + recipeUserId: existingUser.loginMethods[0].recipeUserId, userContext, }); - if (canCreatePrimaryUser.status !== "OK") { + if (canCreatePrimaryUserResult.status !== "OK") { + // TODO: we need to think about the implications of the different + // reasons here - which is possible? and which is not? return { createRecipeUser: false, accountsLinked: false, - reason: canCreatePrimaryUser.status, - primaryUserId: canCreatePrimaryUser.primaryUserId, + reason: canCreatePrimaryUserResult.status, + primaryUserId: canCreatePrimaryUserResult.primaryUserId, }; } /** * creating primary user for the recipe user */ let createPrimaryUserResult = await this.recipeInterfaceImpl.createPrimaryUser({ - recipeUserId: user.id, + recipeUserId: existingUser.loginMethods[0].recipeUserId, userContext, }); - if (createPrimaryUserResult.status !== "OK") { + + if (createPrimaryUserResult.status === "RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR") { + // this can happen if there is a race condition in which the + // existing user becomes a primary user ID by the time the code + // execution comes into this block. So we call the function once again. + return await this.accountLinkPostSignInViaSession({ + session, + newUser, + newUserVerified, + userContext, + }); + } else if ( + createPrimaryUserResult.status === "ACCOUNT_INFO_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" + ) { + /* this can come here if in the following example: + - User creates a primary account (P1) using email R + - User2 created another account (A2) with email R (but this is not yet linked to P1 cause maybe A2 is not a candidate for account linking) + - Now User2 is logged in with A2 account, and they are trying to link with another account. + - In this case, existingUser (A2 account), cannot become a primary user + - So we are in a stuck state, and must ask the end user to contact support*/ return { createRecipeUser: false, accountsLinked: false, reason: createPrimaryUserResult.status, primaryUserId: createPrimaryUserResult.primaryUserId, }; + } else if (createPrimaryUserResult.status === "OK") { + // this if condition is not needed, but for some reason TS complains if it's not there. + existingUser = createPrimaryUserResult.user; } - user = createPrimaryUserResult.user; + + // at this point, the existingUser is a primary user. So we can + // go ahead and attempt account linking for the new user and existingUser. } + /** * checking if account linking is allowed for given primary user * and new login info */ - let shouldDoAccountLinking = await this.config.shouldDoAutomaticAccountLinking( - info, - user, + const shouldDoAccountLinking = await this.config.shouldDoAutomaticAccountLinking( + newUser, + existingUser, session, userContext ); @@ -524,49 +571,59 @@ export default class Recipe extends RecipeModule { * checking if a recipe user already exists for the given * login info */ - let identifier: - | { - email: string; - } - | { - phoneNumber: string; - }; - if (info.email !== undefined) { - identifier = { - email: info.email, + let accountInfo: AccountInfo; + if (newUser.email !== undefined) { + accountInfo = { + email: newUser.email, }; - } - if (info.phoneNumber !== undefined) { - identifier = { - phoneNumber: info.phoneNumber, + } else if (newUser.phoneNumber !== undefined) { + accountInfo = { + phoneNumber: newUser.phoneNumber, + }; + } else if (newUser.thirdParty !== undefined) { + accountInfo = { + thirdPartyId: newUser.thirdParty.id, + thirdPartyUserId: newUser.thirdParty.userId, }; } else { throw Error("this error should never be thrown"); } - let existingRecipeUsersForInputInfo = await this.recipeInterfaceImpl.listUsersByAccountInfo({ - accountInfo: identifier, + + /** + * We try and find if there is an existing recipe user for the same login method + * and same identifying info as the newUser object. + */ + let usersArrayThatHaveSameAccountInfoAsNewUser = await this.recipeInterfaceImpl.listUsersByAccountInfo({ + accountInfo, userContext, }); - let recipeUser = existingRecipeUsersForInputInfo.find((u) => + + const userObjThatHasSameAccountInfoAndRecipeIdAsNewUser = usersArrayThatHaveSameAccountInfoAsNewUser.find((u) => u.loginMethods.find((lU) => { - if (lU.recipeId === info.recipeId) { + if (lU.recipeId !== newUser.recipeId) { return false; } - if (info.recipeId === "thirdparty") { - if (info.thirdParty !== undefined) { - if (lU.thirdParty === undefined) { - return false; - } - return ( - lU.thirdParty.id === info.thirdParty.id && lU.thirdParty.userId === info.thirdParty.userId - ); + if (newUser.recipeId === "thirdparty") { + if (lU.thirdParty === undefined) { + return false; } - return false; + return ( + lU.thirdParty.id === newUser.thirdParty!.id && + lU.thirdParty.userId === newUser.thirdParty!.userId + ); } - return lU.email === info.email || info.phoneNumber === info.phoneNumber; + return lU.email === newUser.email || newUser.phoneNumber === newUser.phoneNumber; }) ); - if (recipeUser === undefined) { + + if (userObjThatHasSameAccountInfoAndRecipeIdAsNewUser === undefined) { + /* + Before proceeding to linking accounts, we need to create the recipe user ID associated + with newUser. In order to do that in a secure way, we need to check if the accountInfo + of the newUser is the same as of the existingUser - if it is, then we can go ahead, else + we will have to check about the verification status of the newUser's accountInfo + */ + /** * if recipe user doesn't exists, we check if * any of the identifying info associated with @@ -577,44 +634,43 @@ export default class Recipe extends RecipeModule { * so the recipe will call back this function when the * recipe user is created */ - let identitiesForPrimaryUser = this.transformUserInfoIntoVerifiedAndUnverifiedBucket(user); - if (info.email !== undefined) { + let identitiesForExistingUser = this.transformUserInfoIntoVerifiedAndUnverifiedBucket(existingUser); + if (newUser.email !== undefined) { let result = - identitiesForPrimaryUser.verified.emails.includes(info.email) || - identitiesForPrimaryUser.unverified.emails.includes(info.email); + identitiesForExistingUser.verified.emails.includes(newUser.email) || + identitiesForExistingUser.unverified.emails.includes(newUser.email); if (result) { return { createRecipeUser: true, - updateVerificationClaim: false, + updateAccountLinkingClaim: "NO_CHANGE", }; } } - if (info.phoneNumber !== undefined) { + if (newUser.phoneNumber !== undefined) { let result = - identitiesForPrimaryUser.verified.phoneNumbers.includes(info.phoneNumber) || - identitiesForPrimaryUser.unverified.phoneNumbers.includes(info.phoneNumber); + identitiesForExistingUser.verified.phoneNumbers.includes(newUser.phoneNumber) || + identitiesForExistingUser.unverified.phoneNumbers.includes(newUser.phoneNumber); if (result) { return { createRecipeUser: true, - updateVerificationClaim: false, + updateAccountLinkingClaim: "NO_CHANGE", }; } } /** * checking if there already exists any other primary - * user which is associated with the identifying info - * for the given input + * user which is associated with the account info + * for the newUser */ - if (existingRecipeUsersForInputInfo !== undefined) { - let primaryUserIfExists = existingRecipeUsersForInputInfo.find((u) => u.isPrimaryUser); - if (primaryUserIfExists !== undefined) { - return { - createRecipeUser: false, - accountsLinked: false, - reason: "ACCOUNT_INFO_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR", - primaryUserId: primaryUserIfExists.id, - }; - } + const primaryUserIfExists = usersArrayThatHaveSameAccountInfoAsNewUser.find((u) => u.isPrimaryUser); + if (primaryUserIfExists !== undefined) { + // TODO: as per the lucid chart diagram, there should be an assert here? + return { + createRecipeUser: false, + accountsLinked: false, + reason: "ACCOUNT_INFO_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR", + primaryUserId: primaryUserIfExists.id, + }; } /** * if the existing info is not verified, we do want @@ -622,44 +678,61 @@ export default class Recipe extends RecipeModule { * to again callback this function for any further * linking part. Instead, we want the recipe to * update the session claim so it can be known that - * the new account needs to be verified. so, return + * the new account needs to be verified. So, return * createRecipeUser as true to let the recipe know * that a recipe user needs to be created and set * updateVerificationClaim to true so the recipe will * not call back this function and update the session * claim instead */ - if (!infoVerified) { - if (shouldDoAccountLinking.shouldRequireVerification) { - return { - createRecipeUser: true, - updateVerificationClaim: true, - }; - } + if (!newUserVerified && shouldDoAccountLinking.shouldRequireVerification) { + return { + createRecipeUser: true, + updateAccountLinkingClaim: "ADD_CLAIM", + }; } return { createRecipeUser: true, - updateVerificationClaim: false, + updateAccountLinkingClaim: "NO_CHANGE", }; } + /** - * checking if th primary user (associated with session) + * checking if the primary user (associated with session) * and recipe user (associated with login info) can be * linked */ let canLinkAccounts = await this.recipeInterfaceImpl.canLinkAccounts({ - recipeUserId: recipeUser.id, - primaryUserId: user.id, + recipeUserId: userObjThatHasSameAccountInfoAndRecipeIdAsNewUser.id, + primaryUserId: existingUser.id, userContext, }); if (canLinkAccounts.status === "ACCOUNTS_ALREADY_LINKED_ERROR") { return { createRecipeUser: false, accountsLinked: true, - updateVerificationClaim: false, + updateAccountLinkingClaim: "REMOVE_CLAIM", }; } - if (canLinkAccounts.status !== "OK") { + if ( + canLinkAccounts.status === "ACCOUNT_INFO_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" || + canLinkAccounts.status === "RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" + ) { + /* ACCOUNT_INFO_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR can be possible if + - existingUser has email E1 + - you try and link an account with email E2 + - there already exists another primary account with email E2 + - so linking of existingUser and new account would fail + - this is a stuck state cause the user will have to contact support or login to their other account.*/ + + /** + * RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR can be possible if: + - existingUser has email E1 + - you try and link an account with email E2 + - there already exists another primary account with email E2 that is linked to new account (input to this function) + - so linking of existingUser and new account would fail + - this is a stuck state cause the user will have to contact support or login to their other account. + */ return { createRecipeUser: false, accountsLinked: false, @@ -668,72 +741,74 @@ export default class Recipe extends RecipeModule { }; } - let identitiesForPrimaryUser = this.transformUserInfoIntoVerifiedAndUnverifiedBucket(user); - let recipeUserIdentifyingInfoIsAssociatedWithPrimaryUser = false; - let emailIdentityVerifiedForPrimaryUser = false; - let phoneNumberIdentityVerifiedForPrimaryUser = false; - if (info.email !== undefined) { - recipeUserIdentifyingInfoIsAssociatedWithPrimaryUser = - identitiesForPrimaryUser.verified.emails.includes(info.email) || - identitiesForPrimaryUser.unverified.emails.includes(info.email); - emailIdentityVerifiedForPrimaryUser = identitiesForPrimaryUser.verified.emails.includes(info.email); - } - if (!recipeUserIdentifyingInfoIsAssociatedWithPrimaryUser && info.phoneNumber !== undefined) { - recipeUserIdentifyingInfoIsAssociatedWithPrimaryUser = - identitiesForPrimaryUser.verified.phoneNumbers.includes(info.phoneNumber) || - identitiesForPrimaryUser.unverified.phoneNumbers.includes(info.phoneNumber); - phoneNumberIdentityVerifiedForPrimaryUser = identitiesForPrimaryUser.verified.phoneNumbers.includes( - info.phoneNumber + let accountInfoForExistingUser = this.transformUserInfoIntoVerifiedAndUnverifiedBucket(existingUser); + + let newUserAccountInfoIsAssociatedWithExistingUser = false; + let newUsersEmailAlreadyVerifiedInExistingUser = false; + if (newUser.email !== undefined) { + newUserAccountInfoIsAssociatedWithExistingUser = + accountInfoForExistingUser.verified.emails.includes(newUser.email) || + accountInfoForExistingUser.unverified.emails.includes(newUser.email); + newUsersEmailAlreadyVerifiedInExistingUser = accountInfoForExistingUser.verified.emails.includes( + newUser.email ); + } else if (newUser.phoneNumber !== undefined) { + newUserAccountInfoIsAssociatedWithExistingUser = + accountInfoForExistingUser.verified.phoneNumbers.includes(newUser.phoneNumber) || + accountInfoForExistingUser.unverified.phoneNumbers.includes(newUser.phoneNumber); } - if (recipeUserIdentifyingInfoIsAssociatedWithPrimaryUser) { + if (newUserAccountInfoIsAssociatedWithExistingUser) { /** - * let's Ly belongs to P1 such that Ly equal to Lx. - * if LY verified, mark Lx as verified. If Lx is verfied, - * then mark all Ly as verified + * let Ly belong to P1 such that Ly equal to Lx. + * if LY verified or if Lx is verfied, + * then mark all Ly and Lx as verified */ - if (info.email !== undefined && (emailIdentityVerifiedForPrimaryUser || infoVerified)) { - let recipeUserIdsForEmailVerificationUpdate = user.loginMethods - .filter((u) => u.email === info.email && !u.verified) - .map((l) => l.email); - if (!infoVerified) { - recipeUserIdsForEmailVerificationUpdate.push(recipeUser.id); + if (newUser.email !== undefined && (newUsersEmailAlreadyVerifiedInExistingUser || newUserVerified)) { + let recipeUserIdsForEmailVerificationUpdate = existingUser.loginMethods + .filter((u) => u.email === newUser.email && !u.verified) + .map((l) => l.recipeUserId); + if (!newUserVerified) { + recipeUserIdsForEmailVerificationUpdate.push(userObjThatHasSameAccountInfoAndRecipeIdAsNewUser.id); } recipeUserIdsForEmailVerificationUpdate = Array.from(new Set(recipeUserIdsForEmailVerificationUpdate)); for (let i = 0; i < recipeUserIdsForEmailVerificationUpdate.length; i++) { - let rUserId = recipeUserIdsForEmailVerificationUpdate[i]; - if (rUserId !== undefined) { - await this.markEmailAsVerified({ - email: info.email, - recipeUserId: rUserId, - userContext, - }); - } + const recipeUserId = recipeUserIdsForEmailVerificationUpdate[i]; + await this.markEmailAsVerified({ + email: newUser.email, + recipeUserId, + userContext, + }); } - } else if (info.phoneNumber !== undefined && (phoneNumberIdentityVerifiedForPrimaryUser || infoVerified)) { - // DISCUSS: should we consider this scenario. phoneNumber will always be verified } } else { - if (shouldDoAccountLinking.shouldRequireVerification) { - if (!infoVerified) { - return { - createRecipeUser: false, - accountsLinked: false, - reason: "NEW_ACCOUNT_NEEDS_TO_BE_VERIFIED_ERROR", - }; - } + if (shouldDoAccountLinking.shouldRequireVerification && !newUserVerified) { + return { + createRecipeUser: false, + accountsLinked: false, + updateAccountLinkingClaim: "ADD_CLAIM", + }; } } - await this.recipeInterfaceImpl.linkAccounts({ - recipeUserId: recipeUser.id, - primaryUserId: user.id, + + const linkAccountResponse = await this.recipeInterfaceImpl.linkAccounts({ + recipeUserId: userObjThatHasSameAccountInfoAndRecipeIdAsNewUser.id, + primaryUserId: existingUser.id, userContext, }); - return { - createRecipeUser: false, - accountsLinked: true, - updateVerificationClaim: true, - }; + if (linkAccountResponse.status === "OK" || linkAccountResponse.status === "ACCOUNTS_ALREADY_LINKED_ERROR") { + return { + createRecipeUser: false, + accountsLinked: true, + updateAccountLinkingClaim: "REMOVE_CLAIM", + }; + } else { + return { + createRecipeUser: false, + accountsLinked: false, + primaryUserId: linkAccountResponse.primaryUserId, + reason: linkAccountResponse.status, + }; + } }; getPrimaryUserIdThatCanBeLinkedToRecipeUserId = async ({ @@ -754,36 +829,33 @@ export default class Recipe extends RecipeModule { if (pUser !== undefined && pUser.isPrimaryUser) { return pUser; } - let identifier: - | { - email: string; - } - | { - phoneNumber: string; - }; + + let accountInfo: AccountInfo; let loginMethodInfo = user.loginMethods[0]; // this is a recipe user so there will be only one item in the array if (loginMethodInfo.email !== undefined) { - identifier = { + accountInfo = { email: loginMethodInfo.email, }; } else if (loginMethodInfo.phoneNumber !== undefined) { - identifier = { + accountInfo = { phoneNumber: loginMethodInfo.phoneNumber, }; + } else if (loginMethodInfo.thirdParty !== undefined) { + accountInfo = { + thirdPartyId: loginMethodInfo.thirdParty.id, + thirdPartyUserId: loginMethodInfo.thirdParty.userId, + }; } else { throw Error("this error should never be thrown"); } let users = await this.recipeInterfaceImpl.listUsersByAccountInfo({ - accountInfo: identifier, + accountInfo, userContext, }); - if (users === undefined || users.length === 0) { - return undefined; - } return users.find((u) => u.isPrimaryUser); }; - createPrimaryUserIdOrLinkAccounts = async ({ + createPrimaryUserIdOrLinkAccountsAfterEmailVerification = async ({ recipeUserId, session, userContext, @@ -798,52 +870,95 @@ export default class Recipe extends RecipeModule { }); if (primaryUser === undefined) { let user = await getUser(recipeUserId, userContext); - if (user === undefined || user.isPrimaryUser) { + if (user === undefined) { throw Error("this error should never be thrown"); } + if (user.isPrimaryUser) { + // this can come here cause of a race condition. + await this.createPrimaryUserIdOrLinkAccountsAfterEmailVerification({ + recipeUserId, + session, + userContext, + }); + return; + } let shouldDoAccountLinking = await this.config.shouldDoAutomaticAccountLinking( { - ...user.loginMethods[0], + recipeId: user.loginMethods[0].recipeId, + email: user.loginMethods[0].email, + phoneNumber: user.loginMethods[0].phoneNumber, + thirdParty: user.loginMethods[0].thirdParty, }, undefined, session, userContext ); + if (shouldDoAccountLinking.shouldAutomaticallyLink) { - await this.recipeInterfaceImpl.createPrimaryUser({ + let response = await this.recipeInterfaceImpl.createPrimaryUser({ recipeUserId: recipeUserId, userContext, }); - // TODO: remove session claim + if (response.status === "OK") { + // TODO: remove session claim + } else { + // it can come here cause of a race condition.. + await this.createPrimaryUserIdOrLinkAccountsAfterEmailVerification({ + recipeUserId, + session, + userContext, + }); + } } } else { /** * recipeUser already linked with primaryUser */ - let recipeUser = primaryUser.loginMethods.find((u) => u.id === recipeUserId); + let recipeUser = primaryUser.loginMethods.find((u) => u.recipeUserId === recipeUserId); if (recipeUser === undefined) { let user = await getUser(recipeUserId, userContext); - if (user === undefined || user.isPrimaryUser) { + if (user === undefined) { throw Error("this error should never be thrown"); } + if (user.isPrimaryUser) { + // this can come here cause of a race condition. + await this.createPrimaryUserIdOrLinkAccountsAfterEmailVerification({ + recipeUserId, + session, + userContext, + }); + return; + } let shouldDoAccountLinking = await this.config.shouldDoAutomaticAccountLinking( { - ...user.loginMethods[0], + recipeId: user.loginMethods[0].recipeId, + email: user.loginMethods[0].email, + phoneNumber: user.loginMethods[0].phoneNumber, + thirdParty: user.loginMethods[0].thirdParty, }, primaryUser, session, userContext ); + if (shouldDoAccountLinking.shouldAutomaticallyLink) { let linkAccountsResult = await this.recipeInterfaceImpl.linkAccounts({ recipeUserId: recipeUserId, primaryUserId: primaryUser.id, userContext, }); - if (linkAccountsResult.status === "OK") { - // TODO: remove session claim if session claim exists - // else create a new session + let primaryUserId = primaryUser.id; + if ( + linkAccountsResult.status === + "ACCOUNT_INFO_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" || + linkAccountsResult.status === "RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR" + ) { + primaryUserId = linkAccountsResult.primaryUserId; } + console.log(primaryUserId); // TODO: remove this + + // TODO: remove session claim if session claim exists + // else create a new session } } } diff --git a/lib/ts/recipe/accountlinking/recipeImplementation.ts b/lib/ts/recipe/accountlinking/recipeImplementation.ts index bfa7f1331..4e37de0f8 100644 --- a/lib/ts/recipe/accountlinking/recipeImplementation.ts +++ b/lib/ts/recipe/accountlinking/recipeImplementation.ts @@ -13,12 +13,11 @@ * under the License. */ -import { AccountInfo, RecipeInterface, TypeNormalisedInput } from "./types"; +import { AccountInfo, RecipeInterface, TypeNormalisedInput, RecipeLevelUser } from "./types"; import { Querier } from "../../querier"; import type { User } from "../../types"; import NormalisedURLPath from "../../normalisedURLPath"; import Session from "../session"; -import SuperTokens from "../../supertokens"; export default function getRecipeImplementation(querier: Querier, config: TypeNormalisedInput): RecipeInterface { return { @@ -37,7 +36,7 @@ export default function getRecipeImplementation(querier: Querier, config: TypeNo }); return result.userIdMapping; }, - getPrimaryUserIdsforRecipeUserIds: async function ( + getPrimaryUserIdsForRecipeUserIds: async function ( this: RecipeInterface, { recipeUserIds, @@ -131,7 +130,7 @@ export default function getRecipeImplementation(querier: Querier, config: TypeNo * This is to know if the existing recipeUserId * is already associated with a primaryUserId */ - let recipeUserIdToPrimaryUserIdMapping = await this.getPrimaryUserIdsforRecipeUserIds({ + let recipeUserIdToPrimaryUserIdMapping = await this.getPrimaryUserIdsForRecipeUserIds({ recipeUserIds: [recipeUserId], userContext, }); @@ -140,7 +139,11 @@ export default function getRecipeImplementation(querier: Querier, config: TypeNo * checking if primaryUserId exists for the recipeUserId */ let primaryUserId = recipeUserIdToPrimaryUserIdMapping[recipeUserId]; - if (primaryUserId !== undefined && primaryUserId !== null) { + if (primaryUserId === undefined) { + // this means that the recipeUserId doesn't exist + throw new Error("The input recipeUserId does not exist"); + } + if (primaryUserId !== null) { return { status: "RECIPE_USER_ID_ALREADY_LINKED_WITH_ANOTHER_PRIMARY_USER_ID_ERROR", primaryUserId, @@ -164,7 +167,7 @@ export default function getRecipeImplementation(querier: Querier, config: TypeNo * precautionary check */ if (user === undefined) { - throw Error("this error should not be thrown"); + throw new Error("The input recipeUserId does not exist"); } /** @@ -173,7 +176,7 @@ export default function getRecipeImplementation(querier: Querier, config: TypeNo * From those users, we'll try to find if there already exists * a primaryUser which is associated with the identifying info */ - let usersForAccountInfo = []; + let usersForAccountInfo: User[] = []; for (let i = 0; i < user.loginMethods.length; i++) { let loginMethod = user.loginMethods[i]; @@ -188,6 +191,12 @@ export default function getRecipeImplementation(querier: Querier, config: TypeNo phoneNumber: loginMethod.phoneNumber, }); } + if (loginMethod.thirdParty !== undefined) { + infos.push({ + thirdPartyId: loginMethod.thirdParty.id, + thirdPartyUserId: loginMethod.thirdParty.userId, + }); + } for (let j = 0; j < infos.length; j++) { let info = infos[j]; let usersList = await this.listUsersByAccountInfo({ @@ -455,14 +464,18 @@ export default function getRecipeImplementation(querier: Querier, config: TypeNo if (loginMethodInfo === undefined) { throw Error("this error should never be thrown"); } - let recipeUser = await SuperTokens.getInstanceOrThrowError()._getUserForRecipeId( - loginMethodInfo.recipeUserId, - loginMethodInfo.recipeId + await config.onAccountLinked( + user, + { + recipeId: loginMethodInfo.recipeId, + recipeUserId: loginMethodInfo.recipeUserId, + timeJoined: loginMethodInfo.timeJoined, + email: loginMethodInfo.email, + phoneNumber: loginMethodInfo.phoneNumber, + thirdParty: loginMethodInfo.thirdParty, + }, + userContext ); - if (recipeUser.user === undefined) { - throw Error("this error should never be thrown"); - } - await config.onAccountLinked(user, recipeUser.user, userContext); } else { throw Error(`error thrown from core while linking accounts: ${accountsLinkingResult.status}`); } @@ -481,12 +494,15 @@ export default function getRecipeImplementation(querier: Querier, config: TypeNo status: "OK"; wasRecipeUserDeleted: boolean; }> { - let recipeUserIdToPrimaryUserIdMapping = await this.getPrimaryUserIdsforRecipeUserIds({ + let recipeUserIdToPrimaryUserIdMapping = await this.getPrimaryUserIdsForRecipeUserIds({ recipeUserIds: [recipeUserId], userContext, }); let primaryUserId = recipeUserIdToPrimaryUserIdMapping[recipeUserId]; - if (primaryUserId === undefined || primaryUserId === null) { + if (primaryUserId === undefined) { + throw new Error("input recipeUserId does not exist"); + } + if (primaryUserId === null) { throw Error("recipeUserId is not associated with any primaryUserId"); } /** @@ -575,10 +591,7 @@ export default function getRecipeImplementation(querier: Querier, config: TypeNo }; } - let recipeUsersToRemove: { - recipeId: string; - recipeUserId: string; - }[] = []; + let recipeUsersToRemove: RecipeLevelUser[] = []; /** * if true, the user should be treated as primaryUser diff --git a/lib/ts/recipe/accountlinking/types.ts b/lib/ts/recipe/accountlinking/types.ts index 169d35094..b4a432a26 100644 --- a/lib/ts/recipe/accountlinking/types.ts +++ b/lib/ts/recipe/accountlinking/types.ts @@ -72,7 +72,7 @@ export type RecipeInterface = { }) => Promise<{ [primaryUserId: string]: string[]; // recipeUserIds. If input primary user ID doesn't exists, those ids will not be part of the output set. }>; - getPrimaryUserIdsforRecipeUserIds: (input: { + getPrimaryUserIdsForRecipeUserIds: (input: { recipeUserIds: string[]; userContext: any; }) => Promise<{ @@ -203,7 +203,6 @@ export type RecipeInterface = { export type RecipeLevelUser = { recipeId: "emailpassword" | "thirdparty" | "passwordless"; - id: string; // can be recipeUserId or primaryUserId timeJoined: number; recipeUserId: string; email?: string; diff --git a/lib/ts/recipe/dashboard/types.ts b/lib/ts/recipe/dashboard/types.ts index a78c3e7d1..28dd4980f 100644 --- a/lib/ts/recipe/dashboard/types.ts +++ b/lib/ts/recipe/dashboard/types.ts @@ -64,7 +64,6 @@ export type RecipeIdForUser = "emailpassword" | "thirdparty" | "passwordless"; export type RecipeLevelUser = { recipeId: "emailpassword" | "thirdparty" | "passwordless"; - id: string; // can be recipeUserId or primaryUserId timeJoined: number; recipeUserId: string; email?: string;