diff --git a/packages/server/modules/core/domain/users/operations.ts b/packages/server/modules/core/domain/users/operations.ts index d00e0ea00..0830ad0a2 100644 --- a/packages/server/modules/core/domain/users/operations.ts +++ b/packages/server/modules/core/domain/users/operations.ts @@ -24,6 +24,14 @@ export type GetUser = ( params?: GetUserParams ) => Promise> +export type GetUserByEmail = ( + email: string, + options?: Partial<{ + skipClean: boolean + withRole: boolean + }> +) => Promise + export type StoreUser = (params: { user: Omit, 'suuid' | 'createdAt'> }) => Promise diff --git a/packages/server/modules/core/repositories/users.ts b/packages/server/modules/core/repositories/users.ts index fc4ee7369..3e010d653 100644 --- a/packages/server/modules/core/repositories/users.ts +++ b/packages/server/modules/core/repositories/users.ts @@ -13,6 +13,7 @@ import { UserWithOptionalRole } from '@/modules/core/domain/users/types' import { CountAdminUsers, GetUser, + GetUserByEmail, GetUserParams, GetUsers, LegacyGetPaginatedUsers, @@ -154,31 +155,35 @@ export const getUserFactory = /** * Get user by e-mail address */ -export async function getUserByEmail( - email: string, - options?: Partial<{ skipClean: boolean; withRole: boolean }> -) { - const q = Users.knex() - .leftJoin(UserEmails.name, UserEmails.col.userId, Users.col.id) - .where({ - [UserEmails.col.primary]: true - }) - .whereRaw('lower("user_emails"."email") = lower(?)', [email]) - const columns: (Knex.Raw | string)[] = [ - ...Object.values(omit(Users.col, ['email', 'verified'])), - knex.raw(`(array_agg("user_emails"."email"))[1] as email`), - knex.raw(`(array_agg("user_emails"."verified"))[1] as verified`) - ] - if (options?.withRole) { - // Getting first role from grouped results - columns.push(knex.raw(`(array_agg("server_acl"."role"))[1] as role`)) - q.leftJoin(ServerAcl.name, ServerAcl.col.userId, Users.col.id) +export const getUserByEmailFactory = + (deps: { db: Knex }): GetUserByEmail => + async ( + email: string, + options?: Partial<{ skipClean: boolean; withRole: boolean }> + ) => { + const q = tables + .users(deps.db) + .leftJoin(UserEmails.name, UserEmails.col.userId, Users.col.id) + .where({ + [UserEmails.col.primary]: true + }) + .whereRaw('lower("user_emails"."email") = lower(?)', [email]) + const columns: (Knex.Raw | string)[] = [ + ...Object.values(omit(Users.col, ['email', 'verified'])), + knex.raw(`(array_agg("user_emails"."email"))[1] as email`), + knex.raw(`(array_agg("user_emails"."verified"))[1] as verified`) + ] + if (options?.withRole) { + // Getting first role from grouped results + columns.push(knex.raw(`(array_agg("server_acl"."role"))[1] as role`)) + q.leftJoin(ServerAcl.name, ServerAcl.col.userId, Users.col.id) + } + q.columns(columns) + q.groupBy(Users.col.id) + + const user = (await q.first()) as UserWithOptionalRole + return user ? (!options?.skipClean ? sanitizeUserRecord(user) : user) : null } - q.columns(columns) - q.groupBy(Users.col.id) - const user = await q.first() - return user ? (!options?.skipClean ? sanitizeUserRecord(user) : user) : null -} /** * Mark a user as verified by e-mail address, and return true on success diff --git a/packages/server/modules/core/services/users.js b/packages/server/modules/core/services/users.js index 086918bd1..3187fb9d4 100644 --- a/packages/server/modules/core/services/users.js +++ b/packages/server/modules/core/services/users.js @@ -16,7 +16,6 @@ const Users = () => UsersSchema.knex() const Acl = () => ServerAclSchema.knex() const { LIMITED_USER_FIELDS } = require('@/modules/core/helpers/userHelper') -const { getUserByEmail } = require('@/modules/core/repositories/users') const { omit } = require('lodash') const { dbLogger } = require('@/logging/logging') const { @@ -26,6 +25,7 @@ const { const { Roles } = require('@speckle/shared') const { db } = require('@/db/knex') const { deleteStreamFactory } = require('@/modules/core/repositories/streams') +const { getUserByEmailFactory } = require('@/modules/core/repositories/users') const _changeUserRole = async ({ userId, role }) => await Acl().where({ userId }).update({ role }) @@ -42,6 +42,7 @@ const _ensureAtleastOneAdminRemains = async (userId) => { } } } +const getUserByEmail = getUserByEmailFactory({ db }) module.exports = { // TODO: this should be moved to repository diff --git a/packages/server/modules/core/tests/integration/findUsers.spec.ts b/packages/server/modules/core/tests/integration/findUsers.spec.ts index b984ed70e..252f438cd 100644 --- a/packages/server/modules/core/tests/integration/findUsers.spec.ts +++ b/packages/server/modules/core/tests/integration/findUsers.spec.ts @@ -13,7 +13,7 @@ import { db } from '@/db/knex' import { expect } from 'chai' import { countAdminUsersFactory, - getUserByEmail, + getUserByEmailFactory, getUserFactory, getUsersFactory, listUsers, @@ -62,6 +62,7 @@ const createUser = createUserFactory({ }), usersEventsEmitter: UsersEmitter.emit }) +const getUserByEmail = getUserByEmailFactory({ db }) describe('Find users @core', () => { describe('getUsers', () => { diff --git a/packages/server/modules/core/tests/integration/userEmails.spec.ts b/packages/server/modules/core/tests/integration/userEmails.spec.ts index 2c145639c..d201bfc36 100644 --- a/packages/server/modules/core/tests/integration/userEmails.spec.ts +++ b/packages/server/modules/core/tests/integration/userEmails.spec.ts @@ -3,7 +3,7 @@ import { beforeEachContext } from '@/test/hooks' import { expect } from 'chai' import { countAdminUsersFactory, - getUserByEmail, + getUserByEmailFactory, getUserFactory, legacyGetPaginatedUsersCount, legacyGetPaginatedUsersFactory, @@ -81,6 +81,7 @@ const createUser = createUserFactory({ validateAndCreateUserEmail: createUserEmail, usersEventsEmitter: UsersEmitter.emit }) +const getUserByEmail = getUserByEmailFactory({ db }) describe('Core @user-emails', () => { before(async () => { diff --git a/packages/server/modules/emails/graph/resolvers/index.ts b/packages/server/modules/emails/graph/resolvers/index.ts index ca6a690f3..e7bb50b77 100644 --- a/packages/server/modules/emails/graph/resolvers/index.ts +++ b/packages/server/modules/emails/graph/resolvers/index.ts @@ -1,7 +1,10 @@ import { db } from '@/db/knex' import { Resolvers } from '@/modules/core/graph/generated/graphql' import { findPrimaryEmailForUserFactory } from '@/modules/core/repositories/userEmails' -import { getUserByEmail, getUserFactory } from '@/modules/core/repositories/users' +import { + getUserByEmailFactory, + getUserFactory +} from '@/modules/core/repositories/users' import { getServerInfo } from '@/modules/core/services/generic' import { deleteOldAndInsertNewVerificationFactory, @@ -20,6 +23,7 @@ const requestEmailVerification = requestEmailVerificationFactory({ sendEmail, renderEmail }) +const getUserByEmail = getUserByEmailFactory({ db }) export = { User: { diff --git a/packages/server/modules/pwdreset/rest/index.ts b/packages/server/modules/pwdreset/rest/index.ts index ad4c795be..c7296037c 100644 --- a/packages/server/modules/pwdreset/rest/index.ts +++ b/packages/server/modules/pwdreset/rest/index.ts @@ -1,6 +1,6 @@ import { db } from '@/db/knex' import { deleteExistingAuthTokensFactory } from '@/modules/auth/repositories' -import { getUserByEmail } from '@/modules/core/repositories/users' +import { getUserByEmailFactory } from '@/modules/core/repositories/users' import { getServerInfo } from '@/modules/core/services/generic' import { updateUserPassword } from '@/modules/core/services/users' import { renderEmail } from '@/modules/emails/services/emailRendering' @@ -16,6 +16,8 @@ import { ensureError } from '@/modules/shared/helpers/errorHelper' import { Express } from 'express' export default function (app: Express) { + const getUserByEmail = getUserByEmailFactory({ db }) + // sends a password recovery email. app.post('/auth/pwdreset/request', async (req, res) => { try { diff --git a/packages/server/modules/pwdreset/services/finalize.ts b/packages/server/modules/pwdreset/services/finalize.ts index acbc12a3e..d27c3a841 100644 --- a/packages/server/modules/pwdreset/services/finalize.ts +++ b/packages/server/modules/pwdreset/services/finalize.ts @@ -1,11 +1,11 @@ import { DeleteExistingUserAuthTokens } from '@/modules/auth/domain/operations' -import { getUserByEmail } from '@/modules/core/repositories/users' +import { GetUserByEmail } from '@/modules/core/domain/users/operations' import { updateUserPassword } from '@/modules/core/services/users' import { DeleteTokens, GetPendingToken } from '@/modules/pwdreset/domain/operations' import { PasswordRecoveryFinalizationError } from '@/modules/pwdreset/errors' type InitializeStateDeps = { - getUserByEmail: typeof getUserByEmail + getUserByEmail: GetUserByEmail getPendingToken: GetPendingToken } diff --git a/packages/server/modules/pwdreset/services/request.ts b/packages/server/modules/pwdreset/services/request.ts index 3ac655119..433258c19 100644 --- a/packages/server/modules/pwdreset/services/request.ts +++ b/packages/server/modules/pwdreset/services/request.ts @@ -1,5 +1,5 @@ +import { GetUserByEmail } from '@/modules/core/domain/users/operations' import { getPasswordResetFinalizationRoute } from '@/modules/core/helpers/routeHelper' -import { getUserByEmail } from '@/modules/core/repositories/users' import { getServerInfo } from '@/modules/core/services/generic' import { EmailTemplateParams, @@ -14,7 +14,7 @@ import { getFrontendOrigin } from '@/modules/shared/helpers/envHelper' const EMAIL_SUBJECT = 'Speckle Account Password Reset' type InitializeNewTokenDeps = { - getUserByEmail: typeof getUserByEmail + getUserByEmail: GetUserByEmail getPendingToken: GetPendingToken createToken: CreateToken getServerInfo: typeof getServerInfo diff --git a/packages/server/modules/serverinvites/repositories/serverInvites.ts b/packages/server/modules/serverinvites/repositories/serverInvites.ts index dcb66d3db..97b108086 100644 --- a/packages/server/modules/serverinvites/repositories/serverInvites.ts +++ b/packages/server/modules/serverinvites/repositories/serverInvites.ts @@ -1,6 +1,6 @@ import { knex, ServerInvites, Streams, Users } from '@/modules/core/dbSchema' import { - getUserByEmail, + getUserByEmailFactory, getUserFactory, UserWithOptionalRole } from '@/modules/core/repositories/users' @@ -131,7 +131,7 @@ export const findUserByTargetFactory = (target: string): Promise => { const { userEmail, userId } = resolveTarget(target) return userEmail - ? getUserByEmail(userEmail, { withRole: true }) + ? getUserByEmailFactory(deps)(userEmail, { withRole: true }) : getUserFactory(deps)(userId!, { withRole: true }) }