client/components/mma/identity/idapi/user.ts (209 lines of code) (raw):
import { get } from 'lodash';
import {
addCSRFToken,
fetchWithDefaultParameters,
postRequest,
putRequest,
} from '@/client/utilities/fetch';
import type { User, UserError } from '../models';
import { ErrorTypes } from '../models';
type UserPublicFields = Partial<Pick<User, 'username'>> & {
displayName?: string;
};
type UserPrivateFields = Partial<
Pick<
User,
| 'title'
| 'firstName'
| 'secondName'
| 'address1'
| 'address2'
| 'address3'
| 'address4'
| 'postcode'
| 'country'
| 'registrationLocation'
| 'registrationLocationState'
>
> & {
telephoneNumber?: {
countryCode: string;
localNumber: string;
};
};
// The api error message is displayed directly to the user unless
// you create an MMA specific error message here per field.
const userErrorMessageMap = new Map([
[
'user.privateFields.registrationLocation',
'Please select a location from the list or "I prefer not to say"',
],
[
'user.privateFields.registrationLocationState',
'Please select a state/territory from the list or "I prefer not to say"',
],
]);
export interface UserAPIResponse {
user: IdapiUserDetails;
}
interface IdapiUserDetails {
id: string;
consents: [
{
id: string;
consented: boolean;
},
];
publicFields: UserPublicFields;
privateFields: UserPrivateFields;
primaryEmailAddress: User['primaryEmailAddress'];
statusFields: {
userEmailValidated: boolean;
};
}
interface UserAPIRequest {
publicFields: UserPublicFields;
privateFields: UserPrivateFields;
primaryEmailAddress?: User['primaryEmailAddress'];
}
interface UserAPIErrorResponse {
status: string;
errors: Array<{
context: string;
description: string;
[key: string]: string;
}>;
}
const getOrEmpty = (user: IdapiUserDetails) => (path: string) =>
get(user, path, '');
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- we check input to see if it's a UserAPIErrorResponse object
const isErrorResponse = (error: any): error is UserAPIErrorResponse => {
return error.status && error.status === 'error';
};
const toUserApiRequest = (user: Partial<User>): UserAPIRequest => {
const { countryCode: countryCode, localNumber: localNumber } = user;
const telephoneNumber =
countryCode && localNumber
? { countryCode, localNumber: `${localNumber}` }
: undefined;
return {
publicFields: {
username: user.username,
// Currently displayname and username must be set to the same value, but this is not enforced on IDAPI
// and clients are expected to implement this logic for the time being.
displayName: user.username,
},
privateFields: {
title: user.title,
firstName: user.firstName,
secondName: user.secondName,
address1: user.address1,
address2: user.address2,
address3: user.address3,
address4: user.address4,
postcode: user.postcode,
country: user.country,
telephoneNumber,
registrationLocation: user.registrationLocation,
registrationLocationState: user.registrationLocationState,
},
primaryEmailAddress: user.primaryEmailAddress,
};
};
export const toUser = (response: UserAPIResponse): User => {
const consents = getConsentedTo(response);
const { user } = response;
const getFromUser = getOrEmpty(user);
return {
id: user.id,
primaryEmailAddress: user.primaryEmailAddress,
username: getFromUser('publicFields.username'),
title: getFromUser('privateFields.title'),
firstName: getFromUser('privateFields.firstName'),
secondName: getFromUser('privateFields.secondName'),
address1: getFromUser('privateFields.address1'),
address2: getFromUser('privateFields.address2'),
address3: getFromUser('privateFields.address3'),
address4: getFromUser('privateFields.address4'),
postcode: getFromUser('privateFields.postcode'),
country: getFromUser('privateFields.country'),
countryCode: getFromUser('privateFields.telephoneNumber.countryCode'),
localNumber: getFromUser('privateFields.telephoneNumber.localNumber'),
registrationLocation: getFromUser('privateFields.registrationLocation'),
registrationLocationState: getFromUser(
'privateFields.registrationLocationState',
),
consents,
// We don't always receive a full user response from IDAPI, so we shouldn't
// assume that the statusFields object is always present.
validated: user?.statusFields?.userEmailValidated,
};
};
const getConsentedTo = (response: UserAPIResponse) => {
if ('consents' in response.user) {
return response.user.consents
.filter((consent) => consent.consented)
.map((consent) => consent.id);
} else {
return [];
}
};
const getFieldNameFromContext = (context: string): string => {
const fieldname = context.split('.').pop() as string;
return fieldname === 'telephoneNumber' ? 'localNumber' : fieldname;
};
const toUserError = (response: UserAPIErrorResponse): UserError => {
const error = response.errors.reduce((a, e) => {
return {
...a,
[getFieldNameFromContext(e.context)]:
userErrorMessageMap.get(e.context) || e.description,
};
}, {} as UserError['error']);
return {
type: ErrorTypes.VALIDATION,
error,
};
};
export const write = async (user: Partial<User>): Promise<User> => {
const url = '/idapi/user';
const body = toUserApiRequest(user);
try {
const response = await fetchWithDefaultParameters(
url,
addCSRFToken(putRequest(body)),
).then((response) => response.json());
if (isErrorResponse(response)) {
const userErrorObj = toUserError(response);
throw new Error(`Error: ${userErrorObj.type}`, {
cause: userErrorObj.error,
});
}
return toUser(response);
} catch (e) {
throw isErrorResponse(e) ? toUserError(e) : e;
}
};
export const read = async (): Promise<User> => {
const url = '/idapi/user';
const response: UserAPIResponse = await fetchWithDefaultParameters(
url,
).then((response) => response.json());
return toUser(response);
};
export const setUsername = async (user: Partial<User>): Promise<User> => {
const url = '/idapi/user/username';
const body = {
publicFields: {
username: user.username,
},
};
try {
const response: UserAPIResponse = await fetchWithDefaultParameters(
url,
addCSRFToken(postRequest(body)),
).then((response) => response.json());
if (isErrorResponse(response)) {
const userErrorObj = toUserError(response);
throw new Error(`Error: ${userErrorObj.type}`, {
cause: userErrorObj.error,
});
}
return toUser(response);
} catch (e) {
throw isErrorResponse(e) ? toUserError(e) : e;
}
};