From 2901f43e645406dd180e6fc633e044ebfca21916 Mon Sep 17 00:00:00 2001 From: Gao Sun Date: Wed, 16 Aug 2023 22:34:54 +0800 Subject: [PATCH] feat: post affiliate data to cloud (#4321) * feat: post affiliate data to cloud Read data from cookie and post to cloud when needed. * chore: add alteration script * refactor: fix alteration --- packages/core/package.json | 1 + .../interaction/actions/submit-interaction.ts | 53 ++++++++++++++--- .../next-1692194751-add-affiliate-scopes.ts | 57 +++++++++++++++++++ packages/schemas/src/seeds/cloud-api.ts | 5 ++ pnpm-lock.yaml | 12 +++- 5 files changed, 120 insertions(+), 8 deletions(-) create mode 100644 packages/schemas/alterations/next-1692194751-add-affiliate-scopes.ts diff --git a/packages/core/package.json b/packages/core/package.json index 7ad84e55e..9a7049c9b 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -28,6 +28,7 @@ "@aws-sdk/client-s3": "^3.315.0", "@azure/storage-blob": "^12.13.0", "@koa/cors": "^4.0.0", + "@logto/affiliate": "^0.1.0", "@logto/app-insights": "workspace:^1.3.1", "@logto/cli": "workspace:^1.7.0", "@logto/connector-kit": "workspace:^1.1.1", diff --git a/packages/core/src/routes/interaction/actions/submit-interaction.ts b/packages/core/src/routes/interaction/actions/submit-interaction.ts index 413239f34..0f156dff7 100644 --- a/packages/core/src/routes/interaction/actions/submit-interaction.ts +++ b/packages/core/src/routes/interaction/actions/submit-interaction.ts @@ -1,3 +1,4 @@ +import { defaults, parseAffiliateData } from '@logto/affiliate'; import { Component, CoreEvent, getEventName } from '@logto/app-insights/custom-event'; import { appInsights } from '@logto/app-insights/node'; import type { User, Profile, CreateUser } from '@logto/schemas'; @@ -11,14 +12,17 @@ import { adminConsoleApplicationId, } from '@logto/schemas'; import { type OmitAutoSetFields } from '@logto/shared'; -import { conditional, conditionalArray } from '@silverhand/essentials'; +import { conditional, conditionalArray, trySafe } from '@silverhand/essentials'; +import { type IRouterContext } from 'koa-router'; import { EnvSet } from '#src/env-set/index.js'; +import { type CloudConnectionLibrary } from '#src/libraries/cloud-connection.js'; import type { ConnectorLibrary } from '#src/libraries/connector.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'; import type TenantContext from '#src/tenants/TenantContext.js'; +import { consoleLog } from '#src/utils/console.js'; import { getTenantId } from '#src/utils/tenant.js'; import type { WithInteractionDetailsContext } from '../middleware/koa-interaction-details.js'; @@ -164,10 +168,45 @@ const parseUserProfile = async ( }; }; +const getInitialUserRoles = ( + isInAdminTenant: boolean, + isCreatingFirstAdminUser: boolean, + isCloud: boolean +) => + conditionalArray( + isInAdminTenant && AdminTenantRole.User, + isCreatingFirstAdminUser && getManagementApiAdminName(defaultTenantId), + isCreatingFirstAdminUser && isCloud && getManagementApiAdminName(adminTenantId) + ); + +/** Post affiliate data to the cloud service. */ +const postAffiliateLogs = async ( + ctx: IRouterContext, + cloudConnection: CloudConnectionLibrary, + userId: string, + tenantId: string +) => { + if (!EnvSet.values.isCloud || tenantId !== adminTenantId) { + return; + } + + const affiliateData = trySafe(() => + parseAffiliateData(JSON.parse(decodeURIComponent(ctx.cookies.get(defaults.cookieName) ?? ''))) + ); + + if (affiliateData) { + const client = await cloudConnection.getClient(); + await client.post('/api/affiliate-logs', { + body: { userId, ...affiliateData }, + }); + consoleLog.info('Affiliate logs posted', userId); + } +}; + export default async function submitInteraction( interaction: VerifiedInteractionResult, ctx: WithLogContext & WithInteractionDetailsContext & WithInteractionHooksContext, - { provider, libraries, connectors, queries }: TenantContext, + { provider, libraries, connectors, queries, cloudConnection, id: tenantId }: TenantContext, log?: LogEntry ) { const { hasActiveUsers, findUserById, updateUserById } = queries.users; @@ -196,11 +235,7 @@ export default async function submitInteraction( id, ...userProfile, }, - conditionalArray( - isInAdminTenant && AdminTenantRole.User, - isCreatingFirstAdminUser && getManagementApiAdminName(defaultTenantId), - isCreatingFirstAdminUser && isCloud && getManagementApiAdminName(adminTenantId) - ) + getInitialUserRoles(isInAdminTenant, isCreatingFirstAdminUser, isCloud) ); // In OSS, we need to limit sign-in experience to "sign-in only" once @@ -216,6 +251,10 @@ export default async function submitInteraction( log?.append({ userId: id }); appInsights.client?.trackEvent({ name: getEventName(Component.Core, CoreEvent.Register) }); + void trySafe(postAffiliateLogs(ctx, cloudConnection, id, tenantId), (error) => { + consoleLog.warn('Failed to post affiliate logs', error); + void appInsights.trackException(error); + }); return; } diff --git a/packages/schemas/alterations/next-1692194751-add-affiliate-scopes.ts b/packages/schemas/alterations/next-1692194751-add-affiliate-scopes.ts new file mode 100644 index 000000000..9e7d7a804 --- /dev/null +++ b/packages/schemas/alterations/next-1692194751-add-affiliate-scopes.ts @@ -0,0 +1,57 @@ +import { generateStandardId } from '@logto/shared/universal'; +import { sql } from 'slonik'; + +import type { AlterationScript } from '../lib/types/alteration.js'; + +const adminTenantId = 'admin'; + +const alteration: AlterationScript = { + up: async (pool) => { + // Get `resourceId` of the admin tenant's resource whose indicator is `https://cloud.logto.io/api`. + const { id: resourceId } = await pool.one<{ id: string }>(sql` + select id from resources + where tenant_id = ${adminTenantId} + and indicator = 'https://cloud.logto.io/api' + `); + + const { id: roleId } = await pool.one<{ id: string }>(sql` + select id from roles + where tenant_id = ${adminTenantId} + and name = 'admin:admin' + `); + + const createAffiliateId = generateStandardId(); + const manageAffiliateId = generateStandardId(); + + await pool.query(sql` + insert into scopes (tenant_id, id, name, description, resource_id) + values ( + ${adminTenantId}, + ${createAffiliateId}, + 'create:affiliate', + 'Allow creating new affiliates and logs.', + ${resourceId} + ), ( + ${adminTenantId}, + ${manageAffiliateId}, + 'manage:affiliate', + 'Allow managing affiliates, including create, update, and delete.', + ${resourceId} + ); + `); + + await pool.query(sql` + insert into roles_scopes (tenant_id, id, role_id, scope_id) values + (${adminTenantId}, ${generateStandardId()}, ${roleId}, ${createAffiliateId}), + (${adminTenantId}, ${generateStandardId()}, ${roleId}, ${manageAffiliateId}); + `); + }, + down: async (pool) => { + await pool.query(sql` + delete from scopes + where tenant_id = ${adminTenantId} and name = any(array['create:affiliate', 'manage:affiliate']); + `); + }, +}; + +export default alteration; diff --git a/packages/schemas/src/seeds/cloud-api.ts b/packages/schemas/src/seeds/cloud-api.ts index b09c8e8c9..edbe1fbf9 100644 --- a/packages/schemas/src/seeds/cloud-api.ts +++ b/packages/schemas/src/seeds/cloud-api.ts @@ -66,6 +66,11 @@ export const createCloudApi = (): Readonly<[UpdateAdminData, ...CreateScope[]]> CloudScope.SendSms, 'Allow sending SMS. This scope is only available to M2M application.' ), + buildScope(CloudScope.CreateAffiliate, 'Allow creating new affiliates and logs.'), + buildScope( + CloudScope.ManageAffiliate, + 'Allow managing affiliates, including create, update, and delete.' + ), ]); }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8b71e16cf..13f121231 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3124,6 +3124,9 @@ importers: '@koa/cors': specifier: ^4.0.0 version: 4.0.0 + '@logto/affiliate': + specifier: ^0.1.0 + version: 0.1.0 '@logto/app-insights': specifier: workspace:^1.3.1 version: link:../app-insights @@ -7290,6 +7293,14 @@ packages: dev: true optional: true + /@logto/affiliate@0.1.0: + resolution: {integrity: sha512-yDWSZMI2Qo/xoYU92tnwSP/gnSvq8+CLK5DqD/4brO42QJa7xjt7eA+HSyuMmSUrKffY2nP3riU81gs+nR8DkA==} + engines: {node: ^18.12.0} + dependencies: + '@silverhand/essentials': 2.8.2 + tiny-cookie: 2.4.1 + dev: false + /@logto/browser@2.1.0: resolution: {integrity: sha512-4XsXlCC0uZHcfazV09/4YKo4koqvSzQlkPUAToTp/WHpb6h2XDOJh5/hi55LXL4zp0PCcgpErKRxFCtgXCc6WQ==} dependencies: @@ -19705,7 +19716,6 @@ packages: /tiny-cookie@2.4.1: resolution: {integrity: sha512-h8ueaMyvUd/9ZfRqCfa1t+0tXqfVFhdK8WpLHz8VXMqsiaj3Sqg64AOCH/xevLQGZk0ZV+/75ouITdkvp3taVA==} - dev: true /tiny-glob@0.2.9: resolution: {integrity: sha512-g/55ssRPUjShh+xkfx9UPDXqhckHEsHr4Vd9zX55oSdGZc/MD0m3sferOkwWtp98bv+kcVfEHtRJgBVJzelrzg==}