mirror of
https://github.com/wassname/talk.git
synced 2026-09-10 12:43:11 +08:00
[CORL-445] Change Password (#2426)
* fix: reversed `new-password` autocomplete option * feat: initial implementation * fix: localization and testing * fix: updated snapshot
This commit is contained in:
@@ -1,10 +1,13 @@
|
||||
import { LocalProfile, SSOProfile } from "coral-server/models/user";
|
||||
import uuid from "uuid/v1";
|
||||
|
||||
import { getLocalProfile, hasLocalProfile } from "./helpers";
|
||||
import { LocalProfile, SSOProfile } from "./user";
|
||||
|
||||
const localProfile: LocalProfile = {
|
||||
type: "local",
|
||||
id: "hans@email.com",
|
||||
password: "secret",
|
||||
passwordID: uuid(),
|
||||
};
|
||||
|
||||
const ssoProfile: SSOProfile = {
|
||||
@@ -25,6 +28,12 @@ it("will return true when the profile exists", () => {
|
||||
expect(hasLocalProfile({ profiles: [localProfile] })).toBeTruthy();
|
||||
});
|
||||
|
||||
it("will get the local profile with the correct email", () => {
|
||||
expect(
|
||||
getLocalProfile({ profiles: [localProfile] }, localProfile.id)
|
||||
).toEqual(localProfile);
|
||||
});
|
||||
|
||||
it("will return true when the profile exists with the right email", () => {
|
||||
expect(
|
||||
hasLocalProfile({ profiles: [localProfile] }, localProfile.id)
|
||||
|
||||
@@ -34,11 +34,22 @@ export function needsSSOUpdate(
|
||||
* @param user the User to pull the LocalProfile out of
|
||||
*/
|
||||
export function getLocalProfile(
|
||||
user: Pick<User, "profiles">
|
||||
user: Pick<User, "profiles">,
|
||||
withEmail?: string
|
||||
): LocalProfile | undefined {
|
||||
return user.profiles.find(({ type }) => type === "local") as
|
||||
const profile = user.profiles.find(({ type }) => type === "local") as
|
||||
| LocalProfile
|
||||
| undefined;
|
||||
|
||||
if (!profile) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (withEmail && profile.id !== withEmail) {
|
||||
return;
|
||||
}
|
||||
|
||||
return profile;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -53,14 +64,10 @@ export function hasLocalProfile(
|
||||
user: Pick<User, "profiles">,
|
||||
withEmail?: string
|
||||
): boolean {
|
||||
const profile = getLocalProfile(user);
|
||||
const profile = getLocalProfile(user, withEmail);
|
||||
if (!profile) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (withEmail && profile.id !== withEmail) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -47,6 +47,13 @@ export interface LocalProfile {
|
||||
id: string;
|
||||
password: string;
|
||||
|
||||
/**
|
||||
* passwordID is used to help protect against double password change race
|
||||
* conditions. Because the password cannot be compared against directly, this
|
||||
* ID can be used as it is only changed when the password is changed.
|
||||
*/
|
||||
passwordID: string;
|
||||
|
||||
/**
|
||||
* resetID is used during a password reset process to prevent replay attacks.
|
||||
* When a password reset email is sent, a resetID is associated with the
|
||||
@@ -530,9 +537,10 @@ export async function updateUserRole(
|
||||
|
||||
export async function verifyUserPassword(
|
||||
user: Pick<User, "profiles">,
|
||||
password: string
|
||||
password: string,
|
||||
withEmail?: string
|
||||
) {
|
||||
const profile = getLocalProfile(user);
|
||||
const profile = getLocalProfile(user, withEmail);
|
||||
if (!profile) {
|
||||
throw new LocalProfileNotSetError();
|
||||
}
|
||||
@@ -544,7 +552,8 @@ export async function updateUserPassword(
|
||||
mongo: Db,
|
||||
tenantID: string,
|
||||
id: string,
|
||||
password: string
|
||||
password: string,
|
||||
passwordID: string
|
||||
) {
|
||||
// Hash the password.
|
||||
const hashedPassword = await hashPassword(password);
|
||||
@@ -556,10 +565,17 @@ export async function updateUserPassword(
|
||||
id,
|
||||
// This ensures that the document we're updating already has a local
|
||||
// profile associated with them.
|
||||
"profiles.type": "local",
|
||||
profiles: {
|
||||
$elemMatch: {
|
||||
type: "local",
|
||||
passwordID,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
$set: {
|
||||
// Update the passwordID with a new one.
|
||||
"profiles.$[profiles].passwordID": uuid(),
|
||||
"profiles.$[profiles].password": hashedPassword,
|
||||
},
|
||||
},
|
||||
@@ -576,10 +592,15 @@ export async function updateUserPassword(
|
||||
throw new UserNotFoundError(id);
|
||||
}
|
||||
|
||||
if (!hasLocalProfile(user)) {
|
||||
const profile = getLocalProfile(user);
|
||||
if (!profile) {
|
||||
throw new LocalProfileNotSetError();
|
||||
}
|
||||
|
||||
if (profile.passwordID !== passwordID) {
|
||||
throw new Error("passwordID mismatch");
|
||||
}
|
||||
|
||||
throw new Error("an unexpected error occurred");
|
||||
}
|
||||
|
||||
@@ -936,6 +957,7 @@ export async function setUserLocalProfile(
|
||||
type: "local",
|
||||
id: email,
|
||||
password: hashedPassword,
|
||||
passwordID: uuid(),
|
||||
};
|
||||
|
||||
// The profile wasn't found, so add it to the User.
|
||||
@@ -1553,6 +1575,7 @@ export async function resetUserPassword(
|
||||
tenantID: string,
|
||||
id: string,
|
||||
password: string,
|
||||
passwordID: string,
|
||||
resetID: string
|
||||
) {
|
||||
// Hash the password.
|
||||
@@ -1568,12 +1591,15 @@ export async function resetUserPassword(
|
||||
profiles: {
|
||||
$elemMatch: {
|
||||
type: "local",
|
||||
passwordID,
|
||||
resetID,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
$set: {
|
||||
// Update the passwordID with a new one.
|
||||
"profiles.$[profiles].passwordID": uuid(),
|
||||
"profiles.$[profiles].password": hashedPassword,
|
||||
},
|
||||
$unset: {
|
||||
@@ -1602,6 +1628,10 @@ export async function resetUserPassword(
|
||||
throw new PasswordResetTokenExpired("reset id mismatch");
|
||||
}
|
||||
|
||||
if (profile.passwordID !== passwordID) {
|
||||
throw new PasswordResetTokenExpired("password id mismatch");
|
||||
}
|
||||
|
||||
throw new Error("an unexpected error occurred");
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user