From 735bf1838c6ecaea3453903f99742f11d29c3edb Mon Sep 17 00:00:00 2001 From: SamSalvatico <40636569+SamSalvatico@users.noreply.github.com> Date: Fri, 10 May 2024 13:57:52 +0200 Subject: [PATCH] Chore: assign default organizations (#17) * chore(core): linked default user roles * chore(core): fixed queries * chore(core): added invokation during user create from dashboard * feat(core): added ogcio user library * chore(phrases): updated translations * Merge branch 'dev' into chore/link-user-roles --- docker-compose-local.yml | 3 + packages/core/src/libraries/ogcio-user.ts | 119 ++++++++++++++++++ packages/core/src/routes/admin-user/basics.ts | 7 +- .../interaction/actions/submit-interaction.ts | 3 + .../src/locales/de/errors/organization.ts | 4 + .../src/locales/en/errors/organization.ts | 4 + .../src/locales/es/errors/organization.ts | 4 + .../src/locales/fr/errors/organization.ts | 4 + .../src/locales/it/errors/organization.ts | 4 + .../src/locales/ja/errors/organization.ts | 4 + .../src/locales/ko/errors/organization.ts | 4 + .../src/locales/pl-pl/errors/organization.ts | 4 + .../src/locales/pt-br/errors/organization.ts | 4 + .../src/locales/pt-pt/errors/organization.ts | 4 + .../src/locales/ru/errors/organization.ts | 4 + .../src/locales/tr-tr/errors/organization.ts | 4 + .../src/locales/zh-cn/errors/organization.ts | 2 + .../src/locales/zh-hk/errors/organization.ts | 2 + .../src/locales/zh-tw/errors/organization.ts | 2 + packages/shared/src/node/env/GlobalValues.ts | 10 ++ 20 files changed, 195 insertions(+), 1 deletion(-) create mode 100644 packages/core/src/libraries/ogcio-user.ts diff --git a/docker-compose-local.yml b/docker-compose-local.yml index 7a2add43597..5fae287c5be 100644 --- a/docker-compose-local.yml +++ b/docker-compose-local.yml @@ -22,6 +22,9 @@ services: - ADMIN_ENDPOINT - PORT=3301 - ADMIN_PORT=3302 + - USER_DEFAULT_ORGANIZATION_NAMES=OGCIO Seeded Org + - USER_DEFAULT_ORGANIZATION_ROLE_NAMES=OGCIO Employee, OGCIO Manager + - USER_DEFAULT_ROLE_NAMES=Payments Employee,User-Profile Employee postgres: image: postgres:14-alpine user: postgres diff --git a/packages/core/src/libraries/ogcio-user.ts b/packages/core/src/libraries/ogcio-user.ts new file mode 100644 index 00000000000..1476174dfd9 --- /dev/null +++ b/packages/core/src/libraries/ogcio-user.ts @@ -0,0 +1,119 @@ +import { consoleLog } from '@logto/cli/lib/utils.js'; +import { type Organization, type OrganizationRole } from '@logto/schemas'; +import { deduplicate } from '@silverhand/essentials'; + +import { EnvSet } from '#src/env-set/index.js'; +import type OrganizationQueries from '#src/queries/organization/index.js'; +import assertThat from '#src/utils/assert-that.js'; + +const getDefaultOrganizationsForUser = async (organizationQueries: OrganizationQueries) => { + const organizationNames: string[] = deduplicate(EnvSet.values.userDefaultOrganizationNames); + consoleLog.info('DEFUALT ORG NAMES', organizationNames); + if (organizationNames.length === 0) { + return []; + } + const lowerOrganizationNames = new Set(organizationNames.map((name) => name.toLowerCase())); + const allOrganizations = await organizationQueries.findAll(); + + const outputOrgs = allOrganizations[1].filter((fromDatabaseOrg: Organization) => + lowerOrganizationNames.has(fromDatabaseOrg.name.toLowerCase()) + ); + + assertThat( + outputOrgs.length === organizationNames.length, + 'organization.default_organization_missing' + ); + + return outputOrgs; +}; + +const getDefaultOrganizationRolesForUser = async (organizationQueries: OrganizationQueries) => { + const roleNames: string[] = deduplicate(EnvSet.values.userDefaultOrganizationRoleNames); + consoleLog.info('DEFUALT ORG ROLE NAMES', roleNames); + if (roleNames.length === 0) { + return []; + } + const lowerRoleNames = new Set(roleNames.map((name) => name.toLowerCase())); + const limit = 200; + const offset = 0; + // eslint-disable-next-line @silverhand/fp/no-let + let outputRoleNames: OrganizationRole[] = []; + // eslint-disable-next-line @silverhand/fp/no-let + let foundCount = 1; + while (outputRoleNames.length < lowerRoleNames.size && foundCount > 0) { + // eslint-disable-next-line no-await-in-loop + const allOrganizations = await organizationQueries.roles.findAll(limit, offset); + // eslint-disable-next-line @silverhand/fp/no-mutation + foundCount = allOrganizations[0]; + // eslint-disable-next-line @silverhand/fp/no-mutation + outputRoleNames = [ + ...outputRoleNames, + ...allOrganizations[1].filter((fromDatabaseOrg: OrganizationRole) => + lowerRoleNames.has(fromDatabaseOrg.name.toLowerCase()) + ), + ]; + } + + assertThat( + outputRoleNames.length === lowerRoleNames.size, + 'organization.default_organization_role_missing' + ); + + return outputRoleNames; +}; + +const getOrganizationRelationsForUser = async (organizationQueries: OrganizationQueries) => { + return { + organizations: await getDefaultOrganizationsForUser(organizationQueries), + roles: await getDefaultOrganizationRolesForUser(organizationQueries), + }; +}; + +const insertOrganizationRelationsForUser = async (params: { + userId: string; + organizationQueries: OrganizationQueries; + organizations: Organization[]; + roles: OrganizationRole[]; +}) => { + if (params.organizations.length === 0) { + return; + } + + const orgMappings = params.organizations.map((organization: Organization): [string, string] => [ + organization.id, + params.userId, + ]); + + await Promise.all( + orgMappings.map(async (org) => params.organizationQueries.relations.users.insert(org)) + ); + + if (params.roles.length > 0) { + // Org id, role id, user id + const rolesMappings: Array<[string, string, string]> = []; + for (const role of params.roles) { + for (const orgMap of orgMappings) { + // eslint-disable-next-line @silverhand/fp/no-mutating-methods + rolesMappings.push([orgMap[0], role.id, orgMap[1]]); + } + } + + await Promise.all( + rolesMappings.map(async (roleMap) => + params.organizationQueries.relations.rolesUsers.insert(roleMap) + ) + ); + } +}; + +export const manageDefaultOrganizations = async (params: { + userId: string; + organizationQueries: OrganizationQueries; +}) => { + const orgRelations = await getOrganizationRelationsForUser(params.organizationQueries); + await insertOrganizationRelationsForUser({ + userId: params.userId, + ...orgRelations, + organizationQueries: params.organizationQueries, + }); +}; diff --git a/packages/core/src/routes/admin-user/basics.ts b/packages/core/src/routes/admin-user/basics.ts index 9ab6624e74c..71b58369b34 100644 --- a/packages/core/src/routes/admin-user/basics.ts +++ b/packages/core/src/routes/admin-user/basics.ts @@ -1,3 +1,5 @@ +/* eslint-disable eslint-comments/disable-enable-pair */ +/* eslint-disable max-lines */ import { emailRegEx, phoneRegEx, usernameRegEx } from '@logto/core-kit'; import { UsersPasswordEncryptionMethod, @@ -10,6 +12,7 @@ import { conditional, pick, yes } from '@silverhand/essentials'; import { boolean, literal, nativeEnum, object, string } from 'zod'; import RequestError from '#src/errors/RequestError/index.js'; +import { manageDefaultOrganizations } from '#src/libraries/ogcio-user.js'; import { encryptUserPassword } from '#src/libraries/user.js'; import koaGuard from '#src/middleware/koa-guard.js'; import assertThat from '#src/utils/assert-that.js'; @@ -29,6 +32,7 @@ export default function adminUserBasicsRoutes(...args: R hasUserWithPhone, }, userSsoIdentities, + organizations, } = queries; const { users: { checkIdentifierCollision, generateUserId, insertUser, verifyUserPassword }, @@ -212,6 +216,8 @@ export default function adminUserBasicsRoutes(...args: R [] ); + await manageDefaultOrganizations({ userId: id, organizationQueries: organizations }); + ctx.body = pick(user, ...userInfoSelectFields); return next(); @@ -348,7 +354,6 @@ export default function adminUserBasicsRoutes(...args: R return next(); } - // eslint-disable-next-line max-lines ); router.delete( diff --git a/packages/core/src/routes/interaction/actions/submit-interaction.ts b/packages/core/src/routes/interaction/actions/submit-interaction.ts index 7f4583c5679..c42449377a9 100644 --- a/packages/core/src/routes/interaction/actions/submit-interaction.ts +++ b/packages/core/src/routes/interaction/actions/submit-interaction.ts @@ -20,6 +20,7 @@ import { generateStandardId } from '@logto/shared'; import { conditional, conditionalArray, trySafe } from '@silverhand/essentials'; import { EnvSet } from '#src/env-set/index.js'; +import { manageDefaultOrganizations } from '#src/libraries/ogcio-user.js'; import { assignInteractionResults } from '#src/libraries/session.js'; import { encryptUserPassword } from '#src/libraries/user.js'; import type { LogEntry, WithLogContext } from '#src/middleware/koa-audit-log.js'; @@ -174,6 +175,8 @@ async function handleSubmitRegister( ]); } + await manageDefaultOrganizations({ userId: id, organizationQueries: organizations }); + await assignInteractionResults(ctx, provider, { login: { accountId: id } }); ctx.assignInteractionHookResult({ userId: id }); diff --git a/packages/phrases/src/locales/de/errors/organization.ts b/packages/phrases/src/locales/de/errors/organization.ts index 3c1fb492576..e25d64e870c 100644 --- a/packages/phrases/src/locales/de/errors/organization.ts +++ b/packages/phrases/src/locales/de/errors/organization.ts @@ -1,5 +1,9 @@ const organization = { require_membership: 'Der Benutzer muss Mitglied der Organisation sein, um fortzufahren.', + default_organization_missing: + 'Einige der Standardorganisationen existieren nicht in der Datenbank, bitte stellen Sie sicher, dass Sie zuerst Organisationen erstellen', + default_organization_role_missing: + 'Einige der Standard-Organisationsrollen existieren nicht in der Datenbank. Bitte stellen Sie sicher, dass Sie zuerst die Organisationsrollen erstellen', }; export default Object.freeze(organization); diff --git a/packages/phrases/src/locales/en/errors/organization.ts b/packages/phrases/src/locales/en/errors/organization.ts index 5551cc9b43c..5772b8da436 100644 --- a/packages/phrases/src/locales/en/errors/organization.ts +++ b/packages/phrases/src/locales/en/errors/organization.ts @@ -1,5 +1,9 @@ const organizations = { require_membership: 'The user must be a member of the organization to proceed.', + default_organization_missing: + 'Some of the default organizations does not exist in database, please ensure to create organizations first', + default_organization_role_missing: + 'Some of the default organization roles does not exist in database, please ensure to create organization roles first', }; export default Object.freeze(organizations); diff --git a/packages/phrases/src/locales/es/errors/organization.ts b/packages/phrases/src/locales/es/errors/organization.ts index a05306ebaf4..bf534ce654f 100644 --- a/packages/phrases/src/locales/es/errors/organization.ts +++ b/packages/phrases/src/locales/es/errors/organization.ts @@ -1,5 +1,9 @@ const organization = { require_membership: 'El usuario debe ser miembro de la organización para continuar.', + default_organization_missing: + 'Algunas de las organizaciones predeterminadas no existen en la base de datos, asegúrate de crear primero las organizaciones', + default_organization_role_missing: + 'Algunos de los roles de organización predeterminados no existen en la base de datos, asegúrese de crear primero los roles de organización', }; export default Object.freeze(organization); diff --git a/packages/phrases/src/locales/fr/errors/organization.ts b/packages/phrases/src/locales/fr/errors/organization.ts index eeb28b41e58..e930078b413 100644 --- a/packages/phrases/src/locales/fr/errors/organization.ts +++ b/packages/phrases/src/locales/fr/errors/organization.ts @@ -1,5 +1,9 @@ const organization = { require_membership: "L'utilisateur doit être membre de l'organisation pour continuer.", + default_organization_missing: + "Certaines des organisations par défaut n'existent pas dans la base de données, veuillez vous assurer de créer d'abord les organisations", + default_organization_role_missing: + "Certains des rôles d'organisation par défaut n'existent pas dans la base de données, veuillez vous assurer de créer d'abord les rôles d'organisation", }; export default Object.freeze(organization); diff --git a/packages/phrases/src/locales/it/errors/organization.ts b/packages/phrases/src/locales/it/errors/organization.ts index 5390ae59695..98234803e5b 100644 --- a/packages/phrases/src/locales/it/errors/organization.ts +++ b/packages/phrases/src/locales/it/errors/organization.ts @@ -1,5 +1,9 @@ const organization = { require_membership: "L'utente deve essere un membro dell'organizzazione per procedere.", + default_organization_missing: + 'Alcune delle organizzazioni predefinite non esistono nel database, assicurati di creare prima le organizzazioni', + default_organization_role_missing: + "Alcuni dei ruoli di organizzazione predefiniti non esistono nel database, assicurarsi di creare prima i ruoli dell'organizzazione", }; export default Object.freeze(organization); diff --git a/packages/phrases/src/locales/ja/errors/organization.ts b/packages/phrases/src/locales/ja/errors/organization.ts index 75b99085535..f87f8e82da3 100644 --- a/packages/phrases/src/locales/ja/errors/organization.ts +++ b/packages/phrases/src/locales/ja/errors/organization.ts @@ -1,5 +1,9 @@ const organization = { require_membership: 'ユーザーは組織のメンバーである必要があります。', + default_organization_missing: + 'デフォルトのいくつかの組織はデータベースに存在しません。組織を先に作成してください。', + default_organization_role_missing: + 'デフォルトの組織ロールのいくつかはデータベースに存在しません。組織ロールを先に作成してください', }; export default Object.freeze(organization); diff --git a/packages/phrases/src/locales/ko/errors/organization.ts b/packages/phrases/src/locales/ko/errors/organization.ts index 4b43c28b68c..c2da1cbd426 100644 --- a/packages/phrases/src/locales/ko/errors/organization.ts +++ b/packages/phrases/src/locales/ko/errors/organization.ts @@ -1,5 +1,9 @@ const organization = { require_membership: '사용자는 조직의 구성원이어야 합니다.', + default_organization_missing: + '일부 기본 조직이 데이터베이스에 존재하지 않습니다. 조직을 먼저 생성해주세요', + default_organization_role_missing: + '일부 기본 조직 역할이 데이터베이스에 없습니다. 먼저 조직 역할을 만드십시오', }; export default Object.freeze(organization); diff --git a/packages/phrases/src/locales/pl-pl/errors/organization.ts b/packages/phrases/src/locales/pl-pl/errors/organization.ts index 08ad3cd0b23..372bfe48062 100644 --- a/packages/phrases/src/locales/pl-pl/errors/organization.ts +++ b/packages/phrases/src/locales/pl-pl/errors/organization.ts @@ -1,5 +1,9 @@ const organization = { require_membership: 'Użytkownik musi być członkiem organizacji, aby kontynuować.', + default_organization_missing: + 'Niektóre domyślne organizacje nie istnieją w bazie danych, upewnij się, że najpierw utworzono organizacje', + default_organization_role_missing: + 'Niektóre z domyślnych ról organizacji nie istnieją w bazie danych, upewnij się, że najpierw utworzono role organizacji', }; export default Object.freeze(organization); diff --git a/packages/phrases/src/locales/pt-br/errors/organization.ts b/packages/phrases/src/locales/pt-br/errors/organization.ts index aae67f41e97..69c6acea9ef 100644 --- a/packages/phrases/src/locales/pt-br/errors/organization.ts +++ b/packages/phrases/src/locales/pt-br/errors/organization.ts @@ -1,5 +1,9 @@ const organization = { require_membership: 'O usuário deve ser um membro da organização para continuar.', + default_organization_missing: + 'Algumas das organizações padrão não existem no banco de dados, por favor, certifique-se de criar organizações primeiro', + default_organization_role_missing: + 'Alguns dos papéis de organização padrão não existem no banco de dados, por favor, certifique-se de criar primeiro os papéis de organização', }; export default Object.freeze(organization); diff --git a/packages/phrases/src/locales/pt-pt/errors/organization.ts b/packages/phrases/src/locales/pt-pt/errors/organization.ts index 747941ea378..15c249baef0 100644 --- a/packages/phrases/src/locales/pt-pt/errors/organization.ts +++ b/packages/phrases/src/locales/pt-pt/errors/organization.ts @@ -1,5 +1,9 @@ const organization = { require_membership: 'O utilizador deve ser membro da organização para avançar.', + default_organization_missing: + 'Algumas das organizações padrão não existem no banco de dados, por favor, certifique-se de criar organizações primeiro', + default_organization_role_missing: + 'Alguns dos papéis de organização padrão não existem no banco de dados, por favor, certifique-se de criar primeiro os papéis de organização', }; export default Object.freeze(organization); diff --git a/packages/phrases/src/locales/ru/errors/organization.ts b/packages/phrases/src/locales/ru/errors/organization.ts index fbb8f94b819..889f66b8071 100644 --- a/packages/phrases/src/locales/ru/errors/organization.ts +++ b/packages/phrases/src/locales/ru/errors/organization.ts @@ -1,5 +1,9 @@ const organization = { require_membership: 'Пользователь должен быть участником организации, чтобы продолжить.', + default_organization_missing: + 'Некоторые из стандартных организаций не существуют в базе данных, пожалуйста, убедитесь, что вы создали организации сначала', + default_organization_role_missing: + 'Некоторые из ролей организации по умолчанию не существуют в базе данных. Пожалуйста, убедитесь, что сначала созданы роли организации', }; export default Object.freeze(organization); diff --git a/packages/phrases/src/locales/tr-tr/errors/organization.ts b/packages/phrases/src/locales/tr-tr/errors/organization.ts index 475d26dfd04..7667ec8c7bd 100644 --- a/packages/phrases/src/locales/tr-tr/errors/organization.ts +++ b/packages/phrases/src/locales/tr-tr/errors/organization.ts @@ -1,5 +1,9 @@ const organization = { require_membership: 'Kullanıcının devam etmek için organizasyonun bir üyesi olması gerekir.', + default_organization_missing: + 'Niektóre domyślne organizacje nie istnieją w bazie danych, upewnij się, że najpierw utworzono organizacje', + default_organization_role_missing: + 'Varsayılan organizasyon rollerinden bazıları veritabanında mevcut değil, lütfen önce organizasyon rollerini oluşturduğunuzdan emin olun', }; export default Object.freeze(organization); diff --git a/packages/phrases/src/locales/zh-cn/errors/organization.ts b/packages/phrases/src/locales/zh-cn/errors/organization.ts index bea8a6340a0..e18190db04f 100644 --- a/packages/phrases/src/locales/zh-cn/errors/organization.ts +++ b/packages/phrases/src/locales/zh-cn/errors/organization.ts @@ -1,5 +1,7 @@ const organization = { require_membership: '用户必须是组织的成员才能继续。', + default_organization_missing: '一些默认组织在数据库中不存在,请确保首先创建组织', + default_organization_role_missing: '某些默认组织角色不存在于数据库中,请确保先创建组织角色', }; export default Object.freeze(organization); diff --git a/packages/phrases/src/locales/zh-hk/errors/organization.ts b/packages/phrases/src/locales/zh-hk/errors/organization.ts index d5ffcd2820b..6d6af7c393d 100644 --- a/packages/phrases/src/locales/zh-hk/errors/organization.ts +++ b/packages/phrases/src/locales/zh-hk/errors/organization.ts @@ -1,5 +1,7 @@ const organization = { require_membership: '用戶必須是組織的成員才能繼續。', + default_organization_missing: '一些默认组织在数据库中不存在,请确保首先创建组织', + default_organization_role_missing: '某些默认组织角色不存在于数据库中,请确保先创建组织角色', }; export default Object.freeze(organization); diff --git a/packages/phrases/src/locales/zh-tw/errors/organization.ts b/packages/phrases/src/locales/zh-tw/errors/organization.ts index 349aebe1b6e..d109a106954 100644 --- a/packages/phrases/src/locales/zh-tw/errors/organization.ts +++ b/packages/phrases/src/locales/zh-tw/errors/organization.ts @@ -1,5 +1,7 @@ const organization = { require_membership: '使用者必須是該組織的成員才能繼續。', + default_organization_missing: '一些默认组织在数据库中不存在,请确保首先创建组织', + default_organization_role_missing: '某些默认组织角色不存在于数据库中,请确保先创建组织角色', }; export default Object.freeze(organization); diff --git a/packages/shared/src/node/env/GlobalValues.ts b/packages/shared/src/node/env/GlobalValues.ts index 34fa359e05e..cc68b1cf8a2 100644 --- a/packages/shared/src/node/env/GlobalValues.ts +++ b/packages/shared/src/node/env/GlobalValues.ts @@ -122,6 +122,16 @@ export default class GlobalValues { return this.urlSet.endpoint; } + /** OGCIO Definitions */ + public readonly userDefaultOrganizationNames = getEnvAsStringArray( + 'USER_DEFAULT_ORGANIZATION_NAMES' + ); + + public readonly userDefaultOrganizationRoleNames = getEnvAsStringArray( + 'USER_DEFAULT_ORGANIZATION_ROLE_NAMES' + ); + /** END OGCIO Definitions */ + constructor() { if (this.isPathBasedMultiTenancy) { console.warn(