diff --git a/src/shared/API/sdk/client.ts b/src/shared/API/sdk/client.ts index 3b1cf79e..4f68c36a 100644 --- a/src/shared/API/sdk/client.ts +++ b/src/shared/API/sdk/client.ts @@ -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'; @@ -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, @@ -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: { @@ -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, @@ -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, }; } @@ -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(); } @@ -139,23 +171,19 @@ 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; } @@ -163,7 +191,9 @@ export default class ApiClient { public deleteAuthConnection(): boolean { this.authConnection = null; this.isAuth = false; + getTokenCache(TokenType.AUTH).clear(); this.anonymConnection = this.createAnonymConnection(); + return this.authConnection === null; } } diff --git a/src/shared/API/sdk/root.ts b/src/shared/API/sdk/root.ts index 04caa26a..1ee46bab 100644 --- a/src/shared/API/sdk/root.ts +++ b/src/shared/API/sdk/root.ts @@ -1,12 +1,14 @@ 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, @@ -14,10 +16,9 @@ import { } 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 | null; @@ -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> { - 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(); } @@ -176,17 +198,16 @@ export class RootApi { } public logoutUser(): boolean { - getTokenCache(TokenType.AUTH).clear(); return this.client.deleteAuthConnection(); } public async registrationUser(userData: User): Promise> { - 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(); diff --git a/src/shared/API/sdk/token-cache/token-cache.ts b/src/shared/API/sdk/token-cache/token-cache.ts index ea34a8c0..6e3a1e37 100644 --- a/src/shared/API/sdk/token-cache/token-cache.ts +++ b/src/shared/API/sdk/token-cache/token-cache.ts @@ -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 = { @@ -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)); } } @@ -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 { @@ -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; } diff --git a/src/shared/API/types/validation.ts b/src/shared/API/types/validation.ts index ce2bf6ec..1ca71275 100644 --- a/src/shared/API/types/validation.ts +++ b/src/shared/API/types/validation.ts @@ -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( @@ -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', + ); +} diff --git a/src/shared/utils/address.ts b/src/shared/utils/address.ts new file mode 100644 index 00000000..6f827507 --- /dev/null +++ b/src/shared/utils/address.ts @@ -0,0 +1,13 @@ +import type { Address } from '@commercetools/platform-sdk'; + +export default function findAddressIndex(addresses: Address[], targetAddress: Address): null | number { + const index = addresses?.findIndex( + (address) => + address.city === targetAddress?.city && + address.country === targetAddress?.country && + address.postalCode === targetAddress?.postalCode && + address.streetName === targetAddress?.streetName && + address.streetNumber === targetAddress?.streetNumber, + ); + return index !== undefined && index >= 0 ? index : null; +} diff --git a/src/widgets/RegistrationForm/model/RegistrationFormModel.ts b/src/widgets/RegistrationForm/model/RegistrationFormModel.ts index 4409d9f9..c6c1ba38 100644 --- a/src/widgets/RegistrationForm/model/RegistrationFormModel.ts +++ b/src/widgets/RegistrationForm/model/RegistrationFormModel.ts @@ -1,9 +1,9 @@ import type InputFieldModel from '@/entities/InputField/model/InputFieldModel.ts'; import type { AddressType } from '@/shared/types/address.ts'; -import type { Address, PersonalData, User } from '@/shared/types/user.ts'; +import type { PersonalData, User } from '@/shared/types/user.ts'; import AddressModel from '@/entities/Address/model/AddressModel.ts'; -import getCustomerModel, { CustomerModel } from '@/shared/API/customer/model/CustomerModel.ts'; +import getCustomerModel from '@/shared/API/customer/model/CustomerModel.ts'; import LoaderModel from '@/shared/Loader/model/LoaderModel.ts'; import serverMessageModel from '@/shared/ServerMessage/model/ServerMessageModel.ts'; import getStore from '@/shared/Store/Store.ts'; @@ -35,39 +35,6 @@ class RegisterFormModel { this.init(); } - private async addAddress(address: Address, userData: User | null): Promise { - let currentUserData = userData; - if (currentUserData) { - currentUserData = await getCustomerModel().editCustomer( - [CustomerModel.actionAddAddress(address)], - currentUserData, - ); - } - return currentUserData; - } - - private async editDefaultBillingAddress(addressId: string, userData: User | null): Promise { - let currentUserData = userData; - if (currentUserData) { - currentUserData = await getCustomerModel().editCustomer( - [CustomerModel.actionEditDefaultBillingAddress(addressId)], - currentUserData, - ); - } - return currentUserData; - } - - private async editDefaultShippingAddress(addressId: string, userData: User | null): Promise { - let currentUserData = userData; - if (currentUserData) { - currentUserData = await getCustomerModel().editCustomer( - [CustomerModel.actionEditDefaultShippingAddress(addressId)], - currentUserData, - ); - } - return currentUserData; - } - private getFormUserData(): User { const userData: User = { addresses: [], @@ -78,7 +45,7 @@ class RegisterFormModel { firstName: formattedText(this.view.getFirstNameField().getView().getValue()), id: '', lastName: formattedText(this.view.getLastNameField().getView().getValue()), - locale: '', + locale: getStore().getState().currentLanguage, password: this.view.getPasswordField().getView().getValue(), version: 0, }; @@ -120,11 +87,14 @@ class RegisterFormModel { private registerUser(): void { const loader = new LoaderModel(LOADER_SIZE.SMALL).getHTML(); this.view.getSubmitFormButton().getHTML().append(loader); + const customer = this.getFormUserData(); + this.updateUserAddresses(customer); getCustomerModel() - .registerNewCustomer(this.getFormUserData()) + .registerNewCustomer(customer) .then((newUserData) => { if (newUserData) { - this.successfulUserRegistration(newUserData); + getStore().dispatch(setCurrentUser(newUserData)); + getStore().dispatch(switchIsUserLoggedIn(true)); serverMessageModel.showServerMessage( SERVER_MESSAGE[getStore().getState().currentLanguage].SUCCESSFUL_REGISTRATION, MESSAGE_STATUS.SUCCESS, @@ -205,20 +175,6 @@ class RegisterFormModel { return true; } - private successfulUserRegistration(newUserData: User): void { - const loader = new LoaderModel(LOADER_SIZE.SMALL).getHTML(); - this.view.getSubmitFormButton().getHTML().append(loader); - this.updateUserData(newUserData) - .then(() => getStore().dispatch(switchIsUserLoggedIn(true))) - .catch(() => { - serverMessageModel.showServerMessage( - SERVER_MESSAGE[getStore().getState().currentLanguage].BAD_REQUEST, - MESSAGE_STATUS.ERROR, - ); - }) - .finally(() => loader.remove()); - } - private switchSubmitFormButtonAccess(): boolean { if (this.inputFields.every((inputField) => inputField.getIsValid())) { this.view.getSubmitFormButton().setEnabled(); @@ -229,70 +185,40 @@ class RegisterFormModel { return true; } - private async updatePersonalData(userData: User | null): Promise { - let currentUserData = userData; - if (currentUserData) { - currentUserData = await getCustomerModel().editCustomer( - [ - CustomerModel.actionEditFirstName(formattedText(this.view.getFirstNameField().getView().getValue())), - CustomerModel.actionEditLastName(formattedText(this.view.getLastNameField().getView().getValue())), - CustomerModel.actionEditDateOfBirth(this.view.getDateOfBirthField().getView().getValue()), - ], - currentUserData, - ); - } - - return currentUserData; - } - - private async updateUserAddresses(userData: User | null): Promise { + private updateUserAddresses(userData: User | null): User | null { const { billing, shipping } = this.addressWrappers; const personalData = this.getPersonalData(); const checkboxSingleAddress = shipping.getView().getAddressAsBillingCheckBox()?.getHTML(); const checkboxDefaultShippingAddress = shipping.getView().getAddressByDefaultCheckBox()?.getHTML(); const checkboxDefaultBillingAddress = billing.getView().getAddressByDefaultCheckBox()?.getHTML(); - let currentUserData = userData; - - currentUserData = await this.addAddress(shipping.getAddressData(personalData), currentUserData); + const currentUserData = userData; + currentUserData?.addresses.push(shipping.getAddressData(personalData)); if (!currentUserData) { return null; } - const shippingAddressID = currentUserData.addresses.at(-1)?.id ?? ''; if (checkboxDefaultShippingAddress?.checked) { - currentUserData = await this.editDefaultShippingAddress(shippingAddressID, currentUserData); + currentUserData.defaultShippingAddressId = shipping.getAddressData(personalData); } if (checkboxSingleAddress?.checked && checkboxDefaultShippingAddress?.checked) { - currentUserData = await this.editDefaultBillingAddress(shippingAddressID, currentUserData); + currentUserData.defaultBillingAddressId = shipping.getAddressData(personalData); return currentUserData; } - currentUserData = await this.addAddress(billing.getAddressData(personalData), currentUserData); + currentUserData?.addresses.push(billing.getAddressData(personalData)); if (!currentUserData) { return null; } - const billingAddressID = currentUserData.addresses.at(-1)?.id ?? ''; if (checkboxDefaultBillingAddress?.checked) { - currentUserData = await this.editDefaultBillingAddress(billingAddressID, currentUserData); + currentUserData.defaultBillingAddressId = billing.getAddressData(personalData); } return currentUserData; } - private async updateUserData(newUserData: User): Promise { - let currentUserData: User | null = newUserData; - - currentUserData = await this.updatePersonalData(currentUserData); - - currentUserData = await this.updateUserAddresses(currentUserData); - - getStore().dispatch(setCurrentUser(currentUserData)); - return currentUserData; - } - public getFirstInputField(): InputFieldModel { return this.inputFields[0]; }