@pagamio/frontend-commons-lib
Version:
Pagamio library for Frontend reusable components like the form engine and table container
366 lines (365 loc) • 12.7 kB
JavaScript
import { jwtDecode } from 'jwt-decode';
/**
* Transforms authentication responses from the Events App API format into a standardized AuthResponse.
* This transformer handles responses that include a single token and role-based user information.
*
* Expected API response format:
* ```typescript
* {
* token: string;
* firstName: string;
* lastName: string;
* username: string;
* roles: string[] | null;
* roleName: string;
* roleDesc: string;
* vendorId: number;
* promoterId: number | null;
* company: string;
* }
* ```
*
* @implements {ApiResponseTransformer<EventsAppAuthConfig>}
*
* @example
* ```typescript
* const transformer = new EventsAppResponseTransformer();
* const response = {
* token: "jwt.token.here",
* firstName: "John",
* lastName: "Doe",
* username: "john.doe",
* roleName: "ADMIN"
* // ... other fields
* };
*
* if (transformer.canHandle(response)) {
* const authResponse = transformer.transform(response);
* // Use transformed response
* }
* ```
*/
export class EventsAppResponseTransformer {
/**
* Checks if the given response matches the Event App API format.
* Verifies the presence of required fields specific to Event App responses.
*
* @param response - The raw API response to check
* @returns True if the response contains 'token' and 'roleName' fields
*/
canHandle(response) {
return response.hasOwnProperty('token') && response.hasOwnProperty('roleName');
}
/**
* Transforms a Event App API response into the standardized AuthResponse format.
* Maps API-specific fields to the common auth response structure.
*
* @param response - The raw API response to transform
* @param remember
* @returns Standardized auth response with user and token information
* @throws Error if required fields are missing from the response
*/
transform(response, remember) {
// Decode the token using jwt-decode
let decodedToken;
try {
decodedToken = jwtDecode(response.token);
}
catch (error) {
console.error('Error decoding JWT token:', error);
throw new Error('Failed to decode JWT token. Please check the token format.');
}
if (!decodedToken.exp) {
throw new Error('Token does not contain an expiry time (exp claim)');
}
let expiresIn;
if (remember) {
// 7 days in seconds
expiresIn = 7 * 24 * 60 * 60;
}
else {
// 30 minutes in seconds
expiresIn = 30 * 60;
}
if (expiresIn <= 0) {
throw new Error('Token has already expired');
}
return {
user: {
id: response.userId,
userName: response.username,
firstName: response.firstName,
lastName: response.lastName,
roles: response.roles,
roleName: response.roleName,
roleDesc: response.roleDesc,
vendorId: response.vendorId,
promoterId: response.promoterId,
company: response.company,
hasIQRetailAccess: response.hasIQRetailAccess ?? false,
},
auth: {
accessToken: {
token: response.token,
expiresIn: expiresIn ?? 36000000,
},
},
};
}
}
/**
* Transforms authentication responses from the Vas App API format into a standardized AuthResponse.
* This transformer handles responses that include both access and refresh tokens.
*
* Expected API response format:
* ```typescript
* {
* accessToken: string;
* refreshToken: string;
* accessTokenExpiresIn: number;
* refreshTokenExpiresIn: number;
* username: string;
* roles: string[];
* userId: number;
* }
* ```
*
* @implements {ApiResponseTransformer<VasAppAuthConfig>}
*
* @example
* ```typescript
* const transformer = new VasAppResponseTransformer();
* const response = {
* accessToken: "access.token.here",
* refreshToken: "refresh.token.here",
* accessTokenExpiresIn: 3600,
* username: "vendor_user"
* // ... other fields
* };
*
* if (transformer.canHandle(response)) {
* const authResponse = transformer.transform(response);
* // Use transformed response
* }
* ```
*/
export class VasAppResponseTransformer {
/**
* Checks if the given response matches the Vas App API format.
* Verifies the presence of required fields specific to Vendor responses.
*
* @param response - The raw API response to check
* @returns True if the response contains both 'accessToken' and 'refreshToken' fields
*/
canHandle(response) {
return response.hasOwnProperty('accessToken') && response.hasOwnProperty('refreshToken');
}
/**
* Transforms a Vas App API response into the standardized AuthResponse format.
* Maps API-specific fields to the common auth response structure.
*
* @param response - The raw API response to transform
* @param remember
* @returns Standardized auth response with user and token information
* @throws Error if required fields are missing from the response
*/
transform(response, remember) {
let decodedToken;
try {
decodedToken = jwtDecode(response.accessToken);
}
catch (error) {
console.error('Error decoding JWT access token:', error);
throw new Error('Failed to decode JWT access token. Please check the token format.');
}
if (!decodedToken.exp) {
throw new Error('Access token does not contain an expiry time (exp claim)');
}
let accessExpiresIn;
if (remember) {
// 7 days in seconds
accessExpiresIn = 7 * 24 * 60 * 60;
}
else {
// 30 minutes in seconds
accessExpiresIn = 30 * 60;
}
if (accessExpiresIn <= 0) {
throw new Error('Access token has already expired');
}
const refreshExpiresIn = remember ? 30 * 24 * 60 * 60 : 24 * 60 * 60; // 30 days or 1 day
return {
user: {
id: response.userId,
userName: response.username,
roles: response.roles,
userId: response.userId,
userType: response.userType,
userTypeId: response.userTypeId,
hasWallet: response.hasWallet ?? false,
},
auth: {
accessToken: {
token: response.accessToken,
expiresIn: accessExpiresIn,
},
refreshToken: {
token: response.refreshToken,
expiresIn: refreshExpiresIn,
},
},
};
}
}
/**
* Transforms authentication responses from the Commerce App API format into a standardized AuthResponse.
* This transformer handles responses wrapped in a success/data structure.
*
* Expected API response format:
* ```typescript
* {
* success: boolean;
* data: {
* user: {...};
* accessToken: string;
* refreshToken: string;
* };
* }
* ```
*
* @implements {ApiResponseTransformer<CommerceAppAuthConfig>}
*/
export class CommerceAppResponseTransformer {
/**
* Checks if the given response matches the Commerce App API format.
* Verifies the presence of required fields specific to Commerce App responses.
*
* @param response - The raw API response to check
* @returns True if the response contains 'success' and 'data' fields with user and tokens
*/
canHandle(response) {
return (response.hasOwnProperty('success') &&
response.hasOwnProperty('data') &&
response.data?.hasOwnProperty('user') &&
response.data?.hasOwnProperty('accessToken'));
}
/**
* Transforms a Commerce App API response into the standardized AuthResponse format.
* Maps API-specific fields to the common auth response structure.
*
* @param response - The raw API response to transform
* @param remember - Whether to use extended token expiration
* @returns Standardized auth response with user and token information
* @throws Error if required fields are missing from the response
*/
transform(response, remember) {
const { data } = response;
// Decode access token to get expiration
let decodedAccessToken;
try {
decodedAccessToken = jwtDecode(data.accessToken);
}
catch (error) {
console.error('Error decoding JWT access token:', error);
throw new Error('Failed to decode JWT access token. Please check the token format.');
}
if (!decodedAccessToken.exp) {
throw new Error('Access token does not contain an expiry time (exp claim)');
}
// Calculate access token expiration in seconds from now
const currentTime = Math.floor(Date.now() / 1000);
const accessExpiresIn = decodedAccessToken.exp - currentTime;
if (accessExpiresIn <= 0) {
throw new Error('Access token has already expired');
}
// Decode refresh token to get expiration (if present)
let refreshExpiresIn;
if (data.refreshToken) {
try {
const decodedRefreshToken = jwtDecode(data.refreshToken);
if (decodedRefreshToken.exp) {
refreshExpiresIn = decodedRefreshToken.exp - currentTime;
}
}
catch (error) {
console.error('Error decoding JWT refresh token:', error);
// Use fallback if refresh token can't be decoded
refreshExpiresIn = remember ? 30 * 24 * 60 * 60 : 7 * 24 * 60 * 60; // 30 days or 7 days
}
}
return {
user: {
id: data.user.id,
userName: data.user.userName,
roles: data.user.roles || [],
onboardingStep: data.user.onboardingStep,
userId: data.user.id,
userType: data.user.userType || 'customer',
userTypeId: data.user.userTypeId || data.user.employeeId || data.user.id,
},
auth: {
accessToken: {
token: data.accessToken,
expiresIn: accessExpiresIn,
},
refreshToken: data.refreshToken && refreshExpiresIn
? {
token: data.refreshToken,
expiresIn: refreshExpiresIn,
}
: undefined,
},
};
}
}
/**
* Factory class for creating and managing response transformers.
* Implements the Factory pattern to dynamically select the appropriate transformer
* based on the API response format.
*
* @example
* ```typescript
* const apiResponse = await fetchFromApi();
* const transformer = ResponseTransformerFactory.getTransformer(apiResponse);
* const authResponse = transformer.transform(apiResponse);
* ```
*/
export class ResponseTransformerFactory {
/**
* Collection of available transformers.
* Add new transformer instances here to support additional API response formats.
*
* @private
* @static
*/
static transformers = [
new CommerceAppResponseTransformer(),
new EventsAppResponseTransformer(),
new VasAppResponseTransformer(),
];
/**
* Gets the appropriate transformer for the given API response.
* Iterates through available transformers and returns the first one that can handle the response.
*
* @param response - The raw API response to find a transformer for
* @returns The first transformer that can handle the response format
* @throws Error if no suitable transformer is found
*
* @example
* ```typescript
* try {
* const transformer = ResponseTransformerFactory.getTransformer(apiResponse);
* const authResponse = transformer.transform(apiResponse);
* } catch (error) {
* console.error('No suitable transformer found:', error);
* }
* ```
*/
static getTransformer(response) {
const transformer = this.transformers.find((t) => t.canHandle(response));
if (!transformer) {
throw new Error('No suitable transformer found for response format');
}
return transformer;
}
}