0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2024-12-30 20:33:54 -05:00

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
This commit is contained in:
Gao Sun 2023-08-16 22:34:54 +08:00 committed by GitHub
parent 1c18111344
commit 2901f43e64
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 120 additions and 8 deletions

View file

@ -28,6 +28,7 @@
"@aws-sdk/client-s3": "^3.315.0", "@aws-sdk/client-s3": "^3.315.0",
"@azure/storage-blob": "^12.13.0", "@azure/storage-blob": "^12.13.0",
"@koa/cors": "^4.0.0", "@koa/cors": "^4.0.0",
"@logto/affiliate": "^0.1.0",
"@logto/app-insights": "workspace:^1.3.1", "@logto/app-insights": "workspace:^1.3.1",
"@logto/cli": "workspace:^1.7.0", "@logto/cli": "workspace:^1.7.0",
"@logto/connector-kit": "workspace:^1.1.1", "@logto/connector-kit": "workspace:^1.1.1",

View file

@ -1,3 +1,4 @@
import { defaults, parseAffiliateData } from '@logto/affiliate';
import { Component, CoreEvent, getEventName } from '@logto/app-insights/custom-event'; import { Component, CoreEvent, getEventName } from '@logto/app-insights/custom-event';
import { appInsights } from '@logto/app-insights/node'; import { appInsights } from '@logto/app-insights/node';
import type { User, Profile, CreateUser } from '@logto/schemas'; import type { User, Profile, CreateUser } from '@logto/schemas';
@ -11,14 +12,17 @@ import {
adminConsoleApplicationId, adminConsoleApplicationId,
} from '@logto/schemas'; } from '@logto/schemas';
import { type OmitAutoSetFields } from '@logto/shared'; 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 { 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 type { ConnectorLibrary } from '#src/libraries/connector.js';
import { assignInteractionResults } from '#src/libraries/session.js'; import { assignInteractionResults } from '#src/libraries/session.js';
import { encryptUserPassword } from '#src/libraries/user.js'; import { encryptUserPassword } from '#src/libraries/user.js';
import type { LogEntry, WithLogContext } from '#src/middleware/koa-audit-log.js'; import type { LogEntry, WithLogContext } from '#src/middleware/koa-audit-log.js';
import type TenantContext from '#src/tenants/TenantContext.js'; import type TenantContext from '#src/tenants/TenantContext.js';
import { consoleLog } from '#src/utils/console.js';
import { getTenantId } from '#src/utils/tenant.js'; import { getTenantId } from '#src/utils/tenant.js';
import type { WithInteractionDetailsContext } from '../middleware/koa-interaction-details.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<string>(
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( export default async function submitInteraction(
interaction: VerifiedInteractionResult, interaction: VerifiedInteractionResult,
ctx: WithLogContext & WithInteractionDetailsContext & WithInteractionHooksContext, ctx: WithLogContext & WithInteractionDetailsContext & WithInteractionHooksContext,
{ provider, libraries, connectors, queries }: TenantContext, { provider, libraries, connectors, queries, cloudConnection, id: tenantId }: TenantContext,
log?: LogEntry log?: LogEntry
) { ) {
const { hasActiveUsers, findUserById, updateUserById } = queries.users; const { hasActiveUsers, findUserById, updateUserById } = queries.users;
@ -196,11 +235,7 @@ export default async function submitInteraction(
id, id,
...userProfile, ...userProfile,
}, },
conditionalArray<string>( getInitialUserRoles(isInAdminTenant, isCreatingFirstAdminUser, isCloud)
isInAdminTenant && AdminTenantRole.User,
isCreatingFirstAdminUser && getManagementApiAdminName(defaultTenantId),
isCreatingFirstAdminUser && isCloud && getManagementApiAdminName(adminTenantId)
)
); );
// In OSS, we need to limit sign-in experience to "sign-in only" once // 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 }); log?.append({ userId: id });
appInsights.client?.trackEvent({ name: getEventName(Component.Core, CoreEvent.Register) }); 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; return;
} }

View file

@ -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;

View file

@ -66,6 +66,11 @@ export const createCloudApi = (): Readonly<[UpdateAdminData, ...CreateScope[]]>
CloudScope.SendSms, CloudScope.SendSms,
'Allow sending SMS. This scope is only available to M2M application.' '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.'
),
]); ]);
}; };

View file

@ -3124,6 +3124,9 @@ importers:
'@koa/cors': '@koa/cors':
specifier: ^4.0.0 specifier: ^4.0.0
version: 4.0.0 version: 4.0.0
'@logto/affiliate':
specifier: ^0.1.0
version: 0.1.0
'@logto/app-insights': '@logto/app-insights':
specifier: workspace:^1.3.1 specifier: workspace:^1.3.1
version: link:../app-insights version: link:../app-insights
@ -7290,6 +7293,14 @@ packages:
dev: true dev: true
optional: 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: /@logto/browser@2.1.0:
resolution: {integrity: sha512-4XsXlCC0uZHcfazV09/4YKo4koqvSzQlkPUAToTp/WHpb6h2XDOJh5/hi55LXL4zp0PCcgpErKRxFCtgXCc6WQ==} resolution: {integrity: sha512-4XsXlCC0uZHcfazV09/4YKo4koqvSzQlkPUAToTp/WHpb6h2XDOJh5/hi55LXL4zp0PCcgpErKRxFCtgXCc6WQ==}
dependencies: dependencies:
@ -19705,7 +19716,6 @@ packages:
/tiny-cookie@2.4.1: /tiny-cookie@2.4.1:
resolution: {integrity: sha512-h8ueaMyvUd/9ZfRqCfa1t+0tXqfVFhdK8WpLHz8VXMqsiaj3Sqg64AOCH/xevLQGZk0ZV+/75ouITdkvp3taVA==} resolution: {integrity: sha512-h8ueaMyvUd/9ZfRqCfa1t+0tXqfVFhdK8WpLHz8VXMqsiaj3Sqg64AOCH/xevLQGZk0ZV+/75ouITdkvp3taVA==}
dev: true
/tiny-glob@0.2.9: /tiny-glob@0.2.9:
resolution: {integrity: sha512-g/55ssRPUjShh+xkfx9UPDXqhckHEsHr4Vd9zX55oSdGZc/MD0m3sferOkwWtp98bv+kcVfEHtRJgBVJzelrzg==} resolution: {integrity: sha512-g/55ssRPUjShh+xkfx9UPDXqhckHEsHr4Vd9zX55oSdGZc/MD0m3sferOkwWtp98bv+kcVfEHtRJgBVJzelrzg==}