Skip to content

Commit

Permalink
Add identity integration
Browse files Browse the repository at this point in the history
  • Loading branch information
rupertbates committed Nov 11, 2024
1 parent b0043a2 commit 5a05381
Show file tree
Hide file tree
Showing 9 changed files with 953 additions and 71 deletions.
4 changes: 3 additions & 1 deletion handlers/press-reader-entitlements/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
"dependencies": {
"@aws-sdk/client-dynamodb": "^3.679.0",
"@aws-sdk/util-dynamodb": "^3.679.0",
"fast-xml-parser": "^4.5.0"
"@aws-sdk/client-ssm": "^3.679.0",
"fast-xml-parser": "^4.5.0",
"zod": "^3.23.8"
}
}
2 changes: 1 addition & 1 deletion handlers/press-reader-entitlements/src/dynamo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ export type SupporterRatePlanItem = {
};

export const getSupporterProductData = async (
identityId: string,
stage: Stage,
identityId: string,
): Promise<SupporterRatePlanItem[] | undefined> => {
const input = {
ExpressionAttributeValues: {
Expand Down
48 changes: 48 additions & 0 deletions handlers/press-reader-entitlements/src/identity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { GetParameterCommand, SSMClient } from '@aws-sdk/client-ssm';
import { awsConfig } from '@modules/aws/config';
import type { Stage } from '@modules/stage';
import { getIdentityIdSchema } from './schemas';

export async function getIdentityClientAccessToken() {
const ssmClient = new SSMClient(awsConfig);

const params = {
Name: '/CODE/support/press-reader-entitlements/identity-client-access-token',
WithDecryption: true,
};

const command = new GetParameterCommand(params);

const response = await ssmClient.send(command);
return response.Parameter?.Value;
}

export async function getIdentityId(stage: Stage, userId: string) {
const identityHost =
stage === 'CODE'
? 'https://idapi.code.dev-theguardian.com'
: 'https://idapi.theguardian.com';
const clientAccessToken = await getIdentityClientAccessToken();
if (clientAccessToken == undefined) {
throw new Error('Client access token not found');
}
const response = await fetch(`${identityHost}/user/braze-uuid/${userId}`, {
headers: {
'X-GU-ID-Client-Access-Token': `Bearer ${clientAccessToken}`,
},
method: 'GET',
})
.then(async (res) => {
const json = await res.json();
console.log(`Identity returned ${JSON.stringify(json)}`);
return json;
})
.then((json) => getIdentityIdSchema.parse(json));

if (response.status === 'ok') {
return response.id;
}
throw new Error(
`Failed to get identity id because of ${JSON.stringify(response)}`,
);
}
66 changes: 24 additions & 42 deletions handlers/press-reader-entitlements/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import type {
} from 'aws-lambda';
import type { SupporterRatePlanItem } from './dynamo';
import { getSupporterProductData } from './dynamo';
import { getIdentityId } from './identity';
import type { Member } from './xmlBuilder';
import { buildXml } from './xmlBuilder';

Expand All @@ -31,70 +32,55 @@ export const handler: Handler = async (
'userId does not exist',
);

const memberDetails = await getMemberDetails(userId, stage);
return await Promise.resolve({
const memberDetails = await getMemberDetails(stage, userId);
return {
body: buildXml(memberDetails),
statusCode: 200,
});
};
}

return await Promise.resolve({
return {
body: 'Not found',
statusCode: 404,
});
};
} catch (error) {
console.log('Caught exception with message: ', error);
if (error instanceof IdentityError) {
return await Promise.resolve({
return {
body: 'User not found',
statusCode: 404,
});
};
}

return await Promise.resolve({
return {
body: 'Internal server error',
statusCode: 500,
});
};
}
};

type UserDetails = {
userID: string;
firstname: string;
lastname: string;
};

class IdentityError extends Error {
constructor(message: string) {
super(message);
this.name = 'IdentityError';
}
}

