mirror of
https://github.com/wassname/talk.git
synced 2026-09-09 11:38:08 +08:00
[CORL-220] Change email address (#2461)
* change email form * add update email mutation * validate new email addresses * add email verification callout * add translation strings * fix submit button logic * rename model methods * style email verifcation box * resend email verificatoin button * show success message on email resend * update user profile email * fix duplicate import * add change email spec * fix profile spacing * update snapshots * update snaps * add preventSubmit function to email change component * update business logic for profile updating * update logic for when users can update email or username * check for less specific duplicate email error * prevent sso-only users from editing email * only allow email and username edit if enabeld in local profile * use generic server error for cannot update profile * remove merge conflict * fix tests * extract logic to get auth integrations * fix merge error
This commit is contained in:
@@ -17,6 +17,7 @@ import {
|
||||
suspend,
|
||||
updateAvatar,
|
||||
updateEmail,
|
||||
updateEmailByID,
|
||||
updatePassword,
|
||||
updateRole,
|
||||
updateUsername,
|
||||
@@ -38,6 +39,7 @@ import {
|
||||
GQLSetPasswordInput,
|
||||
GQLSetUsernameInput,
|
||||
GQLSuspendUserInput,
|
||||
GQLUpdateEmailInput,
|
||||
GQLUpdatePasswordInput,
|
||||
GQLUpdateUserAvatarInput,
|
||||
GQLUpdateUserEmailInput,
|
||||
@@ -140,7 +142,18 @@ export const Users = (ctx: TenantContext) => ({
|
||||
ctx.user!
|
||||
),
|
||||
updateUserEmail: async (input: GQLUpdateUserEmailInput) =>
|
||||
updateEmail(ctx.mongo, ctx.tenant, input.userID, input.email),
|
||||
updateEmailByID(ctx.mongo, ctx.tenant, input.userID, input.email),
|
||||
updateEmail: async (input: GQLUpdateEmailInput) =>
|
||||
updateEmail(
|
||||
ctx.mongo,
|
||||
ctx.tenant,
|
||||
ctx.mailerQueue,
|
||||
ctx.config,
|
||||
ctx.signingConfig!,
|
||||
ctx.user!,
|
||||
input.email,
|
||||
input.password
|
||||
),
|
||||
updateUserAvatar: async (input: GQLUpdateUserAvatarInput) =>
|
||||
updateAvatar(ctx.mongo, ctx.tenant, input.userID, input.avatar),
|
||||
updateUserRole: async (input: GQLUpdateUserRoleInput) =>
|
||||
|
||||
@@ -145,6 +145,10 @@ export const Mutation: Required<GQLMutationTypeResolver<void>> = {
|
||||
user: await ctx.mutators.Users.updateUserUsername(input),
|
||||
clientMutationId: input.clientMutationId,
|
||||
}),
|
||||
updateEmail: async (source, { input }, ctx) => ({
|
||||
user: await ctx.mutators.Users.updateEmail(input),
|
||||
clientMutationId: input.clientMutationId,
|
||||
}),
|
||||
updateUserEmail: async (source, { input }, ctx) => ({
|
||||
user: await ctx.mutators.Users.updateUserEmail(input),
|
||||
clientMutationId: input.clientMutationId,
|
||||
|
||||
@@ -4189,6 +4189,38 @@ type UpdateUserUsernamePayload {
|
||||
clientMutationId: String!
|
||||
}
|
||||
|
||||
##################
|
||||
# updateEmail
|
||||
##################
|
||||
|
||||
input UpdateEmailInput {
|
||||
"""
|
||||
email is the email address to set for the User.
|
||||
"""
|
||||
email: String!
|
||||
"""
|
||||
password is the users password.
|
||||
"""
|
||||
password: String!
|
||||
|
||||
"""
|
||||
clientMutationId is required for Relay support.
|
||||
"""
|
||||
clientMutationId: String!
|
||||
}
|
||||
|
||||
type UpdateEmailPayload {
|
||||
"""
|
||||
user is the possibly modified User.
|
||||
"""
|
||||
user: User!
|
||||
|
||||
"""
|
||||
clientMutationId is required for Relay support.
|
||||
"""
|
||||
clientMutationId: String!
|
||||
}
|
||||
|
||||
##################
|
||||
# updateUserEmail
|
||||
##################
|
||||
@@ -4704,6 +4736,12 @@ type Mutation {
|
||||
input: UpdateUserUsernameInput!
|
||||
): UpdateUserUsernamePayload! @auth(roles: [ADMIN])
|
||||
|
||||
"""
|
||||
updateEmail allows administrators to update a given User's email address
|
||||
to the one provided.
|
||||
"""
|
||||
updateEmail(input: UpdateEmailInput!): UpdateEmailPayload! @auth
|
||||
|
||||
"""
|
||||
updateUserEmail allows administrators to update a given User's email address
|
||||
to the one provided.
|
||||
|
||||
@@ -889,53 +889,53 @@ export async function setUserEmail(
|
||||
* @param tenantID the Tenant ID of the Tenant where the User exists
|
||||
* @param id the User ID that we are updating
|
||||
* @param emailAddress email address that we are setting on the User
|
||||
* @param emailVerified whether email is verified
|
||||
*/
|
||||
export async function updateUserEmail(
|
||||
mongo: Db,
|
||||
tenantID: string,
|
||||
id: string,
|
||||
emailAddress: string
|
||||
emailAddress: string,
|
||||
emailVerified = false
|
||||
) {
|
||||
// Lowercase the email address.
|
||||
const email = emailAddress.toLowerCase();
|
||||
|
||||
// Search to see if this email has been used before.
|
||||
let user = await collection(mongo).findOne({
|
||||
tenantID,
|
||||
email,
|
||||
});
|
||||
if (user) {
|
||||
throw new DuplicateEmailError(email);
|
||||
}
|
||||
|
||||
// The email wasn't found, so try to update the User.
|
||||
const result = await collection(mongo).findOneAndUpdate(
|
||||
{
|
||||
tenantID,
|
||||
id,
|
||||
},
|
||||
{
|
||||
$set: {
|
||||
email,
|
||||
try {
|
||||
// The email wasn't found, so try to update the User.
|
||||
const result = await collection(mongo).findOneAndUpdate(
|
||||
{
|
||||
tenantID,
|
||||
id,
|
||||
},
|
||||
},
|
||||
{
|
||||
// False to return the updated document instead of the original
|
||||
// document.
|
||||
returnOriginal: false,
|
||||
}
|
||||
);
|
||||
if (!result.value) {
|
||||
// Try to get the current user to discover what happened.
|
||||
user = await retrieveUser(mongo, tenantID, id);
|
||||
if (!user) {
|
||||
throw new UserNotFoundError(id);
|
||||
}
|
||||
{
|
||||
$set: {
|
||||
email,
|
||||
emailVerified,
|
||||
"profiles.$[profiles].id": email,
|
||||
},
|
||||
},
|
||||
{
|
||||
arrayFilters: [{ "profiles.type": "local" }],
|
||||
returnOriginal: false,
|
||||
}
|
||||
);
|
||||
if (!result.value) {
|
||||
// Try to get the current user to discover what happened.
|
||||
const user = await retrieveUser(mongo, tenantID, id);
|
||||
if (!user) {
|
||||
throw new UserNotFoundError(id);
|
||||
}
|
||||
|
||||
throw new Error("an unexpected error occurred");
|
||||
throw new Error("an unexpected error occurred");
|
||||
}
|
||||
return result.value;
|
||||
} catch (err) {
|
||||
if (err instanceof MongoError && err.code === 11000) {
|
||||
throw new DuplicateEmailError(email!);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
|
||||
return result.value;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -22,7 +22,10 @@ import {
|
||||
UsernameUpdatedWithinWindowError,
|
||||
UserNotFoundError,
|
||||
} from "coral-server/errors";
|
||||
import { GQLUSER_ROLE } from "coral-server/graph/tenant/schema/__generated__/types";
|
||||
import {
|
||||
GQLAuthIntegrations,
|
||||
GQLUSER_ROLE,
|
||||
} from "coral-server/graph/tenant/schema/__generated__/types";
|
||||
import logger from "coral-server/logger";
|
||||
import { Tenant } from "coral-server/models/tenant";
|
||||
import {
|
||||
@@ -58,6 +61,8 @@ import {
|
||||
} from "coral-server/models/user/helpers";
|
||||
import { userIsStaff } from "coral-server/models/user/helpers";
|
||||
import { MailerQueue } from "coral-server/queue/tasks/mailer";
|
||||
import { sendConfirmationEmail } from "coral-server/services/users/auth";
|
||||
|
||||
import { JWTSigningConfig, signPATString } from "coral-server/services/jwt";
|
||||
|
||||
import { generateDownloadLink } from "./download/download";
|
||||
@@ -388,6 +393,11 @@ export async function updateUsername(
|
||||
// Validate the username.
|
||||
validateUsername(username);
|
||||
|
||||
const canUpdate = canUpdateLocalProfile(tenant, user);
|
||||
if (!canUpdate) {
|
||||
throw new Error("Cannot update profile due to tenant settings");
|
||||
}
|
||||
|
||||
// Get the earliest date that the username could have been edited before to/
|
||||
// allow it now.
|
||||
const lastUsernameEditAllowed = DateTime.fromJSDate(now)
|
||||
@@ -483,7 +493,96 @@ export async function updateRole(
|
||||
}
|
||||
|
||||
/**
|
||||
* updateEmail will update the given User's email address. This should not
|
||||
* enabledAuthenticationIntegrations returns enabled auth integrations for a tenant
|
||||
* @param tenant Tenant where the User will be interacted with
|
||||
* @param target whether to filter by stream or admin enabled. defaults to requiring both.
|
||||
*/
|
||||
function enabledAuthenticationIntegrations(
|
||||
tenant: Tenant,
|
||||
target?: "stream" | "admin"
|
||||
): string[] {
|
||||
return Object.keys(tenant.auth.integrations).filter((key: string) => {
|
||||
const { enabled, targetFilter } = tenant.auth.integrations[
|
||||
key as keyof GQLAuthIntegrations
|
||||
];
|
||||
if (target) {
|
||||
return enabled && targetFilter[target];
|
||||
}
|
||||
return enabled && targetFilter.admin && targetFilter.stream;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* canUpdateLocalProfile will determine if a user is permitted to update their email address.
|
||||
* @param tenant Tenant where the User will be interacted with
|
||||
* @param user the User that we are updating
|
||||
*/
|
||||
function canUpdateLocalProfile(tenant: Tenant, user: User): boolean {
|
||||
if (!hasLocalProfile(user)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const streamAuthTypes = enabledAuthenticationIntegrations(tenant, "stream");
|
||||
|
||||
// user can update email if local auth is enabled or any integration other than sso is enabled
|
||||
return (
|
||||
streamAuthTypes.includes("local") ||
|
||||
!(streamAuthTypes.length === 1 && streamAuthTypes[0] === "sso")
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* updateEmail will update the current User's email address.
|
||||
* @param mongo mongo database to interact with
|
||||
* @param tenant Tenant where the User will be interacted with
|
||||
* @param mailer The mailer queue
|
||||
* @param config Convict config
|
||||
* @param user the User that we are updating
|
||||
* @param email the email address that we are setting on the User
|
||||
* @param password the users password for confirmation
|
||||
*/
|
||||
export async function updateEmail(
|
||||
mongo: Db,
|
||||
tenant: Tenant,
|
||||
mailer: MailerQueue,
|
||||
config: Config,
|
||||
signingConfig: JWTSigningConfig,
|
||||
user: User,
|
||||
emailAddress: string,
|
||||
password: string,
|
||||
now = new Date()
|
||||
) {
|
||||
const email = emailAddress.toLowerCase();
|
||||
validateEmail(email);
|
||||
|
||||
const canUpdate = canUpdateLocalProfile(tenant, user);
|
||||
if (!canUpdate) {
|
||||
throw new Error("Cannot update profile due to tenant settings");
|
||||
}
|
||||
|
||||
const passwordVerified = await verifyUserPassword(user, password);
|
||||
if (!passwordVerified) {
|
||||
// We throw a PasswordIncorrect error here instead of an
|
||||
// InvalidCredentialsError because the current user is already signed in.
|
||||
throw new PasswordIncorrect();
|
||||
}
|
||||
|
||||
const updated = await updateUserEmail(mongo, tenant.id, user.id, email);
|
||||
|
||||
await sendConfirmationEmail(
|
||||
mongo,
|
||||
mailer,
|
||||
tenant,
|
||||
config,
|
||||
signingConfig,
|
||||
updated as Required<User>,
|
||||
now
|
||||
);
|
||||
return updated;
|
||||
}
|
||||
|
||||
/**
|
||||
* updateUserEmail will update the given User's email address. This should not
|
||||
* trigger and email notifications as it's designed to be used by administrators
|
||||
* to update a user's email address.
|
||||
*
|
||||
@@ -492,7 +591,7 @@ export async function updateRole(
|
||||
* @param userID the User's ID that we are updating
|
||||
* @param email the email address that we are setting on the User
|
||||
*/
|
||||
export async function updateEmail(
|
||||
export async function updateEmailByID(
|
||||
mongo: Db,
|
||||
tenant: Tenant,
|
||||
userID: string,
|
||||
@@ -501,7 +600,7 @@ export async function updateEmail(
|
||||
// Validate the email address.
|
||||
validateEmail(email);
|
||||
|
||||
return updateUserEmail(mongo, tenant.id, userID, email);
|
||||
return updateUserEmail(mongo, tenant.id, userID, email, true);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user