diff --git a/packages/backend-core/src/db/constants.ts b/packages/backend-core/src/db/constants.ts index 460476da24..4e508280a6 100644 --- a/packages/backend-core/src/db/constants.ts +++ b/packages/backend-core/src/db/constants.ts @@ -18,6 +18,7 @@ export enum ViewName { LINK = "by_link", ROUTING = "screen_routes", AUTOMATION_LOGS = "automation_logs", + PLATFORM_USERS_LOWERCASE = "platform_users_lowercase", } export const DeprecatedViews = { diff --git a/packages/backend-core/src/db/views.js b/packages/backend-core/src/db/views.js index 3a45611a8f..2119c74c6c 100644 --- a/packages/backend-core/src/db/views.js +++ b/packages/backend-core/src/db/views.js @@ -128,12 +128,33 @@ exports.createUserBuildersView = async () => { await db.put(designDoc) } +exports.createPlatformUserView = async db => { + let designDoc + try { + designDoc = await db.get(DESIGN_DB) + } catch (err) { + // no design doc, make one + designDoc = DesignDoc() + } + const view = { + map: `function(doc) { + emit(doc._id.toLowerCase(), doc._id) + }`, + } + designDoc.views = { + ...designDoc.views, + [ViewName.PLATFORM_USERS_LOWERCASE]: view, + } + await db.put(designDoc) +} + exports.queryGlobalView = async (viewName, params, db = null) => { const CreateFuncByName = { [ViewName.USER_BY_EMAIL]: exports.createNewUserEmailView, [ViewName.BY_API_KEY]: exports.createApiKeyView, [ViewName.USER_BY_BUILDERS]: exports.createUserBuildersView, [ViewName.USER_BY_APP]: exports.createUserAppView, + [ViewName.PLATFORM_USERS_LOWERCASE]: exports.createPlatformUserView, } // can pass DB in if working with something specific if (!db) { @@ -149,7 +170,7 @@ exports.queryGlobalView = async (viewName, params, db = null) => { if (err != null && err.name === "not_found") { const createFunc = CreateFuncByName[viewName] await removeDeprecated(db, viewName) - await createFunc() + await createFunc(db) return exports.queryGlobalView(viewName, params) } else { throw err diff --git a/packages/backend-core/src/tenancy/tenancy.ts b/packages/backend-core/src/tenancy/tenancy.ts index 041f694d34..ebff4dd056 100644 --- a/packages/backend-core/src/tenancy/tenancy.ts +++ b/packages/backend-core/src/tenancy/tenancy.ts @@ -1,5 +1,5 @@ import { doWithDB } from "../db" -import { StaticDatabases } from "../db/constants" +import { StaticDatabases, UNICODE_MAX, ViewName } from "../db/constants" import { baseGlobalDBName } from "./utils" import { getTenantId, @@ -8,6 +8,7 @@ import { getTenantIDFromAppID, } from "../context" import env from "../environment" +import { queryGlobalView } from "../db/views" const TENANT_DOC = StaticDatabases.PLATFORM_INFO.docs.tenants const PLATFORM_INFO_DB = StaticDatabases.PLATFORM_INFO.name @@ -117,12 +118,22 @@ export const lookupTenantId = async (userId: string) => { // lookup, could be email or userId, either will return a doc export const getTenantUser = async (identifier: string) => { - return doWithDB(PLATFORM_INFO_DB, async (db: any) => { - try { - return await db.get(identifier) - } catch (err) { - return null + // use the view here and allow to find anyone regardless of casing + const lcIdentifier = identifier.toLowerCase() + + return await doWithDB(StaticDatabases.PLATFORM_INFO.name, async (db: any) => { + let response = await queryGlobalView( + ViewName.PLATFORM_USERS_LOWERCASE, + { + startkey: lcIdentifier, + endkey: `${lcIdentifier}${UNICODE_MAX}`, + }, + db + ) + if (!response) { + response = [] } + return response }) } diff --git a/packages/builder/src/pages/builder/portal/manage/users/_components/PasswordModal.svelte b/packages/builder/src/pages/builder/portal/manage/users/_components/PasswordModal.svelte index 02501f2de0..e7ee28411b 100644 --- a/packages/builder/src/pages/builder/portal/manage/users/_components/PasswordModal.svelte +++ b/packages/builder/src/pages/builder/portal/manage/users/_components/PasswordModal.svelte @@ -42,7 +42,7 @@ { + await createPlatformUserView() +} diff --git a/packages/server/src/migrations/index.ts b/packages/server/src/migrations/index.ts index 494740d1d9..fc0edf5f2b 100644 --- a/packages/server/src/migrations/index.ts +++ b/packages/server/src/migrations/index.ts @@ -9,6 +9,7 @@ import * as appUrls from "./functions/appUrls" import * as developerQuota from "./functions/developerQuota" import * as publishedAppsQuota from "./functions/publishedAppsQuota" import * as backfill from "./functions/backfill" +import * as platformUsersEmailViewCasing from "./functions/platformUserEmailViewCasing" /** * Populate the migration function and additional configuration from @@ -84,6 +85,13 @@ export const buildMigrations = () => { }) break } + case MigrationName.PLATFORM_USERS_EMAIL_CASING: { + serverMigrations.push({ + ...definition, + fn: platformUsersEmailViewCasing.run, + }) + break + } } } diff --git a/packages/types/src/sdk/migrations.ts b/packages/types/src/sdk/migrations.ts index 23a4d6d097..5ad5ccb87c 100644 --- a/packages/types/src/sdk/migrations.ts +++ b/packages/types/src/sdk/migrations.ts @@ -47,6 +47,7 @@ export enum MigrationName { EVENT_GLOBAL_BACKFILL = "event_global_backfill", EVENT_INSTALLATION_BACKFILL = "event_installation_backfill", GLOBAL_INFO_SYNC_USERS = "global_info_sync_users", + PLATFORM_USERS_EMAIL_CASING = "platform_users_email_casing", } export interface MigrationDefinition { diff --git a/packages/worker/src/sdk/users/users.ts b/packages/worker/src/sdk/users/users.ts index 58c2decabf..72316e34a0 100644 --- a/packages/worker/src/sdk/users/users.ts +++ b/packages/worker/src/sdk/users/users.ts @@ -264,11 +264,14 @@ export const bulkCreate = async ( ) let mapped = allUsers.rows.map((row: any) => row.id) - const currentUserEmails = mapped.map((x: any) => x.email) || [] + const currentUserEmails = mapped.map((x: any) => x.email.toLowerCase()) || [] for (const newUser of newUsersRequested) { + // Lowercase emails to ensure several users can't be created with different email casing if ( - newUsers.find((x: any) => x.email === newUser.email) || - currentUserEmails.includes(newUser.email) + newUsers.find( + (x: any) => x.email.toLowerCase() === newUser.email.toLowerCase() + ) || + currentUserEmails.includes(newUser.email.toLowerCase()) ) { continue }