async function getUserDetails(
export async function getMemberDetails(
stage: Stage,
userId: string,
): Promise<{ identityId: string; user: UserDetails }> {
// ToDo: get Identity ID from Identity, using a Braze UUID as userId, if we can't - return a 404
try {
return Promise.resolve({
identityId: '123',
user: {
userID: userId,
firstname: 'Joe',
lastname: 'Bloggs',
},
});
} catch (error) {
throw new IdentityError(JSON.stringify(error));
}
): Promise<Member> {
const identityId = await getIdentityId(stage, userId);
const latestSubscription = await getLatestSubscription(stage, identityId);
return createMember(userId, latestSubscription);
}

function createMember(
userDetails: UserDetails,
userId: string,
latestSubscription: SupporterRatePlanItem | undefined,
): Member {
return {
...userDetails,
userID: userId,
products: latestSubscription
? [
{
Expand All @@ -116,29 +102,25 @@ function createMember(
};
}

export async function getMemberDetails(
userId: string,
export async function getLatestSubscription(
stage: Stage,
): Promise<Member> {
const { identityId, user } = await getUserDetails(userId);

identityId: string,
): Promise<SupporterRatePlanItem | undefined> {
const supporterProductDataItems = await getSupporterProductData(
identityId,
stage,
identityId,
);

if (supporterProductDataItems) {
const productCatalog = await getProductCatalogFromApi(stage);

const latestSubscription = getLatestValidSubscription(
return getLatestValidSubscription(
productCatalog,
supporterProductDataItems,
);

return createMember(user, latestSubscription);
}

return createMember(user, undefined);
return undefined;
}

export function getLatestValidSubscription(
Expand Down
21 changes: 21 additions & 0 deletions handlers/press-reader-entitlements/src/schemas.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { z } from 'zod';

const ok = z.literal('ok');
const error = z.literal('error');

const successfulGetIdentityIdResponse = z.object({
status: ok,
id: z.string(),
});

const failedGetIdentityIdResponse = z.object({
status: error,
errors: z.array(z.object({ message: z.string(), description: z.string() })),
});

export const getIdentityIdSchema = z.discriminatedUnion('status', [
successfulGetIdentityIdResponse,
failedGetIdentityIdResponse,
]);

export type GetIdentityIdResponse = z.infer<typeof getIdentityIdSchema>;
2 changes: 0 additions & 2 deletions handlers/press-reader-entitlements/src/xmlBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@ type Product = {

export type Member = {
userID: string;
firstname: string;
lastname: string;
products: Product[];
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,6 @@ describe('xmlBuilder', () => {

const member: Member = {
userID: '123456',
firstname: 'John',
lastname: 'Doe',
products: [
{
product: {
Expand Down Expand Up @@ -62,8 +60,6 @@ describe('xmlBuilder', () => {

const member: Member = {
userID: '123456',
firstname: 'John',
lastname: 'Doe',
products: [],
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,59 @@
* @group integration
*/

import { getMemberDetails } from '../src';
import { describe } from 'node:test';
import { getLatestSubscription, getMemberDetails } from '../src';
import { getSupporterProductData } from '../src/dynamo';
import { getIdentityClientAccessToken, getIdentityId } from '../src/identity';

test('Dynamo Integration', async () => {
const supporterData = await getSupporterProductData('110001137', 'CODE');
const supporterData = await getSupporterProductData('CODE', '110001137');
expect(supporterData?.length).toEqual(4);
});

describe('Product Catalog integration', () => {
test('Entitlements check', async () => {
const memberDetails = await getMemberDetails('110001137', 'CODE');
expect(memberDetails.products.length).toEqual(2);
const memberDetails = await getLatestSubscription('CODE', '110001137');
expect(memberDetails).toBeDefined();
});
});

test('getIdentityClientAccessToken', async () => {
const accessToken = await getIdentityClientAccessToken();
expect(accessToken).toBeDefined();
});

test('getIdentityId', async () => {
const identityId = await getIdentityId(
'CODE',
'c20da7c7-4f72-44fb-b719-78879bfab70d',
);
expect(identityId).toBe('200149752');
});

test('getMemberDetails', async () => {
const expected = {
userID: 'c20da7c7-4f72-44fb-b719-78879bfab70d',
products: [
{
product: {
productID: 'the-guardian',
enddate: '2025-09-08',
startdate: '2023-09-08',
},
},
{
product: {
productID: 'the-observer',
enddate: '2025-09-08',
startdate: '2023-09-08',
},
},
],
};
const memberDetails = await getMemberDetails(
'CODE',
'c20da7c7-4f72-44fb-b719-78879bfab70d',
);
expect(memberDetails).toStrictEqual(expected);
});
Loading

0 comments on commit 5a05381

Please sign in to comment.