mirror of
https://github.com/wassname/talk.git
synced 2026-07-29 11:28:24 +08:00
[CORL-845] Account Linking (#2818)
* feat: added new linking backend * feat: added duplicateEmail to hash * fix: stored the duplicate email on the user * feat: initial implmentation of account linking in auth * test: fix unit tests * fix+test: translations and tests added * chore+test: rename view to LINK_ACCOUNT + more tests * feat+test: account linking admin + more tests * feat: Handle incomplete accounts * chore: add some comments * feat: expose duplicateEmail through graphql and impl for stream * feat: admin to use duplicateEmail from graphql * fix: no need to validate password for account linking * fix: dont validate password * fix: no need to render error message when account was incomplete * chore: log to console when encountering incomplete account * chore: adjust comment * chore: simplify + add comments * chore: wording * chore: comments Co-authored-by: Vinh <vinh@vinh.tech> Co-authored-by: Kim Gardner <kgardnr@gmail.com>
This commit is contained in:
co-authored by
Vinh
Kim Gardner
parent
7497e33046
commit
dcb2a10e72
@@ -4,6 +4,7 @@ import crypto from "crypto";
|
||||
import { translate } from "coral-server/services/i18n";
|
||||
|
||||
import {
|
||||
GQLAuthIntegrations,
|
||||
GQLFEATURE_FLAG,
|
||||
GQLReactionConfiguration,
|
||||
GQLStaffConfiguration,
|
||||
@@ -68,6 +69,21 @@ export function hasFeatureFlag(
|
||||
return false;
|
||||
}
|
||||
|
||||
export function hasEnabledAuthIntegration(
|
||||
tenant: Pick<Tenant, "auth">,
|
||||
integration: keyof GQLAuthIntegrations
|
||||
) {
|
||||
return tenant.auth.integrations[integration].enabled;
|
||||
}
|
||||
|
||||
export function linkUsersAvailable(tenant: Pick<Tenant, "auth">) {
|
||||
return (
|
||||
hasEnabledAuthIntegration(tenant, "local") &&
|
||||
(hasEnabledAuthIntegration(tenant, "facebook") ||
|
||||
hasEnabledAuthIntegration(tenant, "google"))
|
||||
);
|
||||
}
|
||||
|
||||
export function getWebhookEndpoint(
|
||||
tenant: Pick<Tenant, "webhooks">,
|
||||
endpointID: string
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { isEqual } from "lodash";
|
||||
|
||||
import { SSOUserProfile } from "coral-server/app/middleware/passport/strategies/verifiers/sso";
|
||||
|
||||
import { GQLUSER_ROLE } from "coral-server/graph/schema/__generated__/types";
|
||||
|
||||
import { SSOUserProfile } from "coral-server/app/middleware/passport/strategies/verifiers/sso";
|
||||
import { STAFF_ROLES } from "./constants";
|
||||
import { LocalProfile, SSOProfile, User } from "./user";
|
||||
import { LocalProfile, Profile, SSOProfile, User } from "./user";
|
||||
|
||||
export function roleIsStaff(role: GQLUSER_ROLE) {
|
||||
if (STAFF_ROLES.includes(role)) {
|
||||
@@ -18,14 +19,19 @@ export function hasStaffRole(user: Pick<User, "role">) {
|
||||
return roleIsStaff(user.role);
|
||||
}
|
||||
|
||||
export function getSSOProfile(user: Pick<User, "profiles">) {
|
||||
export function getUserProfile(
|
||||
user: Pick<User, "profiles">,
|
||||
type: Profile["type"]
|
||||
) {
|
||||
if (!user.profiles) {
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
|
||||
return user.profiles.find(profile => profile.type === "sso") as
|
||||
| SSOProfile
|
||||
| undefined;
|
||||
return user.profiles.find(p => p.type === type) || null;
|
||||
}
|
||||
|
||||
export function getSSOProfile(user: Pick<User, "profiles">) {
|
||||
return getUserProfile(user, "sso") as SSOProfile | null;
|
||||
}
|
||||
|
||||
export function needsSSOUpdate(
|
||||
@@ -50,14 +56,7 @@ export function getLocalProfile(
|
||||
user: Pick<User, "profiles">,
|
||||
withEmail?: string
|
||||
): LocalProfile | undefined {
|
||||
if (!user.profiles) {
|
||||
return;
|
||||
}
|
||||
|
||||
const profile = user.profiles.find(({ type }) => type === "local") as
|
||||
| LocalProfile
|
||||
| undefined;
|
||||
|
||||
const profile = getUserProfile(user, "local") as LocalProfile | null;
|
||||
if (!profile) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
ConfirmEmailTokenExpired,
|
||||
DuplicateEmailError,
|
||||
DuplicateUserError,
|
||||
EmailAlreadySetError,
|
||||
LocalProfileAlreadySetError,
|
||||
LocalProfileNotSetError,
|
||||
PasswordResetTokenExpired,
|
||||
@@ -408,6 +409,13 @@ export interface User extends TenantResource {
|
||||
*/
|
||||
emailVerified?: boolean;
|
||||
|
||||
/**
|
||||
* duplicateEmail is used to store the email address that was associated with
|
||||
* the user account on creation that at the time was previously associated
|
||||
* with another local account.
|
||||
*/
|
||||
duplicateEmail?: string;
|
||||
|
||||
/**
|
||||
* profiles is the array of profiles assigned to the user. When a user deletes
|
||||
* their account, this is unset.
|
||||
@@ -492,6 +500,7 @@ export interface FindOrCreateUserInput {
|
||||
badges?: string[];
|
||||
ssoURL?: string;
|
||||
emailVerified?: boolean;
|
||||
duplicateEmail?: string;
|
||||
role: GQLUSER_ROLE;
|
||||
profile: Profile;
|
||||
}
|
||||
@@ -1059,6 +1068,9 @@ export async function setUserEmail(
|
||||
$set: {
|
||||
email,
|
||||
},
|
||||
$unset: {
|
||||
duplicateEmail: "",
|
||||
},
|
||||
},
|
||||
{
|
||||
// False to return the updated document instead of the original
|
||||
@@ -1074,7 +1086,7 @@ export async function setUserEmail(
|
||||
}
|
||||
|
||||
if (user.email) {
|
||||
throw new UsernameAlreadySetError();
|
||||
throw new EmailAlreadySetError();
|
||||
}
|
||||
|
||||
throw new Error("an unexpected error occurred");
|
||||
@@ -2472,6 +2484,50 @@ export async function deleteModeratorNote(
|
||||
return result.value;
|
||||
}
|
||||
|
||||
export async function linkUsers(
|
||||
mongo: Db,
|
||||
tenantID: string,
|
||||
sourceUserID: string,
|
||||
destinationUserID: string
|
||||
) {
|
||||
// Delete the old user from the database.
|
||||
const source = await collection(mongo).findOneAndDelete({
|
||||
id: sourceUserID,
|
||||
tenantID,
|
||||
});
|
||||
if (!source.value) {
|
||||
throw new UserNotFoundError(sourceUserID);
|
||||
}
|
||||
|
||||
// If the source user doesn't have any profiles, we have nothing to do. We
|
||||
// should abort.
|
||||
if (!source.value.profiles) {
|
||||
throw new Error(
|
||||
"cannot link a user with no profiles, failed source authentication precondition"
|
||||
);
|
||||
}
|
||||
|
||||
// Add the new profile to the destination user.
|
||||
const dest = await collection(mongo).findOneAndUpdate(
|
||||
{
|
||||
id: destinationUserID,
|
||||
tenantID,
|
||||
},
|
||||
{
|
||||
$push: {
|
||||
profiles: {
|
||||
$each: source.value.profiles,
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
if (!dest.value) {
|
||||
throw new UserNotFoundError(destinationUserID);
|
||||
}
|
||||
|
||||
return dest.value;
|
||||
}
|
||||
|
||||
export const updateUserCommentCounts = (
|
||||
mongo: Db,
|
||||
tenantID: string,
|
||||
|
||||
Reference in New Issue
Block a user