From e7ed8483b5445c0f3e58a52b29547ef3a698cac0 Mon Sep 17 00:00:00 2001 From: Deepjyoti Barman Date: Tue, 15 Oct 2024 13:42:01 +0530 Subject: [PATCH] Add definition for password update API interface definition --- .../emailpassword/api/implementation.js | 72 ++++++++++++++- lib/build/recipe/emailpassword/types.d.ts | 7 ++ .../emailpassword/api/implementation.ts | 92 ++++++++++++++++++- lib/ts/recipe/emailpassword/types.ts | 4 + 4 files changed, 173 insertions(+), 2 deletions(-) diff --git a/lib/build/recipe/emailpassword/api/implementation.js b/lib/build/recipe/emailpassword/api/implementation.js index 6eea377fe..3fefcd9cb 100644 --- a/lib/build/recipe/emailpassword/api/implementation.js +++ b/lib/build/recipe/emailpassword/api/implementation.js @@ -13,6 +13,7 @@ const recipeUserId_1 = __importDefault(require("../../../recipeUserId")); const utils_1 = require("../utils"); const authUtils_1 = require("../../../authUtils"); const utils_2 = require("../../thirdparty/utils"); +const error_1 = __importDefault(require("../../session/error")); function getAPIImplementation() { return { emailExistsGET: async function ({ email, tenantId, userContext }) { @@ -775,8 +776,77 @@ function getAPIImplementation() { user: postAuthChecks.user, }; }, - updatePasswordPOST: undefined, changeEmailPOST: undefined, + updatePasswordPOST: async function ({ newPassword, oldPassword, userContext, options, session, tenantId }) { + /** + * This function will update the user's password by verifying that + * they have provided the correct old password. + */ + // We need to find the recipe user ID for the emailpassword recipe so we will + // use the userId to get user's login methods and then extract the recipe user + // ID accordingly. + const existingUser = await __1.getUser(session.getUserId(), userContext); + if (existingUser === undefined) { + // Should never come here as we verified above that the user exists. + throw new Error("Should never come here as the user was checked to exist"); + } + const recipeUser = existingUser.loginMethods.find((lm) => lm.recipeId === "emailpassword"); + if (!recipeUser || recipeUser.email === undefined) { + throw new error_1.default({ + type: error_1.default.UNAUTHORISED, + message: "A valid session is required to authenticate user for updating password", + }); + } + const email = recipeUser.email; + // User has provided us the required values so we can go ahead with the rest + // of the logic which is verifying the user's credentials (ie: older password) + const areUserCredentialsValid = async (tenantId) => { + return ( + ( + await options.recipeImplementation.verifyCredentials({ + email, + password: oldPassword, + tenantId, + userContext, + }) + ).status === "OK" + ); + }; + if (!areUserCredentialsValid) { + // Seems like user has provided an invalid password, cannot continue. + return { + status: "WRONG_CREDENTIALS_ERROR", + }; + } + let updateResponse = await options.recipeImplementation.updateEmailOrPassword({ + tenantIdForPasswordPolicy: tenantId, + recipeUserId: recipeUser.recipeUserId, + 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 after user was checked to exist and before their update email + // call was made. + return { + status: "USER_DELETED_WHILE_IN_PROGRESS", + reason: "The user was deleted while the password update was in progress", + }; + } else if (updateResponse.status === "PASSWORD_POLICY_VIOLATED_ERROR") { + return { + status: "PASSWORD_POLICY_VIOLATED_ERROR", + failureReason: updateResponse.failureReason, + }; + } + return { + status: "OK", + }; + }, }; } exports.default = getAPIImplementation; diff --git a/lib/build/recipe/emailpassword/types.d.ts b/lib/build/recipe/emailpassword/types.d.ts index 9757645b2..388e5cab9 100644 --- a/lib/build/recipe/emailpassword/types.d.ts +++ b/lib/build/recipe/emailpassword/types.d.ts @@ -9,6 +9,7 @@ import { import EmailDeliveryIngredient from "../../ingredients/emaildelivery"; import { GeneralErrorResponse, NormalisedAppinfo, User, UserContext } from "../../types"; import RecipeUserId from "../../recipeUserId"; +import SessionError from "../session/error"; export declare type TypeNormalisedInput = { signUpFeature: TypeNormalisedInputSignUp; signInFeature: TypeNormalisedInputSignIn; @@ -330,6 +331,7 @@ export declare type APIInterface = { session: SessionContainerInterface; options: APIOptions; userContext: UserContext; + tenantId: string; }) => Promise< | { status: "OK"; @@ -341,6 +343,11 @@ export declare type APIInterface = { status: "PASSWORD_POLICY_VIOLATED_ERROR"; failureReason: string; } + | { + status: "USER_DELETED_WHILE_IN_PROGRESS"; + reason: string; + } + | SessionError | GeneralErrorResponse >); changeEmailPOST: diff --git a/lib/ts/recipe/emailpassword/api/implementation.ts b/lib/ts/recipe/emailpassword/api/implementation.ts index 1eb46e992..e1e5afd55 100644 --- a/lib/ts/recipe/emailpassword/api/implementation.ts +++ b/lib/ts/recipe/emailpassword/api/implementation.ts @@ -10,6 +10,7 @@ import { getPasswordResetLink } from "../utils"; import { AuthUtils } from "../../../authUtils"; import { isFakeEmail } from "../../thirdparty/utils"; import { SessionContainerInterface } from "../../session/types"; +import SessionError from "../../session/error"; export default function getAPIImplementation(): APIInterface { return { @@ -927,7 +928,96 @@ export default function getAPIImplementation(): APIInterface { }; }, - updatePasswordPOST: undefined, changeEmailPOST: undefined, + + updatePasswordPOST: async function ({ + newPassword, + oldPassword, + userContext, + options, + session, + tenantId, + }): Promise< + | { + status: "OK"; + } + | { status: "WRONG_CREDENTIALS_ERROR" } + | { status: "PASSWORD_POLICY_VIOLATED_ERROR"; failureReason: string } + | { status: "USER_DELETED_WHILE_IN_PROGRESS"; reason: string } + | GeneralErrorResponse + > { + /** + * This function will update the user's password by verifying that + * they have provided the correct old password. + */ + + // We need to find the recipe user ID for the emailpassword recipe so we will + // use the userId to get user's login methods and then extract the recipe user + // ID accordingly. + const existingUser = await getUser(session.getUserId(), userContext); + if (existingUser === undefined) { + // Should never come here as we verified above that the user exists. + throw new Error("Should never come here as the user was checked to exist"); + } + const recipeUser = existingUser.loginMethods.find((lm) => lm.recipeId === "emailpassword"); + if (!recipeUser || recipeUser.email === undefined) { + throw new SessionError({ + type: SessionError.UNAUTHORISED, + message: "A valid session is required to authenticate user for updating password", + }); + } + const email = recipeUser.email; + + // User has provided us the required values so we can go ahead with the rest + // of the logic which is verifying the user's credentials (ie: older password) + const areUserCredentialsValid = async (tenantId: string) => { + return ( + ( + await options.recipeImplementation.verifyCredentials({ + email, + password: oldPassword, + tenantId, + userContext, + }) + ).status === "OK" + ); + }; + if (!areUserCredentialsValid) { + // Seems like user has provided an invalid password, cannot continue. + return { + status: "WRONG_CREDENTIALS_ERROR", + }; + } + + let updateResponse = await options.recipeImplementation.updateEmailOrPassword({ + tenantIdForPasswordPolicy: tenantId, + recipeUserId: recipeUser.recipeUserId, + 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 after user was checked to exist and before their update email + // call was made. + return { + status: "USER_DELETED_WHILE_IN_PROGRESS", + reason: "The user was deleted while the password update was in progress", + }; + } else if (updateResponse.status === "PASSWORD_POLICY_VIOLATED_ERROR") { + return { + status: "PASSWORD_POLICY_VIOLATED_ERROR", + failureReason: updateResponse.failureReason, + }; + } + + return { + status: "OK", + }; + }, }; } diff --git a/lib/ts/recipe/emailpassword/types.ts b/lib/ts/recipe/emailpassword/types.ts index fc780ea15..4ccdd5b54 100644 --- a/lib/ts/recipe/emailpassword/types.ts +++ b/lib/ts/recipe/emailpassword/types.ts @@ -23,6 +23,7 @@ import { import EmailDeliveryIngredient from "../../ingredients/emaildelivery"; import { GeneralErrorResponse, NormalisedAppinfo, User, UserContext } from "../../types"; import RecipeUserId from "../../recipeUserId"; +import SessionError from "../session/error"; export type TypeNormalisedInput = { signUpFeature: TypeNormalisedInputSignUp; @@ -334,12 +335,15 @@ export type APIInterface = { session: SessionContainerInterface; options: APIOptions; userContext: UserContext; + tenantId: string; }) => Promise< | { status: "OK"; } | { status: "WRONG_CREDENTIALS_ERROR" } | { status: "PASSWORD_POLICY_VIOLATED_ERROR"; failureReason: string } + | { status: "USER_DELETED_WHILE_IN_PROGRESS"; reason: string } + | SessionError | GeneralErrorResponse >);