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:
parent
1c18111344
commit
2901f43e64
5 changed files with 120 additions and 8 deletions
|
@ -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",
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
|
@ -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.'
|
||||||
|
),
|
||||||
]);
|
]);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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==}
|
||||||
|
|
Loading…
Reference in a new issue