typescript/src/user/user.ts (127 lines of code) (raw):

import 'source-map-support/register'; import type { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda'; import { HTTPResponses } from '../models/apiGatewayHttp'; import type { Subscription } from '../models/subscription'; import { SubscriptionEmpty } from '../models/subscription'; import { UserSubscriptionEmpty } from '../models/userSubscription'; import { dynamoMapper } from '../utils/aws'; import { plusDays } from '../utils/dates'; import type { UserIdResolution } from '../utils/guIdentityApi'; import { getAuthToken, getUserId } from '../utils/guIdentityApi'; import { mapPlatformToSoftOptInProductName } from '../utils/softOptIns'; import { getConfigValue } from '../utils/ssmConfig'; interface SubscriptionStatus { subscriptionId: string; from: string; to: string; cancellationTimestamp?: string; valid: boolean; gracePeriod: boolean; autoRenewing: boolean; productId: string; softOptInProductName: string; } interface SubscriptionStatusResponse { subscriptions: SubscriptionStatus[]; } async function getUserSubscriptionIds(userId: string): Promise<string[]> { const subs: string[] = []; // ( comment group #488db8c1 ) // TODO: // In PR: https://github.com/guardian/mobile-purchases/pull/1698 // we performed a renaming of ReadSubscription to UserSubscriptionEmpty // With that said it should now be possible to use UserSubscription instead of // UserSubscriptionEmpty as first argument of the dynamoMapper.query( const subscriptionResults = dynamoMapper.query(UserSubscriptionEmpty, { userId: userId, }); for await (const sub of subscriptionResults) { subs.push(sub.subscriptionId); } return subs; } async function getSubscriptions( subscriptionIds: string[], ): Promise<SubscriptionStatusResponse> { const subs: SubscriptionEmpty[] = []; const toGet = subscriptionIds.map((subscriptionId) => new SubscriptionEmpty().setSubscriptionId(subscriptionId), ); for await (const sub of dynamoMapper.batchGet(toGet)) { subs.push(sub); } const sortedSubs = subs.sort( (subscriptionA: Subscription, subscriptionB: Subscription) => { const endTimeA = (subscriptionA.endTimestamp && Date.parse(subscriptionA.endTimestamp)) || 0; const endTimeB = (subscriptionB.endTimestamp && Date.parse(subscriptionB.endTimestamp)) || 0; return endTimeA - endTimeB; }, ); const now = new Date(); const subscriptionStatuses: SubscriptionStatus[] = sortedSubs.map((sub) => { const end = new Date(Date.parse(sub.endTimestamp)); const endWithGracePeriod = plusDays(end, 30); const valid: boolean = now.getTime() <= endWithGracePeriod.getTime(); const gracePeriod: boolean = now.getTime() > end.getTime() && valid; return { subscriptionId: sub.subscriptionId, from: sub.startTimestamp, to: sub.endTimestamp, cancellationTimestamp: sub.cancellationTimestamp, valid: valid, gracePeriod: gracePeriod, autoRenewing: sub.autoRenewing, productId: sub.productId, softOptInProductName: mapPlatformToSoftOptInProductName(sub.platform), }; }); return { subscriptions: subscriptionStatuses, }; } async function apiKeysConfig(): Promise<string[]> { // returning an array just in case we get more than one client one day const apiKey0Default = await getConfigValue<string>('user.api-key.0'); const apiKey1Salesforce = await getConfigValue<string>( 'user.api-key.1.salesforce', ); return [apiKey0Default, apiKey1Salesforce]; } export async function handler( httpRequest: APIGatewayProxyEvent, ): Promise<APIGatewayProxyResult> { try { const apiKeys = await apiKeysConfig(); const authToken = getAuthToken(httpRequest.headers); let userId: string; if (authToken && apiKeys.includes(authToken)) { if (httpRequest.pathParameters && httpRequest.pathParameters['userId']) { userId = httpRequest.pathParameters['userId']; } else { return HTTPResponses.INVALID_REQUEST; } } else { const resolution: UserIdResolution = await getUserId(httpRequest.headers); switch (resolution.status) { case 'incorrect-token': { return HTTPResponses.UNAUTHORISED; } case 'incorrect-scope': { return HTTPResponses.FORBIDDEN; } case 'missing-identity-id': { return HTTPResponses.INVALID_REQUEST; } case 'success': { userId = resolution.userId as string; break; } } } const userSubscriptionIds = await getUserSubscriptionIds(userId); const subscriptionStatuses = await getSubscriptions(userSubscriptionIds); return { statusCode: 200, body: JSON.stringify(subscriptionStatuses) }; } catch (error) { console.log(`Error retrieving user subscriptions: ${error}`); return HTTPResponses.INTERNAL_ERROR; } }