diff --git a/src/core/server/graph/tenant/resolvers/User.ts b/src/core/server/graph/tenant/resolvers/User.ts index eef1556c5..5372a1e0a 100644 --- a/src/core/server/graph/tenant/resolvers/User.ts +++ b/src/core/server/graph/tenant/resolvers/User.ts @@ -49,4 +49,5 @@ export const User: GQLUserTypeResolver = { maybeLoadOnlyIgnoredUserID(ctx, info, ignoredUsers), ignoreable: ({ role }) => !roleIsStaff(role), recentCommentHistory: ({ id }): RecentCommentHistoryInput => ({ userID: id }), + profiles: ({ profiles }) => (profiles ? profiles : []), }; diff --git a/src/core/server/models/user/helpers.ts b/src/core/server/models/user/helpers.ts index cec072dae..20c63f0a5 100644 --- a/src/core/server/models/user/helpers.ts +++ b/src/core/server/models/user/helpers.ts @@ -19,6 +19,10 @@ export function hasStaffRole(user: Pick) { } export function getSSOProfile(user: Pick) { + if (!user.profiles) { + return; + } + return user.profiles.find(profile => profile.type === "sso") as | SSOProfile | undefined; @@ -45,6 +49,10 @@ export function getLocalProfile( user: Pick, withEmail?: string ): LocalProfile | undefined { + if (!user.profiles) { + return; + } + const profile = user.profiles.find(({ type }) => type === "local") as | LocalProfile | undefined; diff --git a/src/core/server/models/user/user.ts b/src/core/server/models/user/user.ts index 1d0602103..bb0b4aa67 100644 --- a/src/core/server/models/user/user.ts +++ b/src/core/server/models/user/user.ts @@ -394,9 +394,10 @@ export interface User extends TenantResource { emailVerified?: boolean; /** - * profiles is the array of profiles assigned to the user. + * profiles is the array of profiles assigned to the user. When a user deletes + * their account, this is unset. */ - profiles: Profile[]; + profiles?: Profile[]; /** * tokens lists the access tokens associated with the account. @@ -506,7 +507,6 @@ async function findOrCreateUserInput( digestFrequency: GQLDIGEST_FREQUENCY.NONE, }, moderatorNotes: [], - profiles: [], digests: [], createdAt: now, }; @@ -521,17 +521,20 @@ async function findOrCreateUserInput( }); } + // Store the user's profiles in a new array. + const profiles: Profile[] = []; + // Mutate the profiles to ensure we mask handle any secrets. switch (profile.type) { case "local": { // Hash the user's password with bcrypt. const password = await hashPassword(profile.password); - defaults.profiles.push({ ...profile, password }); + profiles.push({ ...profile, password }); break; } default: // Push the profile onto the User. - defaults.profiles.push(profile); + profiles.push(profile); break; } @@ -539,6 +542,7 @@ async function findOrCreateUserInput( return { ...defaults, ...input, + profiles, id, }; } diff --git a/src/core/server/services/migrate/indexing.ts b/src/core/server/services/migrate/indexing.ts index e6dfc764e..bc7163743 100644 --- a/src/core/server/services/migrate/indexing.ts +++ b/src/core/server/services/migrate/indexing.ts @@ -17,9 +17,11 @@ type IndexCreationFunction = ( indexOptions?: IndexOptions ) => Promise; -export function createIndexFactory( - collection: Collection -): IndexCreationFunction { +export async function createIndex( + collection: Collection, + indexSpec: IndexSpecification, + indexOptions: IndexOptions = {} +) { const log = logger.child( { collectionName: collection.collectionName, @@ -27,34 +29,38 @@ export function createIndexFactory( true ); + try { + // Try to create the index. + const start = now(); + log.debug({ indexSpec, indexOptions }, "creating index"); + const indexName = await collection.createIndex(indexSpec, indexOptions); + log.debug( + { indexName, indexSpec, indexOptions, took: Math.round(now() - start) }, + "index was created" + ); + + // Match the interface from the `createIndex` function by returning the + // index name. + return indexName; + } catch (err) { + log.error({ err, indexSpec, indexOptions }, "could not create index"); + + // Rethrow the error here. + throw err; + } +} + +export function createIndexFactory( + collection: Collection +): IndexCreationFunction { return async ( indexSpec: IndexSpecification, indexOptions: IndexOptions = {} - ) => { - try { - // Try to create the index. - const start = now(); - log.debug({ indexSpec, indexOptions }, "creating index"); - const indexName = await collection.createIndex(indexSpec, indexOptions); - log.debug( - { indexName, indexSpec, indexOptions, took: Math.round(now() - start) }, - "index was created" - ); - - // Match the interface from the `createIndex` function by returning the - // index name. - return indexName; - } catch (err) { - log.error({ err, indexSpec, indexOptions }, "could not create index"); - - // Rethrow the error here. - throw err; - } - }; + ) => createIndex(collection, indexSpec, indexOptions); } export function createConnectionOrderVariants( - createIndex: IndexCreationFunction, + createIndexFn: IndexCreationFunction, variants: Array>, indexOptions: IndexOptions = { background: true } ) { @@ -69,7 +75,7 @@ export function createConnectionOrderVariants( * @param variantSpec the spec that makes this variant different */ const createIndexVariant = (variantSpec: IndexSpecification) => - createIndex( + createIndexFn( merge({}, indexSpec, variantSpec), merge({}, indexOptions, variantIndexOptions) ); diff --git a/src/core/server/services/migrate/migrations/1572920233903_comment_moderation_actions_indexes.ts b/src/core/server/services/migrate/migrations/1572920233903_comment_moderation_actions_indexes.ts new file mode 100644 index 000000000..ed52e9d81 --- /dev/null +++ b/src/core/server/services/migrate/migrations/1572920233903_comment_moderation_actions_indexes.ts @@ -0,0 +1,19 @@ +import { Db } from "mongodb"; + +import Migration from "coral-server/services/migrate/migration"; +import collections from "coral-server/services/mongodb/collections"; + +import { createIndexFactory } from "../indexing"; + +export default class extends Migration { + public async indexes(mongo: Db) { + const createIndex = createIndexFactory( + collections.commentModerationActions(mongo) + ); + + await createIndex( + { tenantID: 1, commentID: 1, createdAt: -1 }, + { background: true } + ); + } +} diff --git a/src/core/server/services/migrate/migrations/1572991283040_user_profiles.ts b/src/core/server/services/migrate/migrations/1572991283040_user_profiles.ts new file mode 100644 index 000000000..d9aeae472 --- /dev/null +++ b/src/core/server/services/migrate/migrations/1572991283040_user_profiles.ts @@ -0,0 +1,34 @@ +import { Db } from "mongodb"; + +import Migration from "coral-server/services/migrate/migration"; +import collections from "coral-server/services/mongodb/collections"; + +import { createIndex } from "../indexing"; + +export default class extends Migration { + public async indexes(mongo: Db) { + // Drop the old index. + await collections + .users(mongo) + .dropIndex("tenantID_1_profiles.type_1_profiles.id_1"); + + // Clean up the old users that have deleted their accounts. + await collections + .users(mongo) + .updateMany({ profiles: [] }, { $unset: { profiles: "" } }); + + // Add the new index. + await createIndex( + collections.users(mongo), + { + tenantID: 1, + "profiles.id": 1, + "profiles.type": 1, + }, + { + unique: true, + partialFilterExpression: { profiles: { $exists: true } }, + } + ); + } +} diff --git a/src/core/server/services/users/delete.ts b/src/core/server/services/users/delete.ts index b53da135f..4e09d5bdb 100644 --- a/src/core/server/services/users/delete.ts +++ b/src/core/server/services/users/delete.ts @@ -103,7 +103,8 @@ async function deleteUserActionCounts( async function deleteUserComments( mongo: Db, authorID: string, - tenantID: string + tenantID: string, + now: Date ) { await collections.comments(mongo).updateMany( { tenantID, authorID }, @@ -112,7 +113,7 @@ async function deleteUserComments( authorID: null, revisions: [], tags: [], - deleted: true, + deletedAt: now, }, } ); @@ -143,17 +144,17 @@ export async function deleteUser( await deleteUserActionCounts(mongo, userID, tenantID); // Delete the user's comments. - await deleteUserComments(mongo, userID, tenantID); + await deleteUserComments(mongo, userID, tenantID, now); // Mark the user as deleted. const result = await collections.users(mongo).findOneAndUpdate( { tenantID, id: userID }, { $set: { - profiles: [], deletedAt: now, }, $unset: { + profiles: "", email: "", }, },