Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Feat: Add support for password and session related API interface changes #947

Draft
wants to merge 9 commits into
base: feat/user-management-support
Choose a base branch
from
74 changes: 73 additions & 1 deletion lib/build/recipe/emailpassword/api/implementation.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 }) {
Expand Down Expand Up @@ -775,8 +776,79 @@ 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) {
throw new error_1.default({
type: error_1.default.UNAUTHORISED,
message: "Session user not found",
});
}
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: "No user found with emailpassword recipe",
});
}
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;
12 changes: 12 additions & 0 deletions lib/build/recipe/emailpassword/api/updatePassword.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// @ts-nocheck
/**
* This file contains the top-level handler definition for password update
*/
import { APIInterface, APIOptions } from "../";
import { UserContext } from "../../../types";
export default function updatePassword(
apiImplementation: APIInterface,
tenantId: string,
options: APIOptions,
userContext: UserContext
): Promise<boolean>;
48 changes: 48 additions & 0 deletions lib/build/recipe/emailpassword/api/updatePassword.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
"use strict";
/**
* This file contains the top-level handler definition for password update
*/
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 error_1 = __importDefault(require("../error"));
const session_1 = __importDefault(require("../../session"));
async function updatePassword(apiImplementation, tenantId, options, userContext) {
if (apiImplementation.updatePasswordPOST === undefined) {
return false;
}
const { newPassword, oldPassword } = await options.req.getJSONBody();
if (newPassword === undefined) {
throw new error_1.default({
message: "Missing required parameter 'newPassword'",
type: error_1.default.BAD_INPUT_ERROR,
});
}
if (oldPassword === undefined) {
throw new error_1.default({
message: "Missing required parameter 'oldPassword'",
type: error_1.default.BAD_INPUT_ERROR,
});
}
const session = await session_1.default.getSession(
options.req,
options.res,
{ overrideGlobalClaimValidators: () => [] },
userContext
);
let result = await apiImplementation.updatePasswordPOST({
newPassword,
oldPassword,
session,
options,
userContext,
tenantId,
});
utils_1.send200Response(options.res, result);
return true;
}
exports.default = updatePassword;
1 change: 1 addition & 0 deletions lib/build/recipe/emailpassword/constants.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@ export declare const SIGN_UP_API = "/signup";
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 PASSWORD_UPDATE_API = "/user/password/update";
export declare const SIGNUP_EMAIL_EXISTS_API_OLD = "/signup/email/exists";
export declare const SIGNUP_EMAIL_EXISTS_API = "/emailpassword/email/exists";
3 changes: 2 additions & 1 deletion lib/build/recipe/emailpassword/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,13 @@
* under the License.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.SIGNUP_EMAIL_EXISTS_API = exports.SIGNUP_EMAIL_EXISTS_API_OLD = 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.SIGNUP_EMAIL_EXISTS_API = exports.SIGNUP_EMAIL_EXISTS_API_OLD = exports.PASSWORD_UPDATE_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";
exports.SIGN_IN_API = "/signin";
exports.GENERATE_PASSWORD_RESET_TOKEN_API = "/user/password/reset/token";
exports.PASSWORD_RESET_API = "/user/password/reset";
exports.PASSWORD_UPDATE_API = "/user/password/update";
exports.SIGNUP_EMAIL_EXISTS_API_OLD = "/signup/email/exists";
exports.SIGNUP_EMAIL_EXISTS_API = "/emailpassword/email/exists";
9 changes: 9 additions & 0 deletions lib/build/recipe/emailpassword/recipe.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ const generatePasswordResetToken_1 = __importDefault(require("./api/generatePass
const passwordReset_1 = __importDefault(require("./api/passwordReset"));
const utils_2 = require("../../utils");
const emailExists_1 = __importDefault(require("./api/emailExists"));
const updatePassword_1 = __importDefault(require("./api/updatePassword"));
const recipeImplementation_1 = __importDefault(require("./recipeImplementation"));
const implementation_1 = __importDefault(require("./api/implementation"));
const querier_1 = require("../../querier");
Expand Down Expand Up @@ -84,6 +85,12 @@ class Recipe extends recipeModule_1.default {
id: constants_1.SIGNUP_EMAIL_EXISTS_API_OLD,
disabled: this.apiImpl.emailExistsGET === undefined,
},
{
method: "post",
pathWithoutApiBasePath: new normalisedURLPath_1.default(constants_1.PASSWORD_UPDATE_API),
id: constants_1.PASSWORD_UPDATE_API,
disabled: this.apiImpl.updatePasswordPOST === undefined,
},
];
};
this.handleAPIRequest = async (id, tenantId, req, res, _path, _method, userContext) => {
Expand All @@ -107,6 +114,8 @@ class Recipe extends recipeModule_1.default {
return await passwordReset_1.default(this.apiImpl, tenantId, options, userContext);
} else if (id === constants_1.SIGNUP_EMAIL_EXISTS_API || id === constants_1.SIGNUP_EMAIL_EXISTS_API_OLD) {
return await emailExists_1.default(this.apiImpl, tenantId, options, userContext);
} else if (id === constants_1.PASSWORD_UPDATE_API) {
return await updatePassword_1.default(this.apiImpl, tenantId, options, userContext);
}
return false;
};
Expand Down
7 changes: 7 additions & 0 deletions lib/build/recipe/emailpassword/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -330,6 +331,7 @@ export declare type APIInterface = {
session: SessionContainerInterface;
options: APIOptions;
userContext: UserContext;
tenantId: string;
}) => Promise<
| {
status: "OK";
Expand All @@ -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:
Expand Down
109 changes: 107 additions & 2 deletions lib/build/recipe/session/api/implementation.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,13 @@ var __importDefault =
return mod && mod.__esModule ? mod : { default: mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const __1 = __importDefault(require("../"));
const utils_1 = require("../../../utils");
const normalisedURLPath_1 = __importDefault(require("../../../normalisedURLPath"));
const sessionRequestFunctions_1 = require("../sessionRequestFunctions");
const error_1 = __importDefault(require("../error"));
const __2 = require("../../..");
const constants_1 = require("../constants");
function getAPIInterface() {
return {
refreshPOST: async function ({ options, userContext }) {
Expand Down Expand Up @@ -51,8 +55,109 @@ function getAPIInterface() {
status: "OK",
};
},
allSessionsGET: undefined,
revokeSessionPOST: undefined,
allSessionsGET: async function ({ session, options, tenantId, userContext }) {
/**
* Get all the active sessions for the logged in user.
*
* This function will fetched all sessions for the user and
* return them in descending order based on the time the session
* was created at.
*/
// Get the logged in user's userId
const userId = session.getUserId(userContext);
// We need to verify that the user is authenticated because
// the getAllSessionHandlesForUser function doesn't check
// whether the user is logged in or not
const existingUser = await __2.getUser(userId, userContext);
if (existingUser === undefined) {
throw new error_1.default({
type: error_1.default.UNAUTHORISED,
message: "Session user not found",
});
}
// We will first fetch the list of sessionHandles for the user
// and then fetch the information for each of them.
const allSessionHandles = await options.recipeImplementation.getAllSessionHandlesForUser({
userId,
fetchSessionsForAllLinkedAccounts: true,
tenantId,
fetchAcrossAllTenants: false,
userContext,
});
// Since we need to fetch multiple sessions information,
// we are creating multiple promises for fetching the details
// and using a Promise.all() to resolve them together.
const userSessions = [];
const sessionGetPromises = [];
allSessionHandles.forEach((sessionHandle) => {
sessionGetPromises.push(
new Promise(async (resolve, reject) => {
try {
const sessionInformation = await __1.default.getSessionInformation(
sessionHandle,
userContext
);
if (sessionInformation !== undefined) {
userSessions.push(
Object.assign(Object.assign({}, sessionInformation), {
userAgent:
sessionInformation.sessionDataInDatabase[
constants_1.USER_AGENT_KEY_FOR_SESSION_DATA
],
})
);
}
resolve();
} catch (err) {
reject(err);
}
})
);
});
// Wait for the sessions to be fetched.
await Promise.all(sessionGetPromises);
// Sort the fetched session based on their timeCreated values
// to ensure that the newer sessions show up at the top.
const sessionsSortedByCreatedTime = userSessions.sort(
(sessionA, sessionB) => sessionB.timeCreated - sessionA.timeCreated
);
return {
status: "OK",
sessions: sessionsSortedByCreatedTime,
};
},
revokeSessionPOST: async function ({ sessionHandle, session, options, userContext }) {
/**
* Revoke the session passed using the sessionHandle.
*/
// Get the logged in user's userId
const userId = session.getUserId(userContext);
// We need to verify that the user is authenticated because
// the revokeSession function doesn't check
// whether the user is logged in or not
const existingUser = await __2.getUser(userId, userContext);
if (existingUser === undefined) {
throw new error_1.default({
type: error_1.default.UNAUTHORISED,
message: "Session user not found",
});
}
const wasRevoked = await options.recipeImplementation.revokeSession({
sessionHandle,
userContext,
});
if (!wasRevoked) {
// This is a very unlikely case but we should still consider
// it since the upper level function returns this.
//
// We will just throw an error so that the API consumer
// can understand that session was not removed.
throw new Error("Failed to revoke session");
}
return {
status: "OK",
};
},
};
}
exports.default = getAPIInterface;
11 changes: 11 additions & 0 deletions lib/build/recipe/session/api/sessionRevoke.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// @ts-nocheck
/**
* Defines top-level handler for revoking a session using it's handle.
*/
import { APIInterface, APIOptions } from "..";
import { UserContext } from "../../../types";
export default function sessionRevoke(
apiImplementation: APIInterface,
options: APIOptions,
userContext: UserContext
): Promise<boolean>;
Loading
Loading