Skip to content

Commit

Permalink
feat(RSS-ECOMM-3_98): add all data in one request (#229)
Browse files Browse the repository at this point in the history
* feat: add all data in one request

* feat: add refresh token, fix auth, refactor client
  • Loading branch information
YulikK authored May 13, 2024
1 parent 81af873 commit 0df859f
Show file tree
Hide file tree
Showing 6 changed files with 182 additions and 170 deletions.
146 changes: 88 additions & 58 deletions src/shared/API/sdk/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,8 @@ import {
type Client,
ClientBuilder,
type HttpMiddlewareOptions,
type Middleware,
type PasswordAuthMiddlewareOptions,
type RefreshAuthMiddlewareOptions,
createAuthForAnonymousSessionFlow,
createAuthForClientCredentialsFlow,
createAuthForPasswordFlow,
} from '@commercetools/sdk-client-v2';

import type { TokenTypeType } from '../types/type.ts';
Expand All @@ -21,7 +17,7 @@ import getTokenCache from './token-cache/token-cache.ts';

const URL_AUTH = 'https://auth.europe-west1.gcp.commercetools.com';
const URL_HTTP = 'https://api.europe-west1.gcp.commercetools.com';
const USE_SAVE_TOKEN = false;
const USE_SAVE_TOKEN = true;

const httpMiddlewareOptions: HttpMiddlewareOptions = {
fetch,
Expand Down Expand Up @@ -51,35 +47,21 @@ export default class ApiClient {
this.clientSecret = clientSecret;
this.scopes = scopes.split(',');

this.anonymConnection = this.createAnonymConnection();

const adminOptions = createAuthForClientCredentialsFlow({
credentials: {
clientId: this.clientID,
clientSecret: this.clientSecret,
},
fetch,
host: URL_AUTH,
projectKey: this.projectKey,
scopes: this.scopes,
});

const adminClient = this.getAdminClient(adminOptions);

this.adminConnection = this.getConnection(adminClient);
}
if (USE_SAVE_TOKEN && getTokenCache(TokenType.AUTH).isExist()) {
this.authConnection = this.createAuthConnectionWithRefreshToken();
this.isAuth = true;
} else {
this.anonymConnection = this.createAnonymConnection();
}

private getAdminClient(middleware: Middleware): Client {
return new ClientBuilder()
.withProjectKey(this.projectKey)
.withHttpMiddleware(httpMiddlewareOptions)
.withMiddleware(middleware)
.build();
this.adminConnection = this.createAdminConnection();
}

private getAuthOption(credentials: UserCredentials): Middleware {
private addAuthMiddleware(
defaultOptions: AuthMiddlewareOptions,
credentials: UserCredentials,
): PasswordAuthMiddlewareOptions {
const { email, password } = credentials || { email: '', password: '' };
const defaultOptions = this.getDefaultOptions(TokenType.AUTH);
const authOptions: PasswordAuthMiddlewareOptions = {
...defaultOptions,
credentials: {
Expand All @@ -90,28 +72,74 @@ export default class ApiClient {
},
},
};
return createAuthForPasswordFlow(authOptions);
return authOptions;
}

private getClient(middleware: Middleware, token: TokenTypeType): Client {
const defaultOptions = this.getDefaultOptions(token);
const opt: RefreshAuthMiddlewareOptions = {
...defaultOptions,
refreshToken: token,
};
return new ClientBuilder()
.withProjectKey(this.projectKey)
.withHttpMiddleware(httpMiddlewareOptions)
.withMiddleware(middleware)
.withRefreshTokenFlow(opt)
.build();
private addRefreshMiddleware(
tokenType: TokenTypeType,
client: ClientBuilder,
defaultOptions: AuthMiddlewareOptions,
): void {
const { refreshToken } = getTokenCache(tokenType).get();
if (refreshToken) {
const optionsRefreshToken: RefreshAuthMiddlewareOptions = {
...defaultOptions,
refreshToken,
};
client.withRefreshTokenFlow(optionsRefreshToken);
}
}

private createAdminConnection(): ByProjectKeyRequestBuilder {
const defaultOptions = this.getDefaultOptions();
const client = this.getDefaultClient();

client.withClientCredentialsFlow(defaultOptions);

this.adminConnection = this.getConnection(client.build());
return this.adminConnection;
}

private createAnonymConnection(): ByProjectKeyRequestBuilder {
const defaultOptions = this.getDefaultOptions(TokenType.ANONYM);
const client = this.getDefaultClient();

client.withAnonymousSessionFlow(defaultOptions);

this.addRefreshMiddleware(TokenType.ANONYM, client, defaultOptions);

this.anonymConnection = this.getConnection(client.build());
return this.anonymConnection;
}

private createAuthConnectionWithRefreshToken(): ByProjectKeyRequestBuilder {
if (!this.authConnection || (this.authConnection && !this.isAuth)) {
const defaultOptions = this.getDefaultOptions(TokenType.AUTH);
const client = this.getDefaultClient();

this.addRefreshMiddleware(TokenType.AUTH, client, defaultOptions);

this.authConnection = this.getConnection(client.build());
}
return this.authConnection;
}

private deleteAnonymConnection(): boolean {
this.anonymConnection = null;
getTokenCache(TokenType.ANONYM).clear();

return this.anonymConnection === null;
}

private getConnection(client: Client): ByProjectKeyRequestBuilder {
return createApiBuilderFromCtpClient(client).withProjectKey({ projectKey: this.projectKey });
}

private getDefaultOptions(tokenType: TokenTypeType): AuthMiddlewareOptions {
private getDefaultClient(): ClientBuilder {
return new ClientBuilder().withProjectKey(this.projectKey).withHttpMiddleware(httpMiddlewareOptions);
}

private getDefaultOptions(tokenType?: TokenTypeType): AuthMiddlewareOptions {
return {
credentials: {
clientId: this.clientID,
Expand All @@ -121,7 +149,7 @@ export default class ApiClient {
host: URL_AUTH,
projectKey: this.projectKey,
scopes: this.scopes,
tokenCache: USE_SAVE_TOKEN ? getTokenCache(tokenType) : undefined,
tokenCache: USE_SAVE_TOKEN && tokenType ? getTokenCache(tokenType) : undefined,
};
}

Expand All @@ -130,7 +158,11 @@ export default class ApiClient {
}

public apiRoot(): ByProjectKeyRequestBuilder {
let client = this.authConnection && this.isAuth ? this.authConnection : this.anonymConnection;
let client =
(this.authConnection && this.isAuth) || (this.authConnection && !this.anonymConnection)
? this.authConnection
: this.anonymConnection;

if (!client) {
client = this.createAnonymConnection();
}
Expand All @@ -139,31 +171,29 @@ export default class ApiClient {

public approveAuth(): boolean {
this.isAuth = true;
this.deleteAnonymConnection();
return this.isAuth;
}

public createAnonymConnection(): ByProjectKeyRequestBuilder {
const defaultOptions = this.getDefaultOptions(TokenType.ANONYM);
const anonymOptions = createAuthForAnonymousSessionFlow(defaultOptions);
const anonymClient = this.getClient(anonymOptions, TokenType.ANONYM);
this.anonymConnection = this.getConnection(anonymClient);

return this.anonymConnection;
}

public createAuthConnection(credentials: UserCredentials): ByProjectKeyRequestBuilder {
if (!this.authConnection || (this.authConnection && !this.isAuth)) {
const authOptions = this.getAuthOption(credentials);
const authClient = this.getClient(authOptions, TokenType.AUTH);
this.authConnection = this.getConnection(authClient);
const defaultOptions = this.getDefaultOptions(TokenType.AUTH);
const client = this.getDefaultClient();

const authOptions = this.addAuthMiddleware(defaultOptions, credentials);

client.withPasswordFlow(authOptions);
this.authConnection = this.getConnection(client.build());
}
return this.authConnection;
}

public deleteAuthConnection(): boolean {
this.authConnection = null;
this.isAuth = false;
getTokenCache(TokenType.AUTH).clear();
this.anonymConnection = this.createAnonymConnection();

return this.authConnection === null;
}
}
43 changes: 32 additions & 11 deletions src/shared/API/sdk/root.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,24 @@
import type { User, UserCredentials } from '@/shared/types/user.ts';

import { DEFAULT_PAGE, MAX_PRICE, MIN_PRICE, PRODUCT_LIMIT } from '@/shared/constants/product.ts';
import findAddressIndex from '@/shared/utils/address.ts';
import {
type CategoryPagedQueryResponse,
type ClientResponse,
type Customer,
type CustomerPagedQueryResponse,
type CustomerSignInResult,
type MyCustomerDraft,
type MyCustomerUpdateAction,
type Product,
type ProductProjectionPagedQueryResponse,
type ProductProjectionPagedSearchResponse,
} from '@commercetools/platform-sdk';

import makeSortRequest from '../product/utils/sort.ts';
import { type OptionsRequest, TokenType } from '../types/type.ts';
import { type OptionsRequest } from '../types/type.ts';
import { isErrorResponse } from '../types/validation.ts';
import ApiClient from './client.ts';
import getTokenCache from './token-cache/token-cache.ts';

type Nullable<T> = T | null;

Expand Down Expand Up @@ -54,9 +55,30 @@ export class RootApi {
);
}

private makeCustomerDraft(userData: User): MyCustomerDraft {
const billingAddress = userData.defaultBillingAddressId
? findAddressIndex(userData.addresses, userData.defaultBillingAddressId)
: null;
const shippingAddress = userData.defaultShippingAddressId
? findAddressIndex(userData.addresses, userData.defaultShippingAddressId)
: null;

return {
addresses: [...userData.addresses],
dateOfBirth: userData.birthDate,
...(billingAddress !== null && billingAddress >= 0 && { defaultBillingAddress: billingAddress }),
...(shippingAddress !== null && shippingAddress >= 0 && { defaultShippingAddress: shippingAddress }),
email: userData.email,
firstName: userData.firstName,
lastName: userData.lastName,
locale: userData.locale,
password: userData.password,
};
}

public async authenticateUser(userLoginData: UserCredentials): Promise<ClientResponse<CustomerSignInResult>> {
this.client.createAuthConnection(userLoginData);
const data = await this.client.apiRoot().me().login().post({ body: userLoginData }).execute();
const client = this.client.createAuthConnection(userLoginData);
const data = await client.me().login().post({ body: userLoginData }).execute();
if (!isErrorResponse(data)) {
this.client.approveAuth();
}
Expand Down Expand Up @@ -176,17 +198,16 @@ export class RootApi {
}

public logoutUser(): boolean {
getTokenCache(TokenType.AUTH).clear();
return this.client.deleteAuthConnection();
}

public async registrationUser(userData: User): Promise<ClientResponse<CustomerSignInResult>> {
const userCredentials = {
email: userData.email,
password: userData.password,
};

const data = await this.client.apiRoot().me().signup().post({ body: userCredentials }).execute();
const data = await this.client
.apiRoot()
.me()
.signup()
.post({ body: this.makeCustomerDraft(userData) })
.execute();
if (!isErrorResponse(data)) {
this.client.createAuthConnection(userData);
this.client.approveAuth();
Expand Down
32 changes: 20 additions & 12 deletions src/shared/API/sdk/token-cache/token-cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import Cookies from 'js-cookie';
import type { TokenTypeType } from '../../types/type.ts';

import { TokenType } from '../../types/type.ts';
import { isTokenType } from '../../types/validation.ts';

const NAME = 'token-data';

export class MyTokenCache implements TokenCache {
private myCache: TokenStore = {
Expand All @@ -17,19 +20,26 @@ export class MyTokenCache implements TokenCache {

constructor(name: string) {
this.name = name;
const token = Cookies.get(`${this.name}-token`);
const expirationTime = Number(Cookies.get(`${this.name}-expirationTime`));
const refreshToken = Cookies.get(`${this.name}-refreshToken`);
if (token && refreshToken) {
this.myCache = { expirationTime, refreshToken, token };
const soreData = Cookies.get(`${this.name}-${NAME}`);
if (soreData) {
const cookieData: unknown = JSON.parse(soreData);
if (isTokenType(cookieData)) {
const { expirationTime, refreshToken, token } = cookieData;
if (token && refreshToken) {
this.myCache = { expirationTime, refreshToken, token };
}
}
}
}

private saveToken(): void {
if (this.myCache.token) {
Cookies.set(`${this.name}-token`, this.myCache.token);
Cookies.set(`${this.name}-expirationTime`, this.myCache.expirationTime.toString());
Cookies.set(`${this.name}-refreshToken`, this.myCache.refreshToken || '');
const cookieData = {
expirationTime: this.myCache.expirationTime.toString(),
refreshToken: this.myCache.refreshToken || '',
token: this.myCache.token,
};
Cookies.set(`${this.name}-${NAME}`, JSON.stringify(cookieData));
}
}

Expand All @@ -39,9 +49,7 @@ export class MyTokenCache implements TokenCache {
refreshToken: undefined,
token: '',
};
Cookies.remove(`${this.name}-token`);
Cookies.remove(`${this.name}-expirationTime`);
Cookies.remove(`${this.name}-refreshToken`);
Cookies.remove(`${this.name}-${NAME}`);
}

public get(): TokenStore {
Expand All @@ -64,5 +72,5 @@ const anonymTokenCache = createTokenCache(TokenType.ANONYM);
const authTokenCache = createTokenCache(TokenType.AUTH);

export default function getTokenCache(tokenType?: TokenTypeType): MyTokenCache {
return tokenType === TokenType.AUTH || authTokenCache.isExist() ? authTokenCache : anonymTokenCache;
return tokenType === TokenType.AUTH ? authTokenCache : anonymTokenCache;
}
14 changes: 14 additions & 0 deletions src/shared/API/types/validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import type {
RangeFacetResult,
TermFacetResult,
} from '@commercetools/platform-sdk';
import type { TokenStore } from '@commercetools/sdk-client-v2';

export function isClientResponse(data: unknown): data is ClientResponse {
return Boolean(
Expand Down Expand Up @@ -193,3 +194,16 @@ export function isErrorResponse(data: unknown): data is ErrorResponse {
typeof data.message === 'string',
);
}

export function isTokenType(data: unknown): data is TokenStore {
return Boolean(
typeof data === 'object' &&
data &&
'expirationTime' in data &&
typeof data.expirationTime === 'number' &&
'refreshToken' in data &&
typeof data.refreshToken === 'string' &&
'token' in data &&
typeof data.token === 'string',
);
}
Loading

0 comments on commit 0df859f

Please sign in to comment.