{"id":436758,"date":"2024-11-02T21:00:43","date_gmt":"2024-11-02T21:00:43","guid":{"rendered":"http:\/\/savepearlharbor.com\/?p=436758"},"modified":"-0001-11-30T00:00:00","modified_gmt":"-0001-11-29T21:00:00","slug":"","status":"publish","type":"post","link":"https:\/\/savepearlharbor.com\/?p=436758","title":{"rendered":"<span>\u0421\u043e\u0437\u0434\u0430\u043d\u0438\u0435 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0441\u043a\u043e\u0433\u043e \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0430 \u0434\u043b\u044f \u043c\u043e\u0434\u0443\u043b\u044f Webhook \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e Angular<\/span>"},"content":{"rendered":"<div><!--[--><!--]--><\/div>\n<div id=\"post-content-body\">\n<div>\n<div class=\"article-formatted-body article-formatted-body article-formatted-body_version-2\">\n<div xmlns=\"http:\/\/www.w3.org\/1999\/xhtml\">\n<p>\u041f\u0440\u0435\u0434\u044b\u0434\u0443\u0449\u0430\u044f \u0441\u0442\u0430\u0442\u044c\u044f: <a href=\"https:\/\/habr.com\/ru\/articles\/848634\/\" rel=\"noopener noreferrer nofollow\">\u0421\u043e\u0437\u0434\u0430\u043d\u0438\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0443\u0435\u043c\u043e\u0433\u043e Webhook-\u043c\u043e\u0434\u0443\u043b\u044f \u0434\u043b\u044f NestJS-\u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0438<\/a><\/p>\n<p>\u0412 \u044d\u0442\u043e\u0439 \u0441\u0442\u0430\u0442\u044c\u0435 \u044f \u043e\u043f\u0438\u0448\u0443 \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u0435 \u0442\u0430\u0431\u043b\u0438\u0447\u043a\u0438 \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0430\u044e\u0449\u0435\u0439 \u0434\u0430\u043d\u043d\u044b\u0435 \u0438 \u0444\u043e\u0440\u043c\u044b \u0434\u043b\u044f \u0435\u0435 \u0437\u0430\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f, \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u044b \u0441\u0442\u0440\u043e\u044f\u0442\u0441\u044f \u043d\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430\u0445 \u043e\u0442 <a href=\"https:\/\/ng.ant.design\" rel=\"noopener noreferrer nofollow\">https:\/\/ng.ant.design<\/a>, \u0444\u043e\u0440\u043c\u044b \u0441\u043e\u0437\u0434\u0430\u044e\u0442\u0441\u044f \u0438 \u0443\u043f\u0440\u0430\u0432\u043b\u044f\u044e\u0442\u0441\u044f \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e <a href=\"https:\/\/formly.dev\" rel=\"noopener noreferrer nofollow\">https:\/\/formly.dev<\/a>, \u0434\u043b\u044f \u0441\u0442\u0438\u043b\u0435\u0439 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f <a href=\"https:\/\/tailwindcss.com\" rel=\"noopener noreferrer nofollow\">https:\/\/tailwindcss.com<\/a>, \u0441\u0442\u0435\u0439\u0442 \u043c\u0430\u0448\u0438\u043d\u044b \u043d\u0435\u0442.<\/p>\n<h4>1. \u0421\u043e\u0437\u0434\u0430\u0435\u043c \u043f\u0443\u0441\u0442\u0443\u044e Angular \u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a\u0443<\/h4>\n<p>\u0412 \u044d\u0442\u043e\u0439 \u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a\u0435 \u043b\u0435\u0436\u0430\u0442 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u044b \u0434\u043b\u044f \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u044f \u0438 \u0440\u0430\u0431\u043e\u0442\u044b \u0441 \u0434\u0430\u043d\u043d\u044b\u043c\u0438 \u0441\u0443\u0449\u043d\u043e\u0441\u0442\u0438 <code>Webhook<\/code>.<\/p>\n<p><em>Commands<\/em><\/p>\n<pre><code class=\"bash\"># Create Angular library .\/node_modules\/.bin\/nx g @nx\/angular:library webhook-angular --buildable --publishable --directory=libs\/feature\/webhook-angular --simpleName=true --projectNameAndRootFormat=as-provided --strict=true --prefix= --standalone=true --selector= --changeDetection=OnPush --importPath=@nestjs-mod-fullstack\/webhook-angular  # Change file with test options rm -rf libs\/feature\/webhook-angular\/src\/test-setup.ts cp apps\/client\/src\/test-setup.ts libs\/feature\/webhook-angular\/src\/test-setup.ts <\/code><\/pre>\n<details class=\"spoiler\">\n<summary>\u0412\u044b\u0432\u043e\u0434 \u043a\u043e\u043d\u0441\u043e\u043b\u0438<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"bash\">$ .\/node_modules\/.bin\/nx g @nx\/angular:library webhook-angular --buildable --publishable --directory=libs\/feature\/webhook-angular --simpleName=true --projectNameAndRootFormat=as-provided --strict=true --prefix= --standalone=true --selector= --changeDetection=OnPush --importPath=@nestjs-mod-fullstack\/webhook-angular   NX  Generating @nx\/angular:library  CREATE libs\/feature\/webhook-angular\/project.json CREATE libs\/feature\/webhook-angular\/README.md CREATE libs\/feature\/webhook-angular\/ng-package.json CREATE libs\/feature\/webhook-angular\/package.json CREATE libs\/feature\/webhook-angular\/tsconfig.json CREATE libs\/feature\/webhook-angular\/tsconfig.lib.json CREATE libs\/feature\/webhook-angular\/tsconfig.lib.prod.json CREATE libs\/feature\/webhook-angular\/src\/index.ts CREATE libs\/feature\/webhook-angular\/jest.config.ts CREATE libs\/feature\/webhook-angular\/src\/test-setup.ts CREATE libs\/feature\/webhook-angular\/tsconfig.spec.json CREATE libs\/feature\/webhook-angular\/src\/lib\/webhook-angular\/webhook-angular.component.css CREATE libs\/feature\/webhook-angular\/src\/lib\/webhook-angular\/webhook-angular.component.html CREATE libs\/feature\/webhook-angular\/src\/lib\/webhook-angular\/webhook-angular.component.spec.ts CREATE libs\/feature\/webhook-angular\/src\/lib\/webhook-angular\/webhook-angular.component.ts CREATE libs\/feature\/webhook-angular\/.eslintrc.json UPDATE package.json UPDATE tsconfig.base.json  &gt; @nestjs-mod-fullstack\/source@0.0.9 prepare &gt; npx -y husky install  install command is DEPRECATED  removed 2 packages, changed 5 packages, and audited 2726 packages in 13s  332 packages are looking for funding   run `npm fund` for details  33 vulnerabilities (4 low, 12 moderate, 17 high)  To address issues that do not require attention, run:   npm audit fix  To address all issues possible (including breaking changes), run:   npm audit fix --force  Some issues need review, and may require choosing a different dependency.  Run `npm audit` for details.   NX   \ud83d\udc40 View Details of webhook-angular  Run \"nx show project webhook-angular\" to view details about this project. <\/code><\/pre>\n<\/div>\n<\/details>\n<h4>2. \u0421\u043e\u0437\u0434\u0430\u0435\u043c \u043e\u0431\u0449\u0443\u044e Angular \u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a\u0443<\/h4>\n<p>\u041e\u0431\u0449\u0430\u044f \u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a\u0430 \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u0442 \u0444\u0443\u043d\u043a\u0446\u0438\u0438 \u0438 \u043a\u043b\u0430\u0441\u0441\u044b \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044e\u0442\u0441\u044f \u0434\u0440\u0443\u0433\u0438\u043c\u0438 <code>Angular<\/code>-\u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a\u0430\u043c\u0438.<\/p>\n<p><em>Commands<\/em><\/p>\n<pre><code class=\"bash\"># Create Angular library .\/node_modules\/.bin\/nx g @nx\/angular:library common-angular --buildable --publishable --directory=libs\/common-angular --simpleName=true --projectNameAndRootFormat=as-provided --strict=true --prefix= --standalone=true --selector= --changeDetection=OnPush --importPath=@nestjs-mod-fullstack\/common-angular  # Change file with test options rm -rf libs\/common-angular\/src\/test-setup.ts cp apps\/client\/src\/test-setup.ts libs\/common-angular\/src\/test-setup.ts <\/code><\/pre>\n<details class=\"spoiler\">\n<summary>\u0412\u044b\u0432\u043e\u0434 \u043a\u043e\u043d\u0441\u043e\u043b\u0438<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"bash\">$ .\/node_modules\/.bin\/nx g @nx\/angular:library common-angular --buildable --publishable --directory=libs\/common-angular --simpleName=true --projectNameAndRootFormat=as-provided --strict=true --prefix= --standalone=true --selector= --changeDetection=OnPush --importPath=@nestjs-mod-fullstack\/common-angular   NX  Generating @nx\/angular:library  CREATE libs\/common-angular\/project.json CREATE libs\/common-angular\/README.md CREATE libs\/common-angular\/ng-package.json CREATE libs\/common-angular\/package.json CREATE libs\/common-angular\/tsconfig.json CREATE libs\/common-angular\/tsconfig.lib.json CREATE libs\/common-angular\/tsconfig.lib.prod.json CREATE libs\/common-angular\/src\/index.ts CREATE libs\/common-angular\/jest.config.ts CREATE libs\/common-angular\/src\/test-setup.ts CREATE libs\/common-angular\/tsconfig.spec.json CREATE libs\/common-angular\/src\/lib\/common-angular\/common-angular.component.css CREATE libs\/common-angular\/src\/lib\/common-angular\/common-angular.component.html CREATE libs\/common-angular\/src\/lib\/common-angular\/common-angular.component.spec.ts CREATE libs\/common-angular\/src\/lib\/common-angular\/common-angular.component.ts CREATE libs\/common-angular\/.eslintrc.json UPDATE tsconfig.base.json   NX   \ud83d\udc40 View Details of common-angular  Run \"nx show project common-angular\" to view details about this project. <\/code><\/pre>\n<\/div>\n<\/details>\n<h4>3. \u0423\u0441\u0442\u0430\u043d\u0430\u0432\u043b\u0438\u0432\u0430\u0435\u043c \u0434\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435 \u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a\u0438<\/h4>\n<p>\u0423\u0441\u0442\u0430\u043d\u0430\u0432\u043b\u0438\u0432\u0430\u0435\u043c \u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a\u0443 \u0432\u0438\u0437\u0443\u0430\u043b\u044c\u043d\u044b\u0445 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u043e\u0432 <code>ng-zorro-antd<\/code>, \u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a\u0443 \u0434\u043b\u044f \u0440\u0430\u0431\u043e\u0442\u044b \u0441 \u0444\u043e\u0440\u043c\u0430\u043c\u0438 <code>@ngx-formly\/core @ngx-formly\/ng-zorro-antd<\/code>, \u0443\u0442\u0438\u043b\u0438\u0442\u0443 \u0434\u043b\u044f \u0430\u0432\u0442\u043e-\u043e\u0442\u043f\u0438\u0441\u043a\u0438 <code>@ngneat\/until-destroy<\/code> \u0438 \u043a\u043e\u043b\u0435\u043a\u0446\u0438\u044e \u0443\u0442\u0438\u043b\u0438\u0442 <code>lodash<\/code>.<\/p>\n<p><em>Commands<\/em><\/p>\n<pre><code class=\"bash\">npm install --save ng-zorro-antd @ngx-formly\/core @ngx-formly\/ng-zorro-antd @ngneat\/until-destroy lodash <\/code><\/pre>\n<details class=\"spoiler\">\n<summary>\u0412\u044b\u0432\u043e\u0434 \u043a\u043e\u043d\u0441\u043e\u043b\u0438<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"bash\">$ npm install --save ng-zorro-antd @ngx-formly\/core @ngx-formly\/ng-zorro-antd @ngneat\/until-destroy  added 8 packages, removed 2 packages, and audited 2794 packages in 25s  343 packages are looking for funding   run `npm fund` for details  38 vulnerabilities (8 low, 12 moderate, 18 high)  To address issues that do not require attention, run:   npm audit fix  To address all issues (including breaking changes), run:   npm audit fix --force  Run `npm audit` for details. <\/code><\/pre>\n<\/div>\n<\/details>\n<h4>4. \u0422\u0430\u043a \u043a\u0430\u043a \u0434\u043b\u044f \u0440\u0430\u0431\u043e\u0442\u044b \u043c\u043e\u0434\u0443\u043b\u044f \u043d\u0443\u0436\u0435\u043d \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440\u044b \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u0438 \u043a\u043e\u043c\u043f\u0430\u043d\u0438\u0438, \u0442\u043e \u043d\u0443\u0436\u043d\u043e \u0441\u043e\u0437\u0434\u0430\u0442\u044c \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u044b \u0434\u043b\u044f \u043f\u0435\u0440\u0435\u0434\u0430\u0447\u0438 \u044d\u0442\u0438\u0445 \u0434\u0430\u043d\u043d\u044b\u0445<\/h4>\n<p>\u0421\u043e\u0437\u0434\u0430\u0435\u043c \u0444\u043e\u0440\u043c\u0443 \u0438 \u0441\u0435\u0440\u0432\u0438\u0441 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438 \u0432 <code>Webhook<\/code> &#8212; \u043c\u043e\u0434\u0443\u043b\u0435.<\/p>\n<p>\u0421\u0435\u0440\u0432\u0438\u0441 \u0438\u043c\u0435\u0435\u0442 \u043c\u0435\u0442\u043e\u0434\u044b \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u043f\u0440\u043e\u0444\u0438\u043b\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u043f\u043e \u043f\u0435\u0440\u0435\u0434\u0430\u043d\u043d\u044b\u043c <code>xExternalUserId<\/code> \u0438 <code>xExternalTenantId<\/code>, \u0430 \u0442\u0430\u043a\u0436\u0435 \u0445\u0440\u0430\u043d\u0438\u0442\u044c \u0438\u0445 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f \u0438 \u0434\u0430\u043d\u043d\u044b\u0435 \u043f\u043e \u043f\u0440\u043e\u0444\u0438\u043b\u044e \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f.<\/p>\n<p>\u0418\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u0430\u0434\u043c\u0438\u043d\u0438\u0441\u0442\u0440\u0430\u0442\u043e\u0440\u0430 \u043f\u0440\u043e\u043a\u0438\u0434\u044b\u0432\u0430\u0435\u0442\u0441\u044f \u0438\u0437 \u043f\u0435\u0440\u0435\u043c\u0435\u043d\u043d\u044b\u0445 \u043e\u043a\u0440\u0443\u0436\u0435\u043d\u0438\u044f <code>CI\/CD<\/code>.<\/p>\n<p>\u0414\u043b\u044f \u0437\u0430\u0449\u0438\u0442\u044b \u0441\u0442\u0440\u0430\u043d\u0438\u0446 \u0441\u043e\u0437\u0434\u0430\u0434\u0438\u043c \u0441\u043f\u0435\u0446\u0438\u0430\u043b\u044c\u043d\u044b\u0439 <code>Guard<\/code>.<\/p>\n<p>\u0421\u043e\u0437\u0434\u0430\u0435\u043c \u0441\u0435\u0440\u0432\u0438\u0441 <em>libs\/feature\/webhook-angular\/src\/lib\/services\/webhook-auth.service.ts<\/em><\/p>\n<pre><code class=\"typescript\">import { Injectable } from '@angular\/core'; import { WebhookErrorInterface, WebhookRestService, WebhookUserObjectInterface } from '@nestjs-mod-fullstack\/app-angular-rest-sdk'; import { UntilDestroy, untilDestroyed } from '@ngneat\/until-destroy'; import { BehaviorSubject, catchError, of, tap, throwError } from 'rxjs';  export type WebhookAuthCredentials = {   xExternalUserId?: string;   xExternalTenantId?: string; };  @UntilDestroy() @Injectable({ providedIn: 'root' }) export class WebhookAuthService {   private webhookAuthCredentials$ = new BehaviorSubject&lt;WebhookAuthCredentials&gt;({});   private webhookUser$ = new BehaviorSubject&lt;WebhookUserObjectInterface | null&gt;(null);    constructor(private readonly webhookRestService: WebhookRestService) {}    getWebhookAuthCredentials() {     return this.webhookAuthCredentials$.value;   }    getWebhookUser() {     return this.webhookUser$.value;   }    setWebhookAuthCredentials(webhookAuthCredentials: WebhookAuthCredentials) {     this.webhookAuthCredentials$.next(webhookAuthCredentials);     this.loadWebhookUser().pipe(untilDestroyed(this)).subscribe();   }    loadWebhookUser() {     return this.webhookRestService.webhookControllerProfile(this.getWebhookAuthCredentials().xExternalUserId, this.getWebhookAuthCredentials().xExternalTenantId).pipe(       tap((profile) =&gt; this.webhookUser$.next(profile)),       catchError((err: { error?: WebhookErrorInterface }) =&gt; {         if (err.error?.code === 'WEBHOOK-002') {           return of(null);         }         return throwError(() =&gt; err);       })     );   }    webhookAuthCredentialsUpdates() {     return this.webhookAuthCredentials$.asObservable();   }    webhookUserUpdates() {     return this.webhookUser$.asObservable();   } } <\/code><\/pre>\n<p>\u041f\u0441\u0435\u0432\u0434\u043e \u0444\u043e\u0440\u043c\u0430 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438 \u0438\u043c\u0435\u0435\u0442 \u0434\u0432\u0430 \u043f\u043e\u043b\u044f <code>xExternalUserId<\/code> \u0438 <code>xExternalTenantId<\/code>, \u043f\u043e\u0441\u0442\u0440\u043e\u0435\u043d\u0438\u0435 \u0438 \u0432\u0430\u043b\u0438\u0434\u0430\u0446\u0438\u044f \u0444\u043e\u0440\u043c\u044b \u043f\u0440\u043e\u0438\u0441\u0445\u043e\u0434\u0438\u0442 \u0447\u0435\u0440\u0435\u0437 \u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a\u0443 <a href=\"https:\/\/formly.dev\" rel=\"noopener noreferrer nofollow\">https:\/\/formly.dev<\/a>.<\/p>\n<p>\u041a\u0440\u043e\u043c\u0435 \u043a\u043d\u043e\u043f\u043a\u0438 \u0432\u043e\u0439\u0442\u0438, \u043d\u0430 \u0444\u043e\u0440\u043c\u0435 \u0442\u0430\u043a\u0436\u0435 \u0435\u0441\u0442\u044c \u0435\u0449\u0435 \u0434\u0432\u0435 \u043a\u043d\u043e\u043f\u043a\u0438:<\/p>\n<ol>\n<li>\n<p>\u0417\u0430\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u0434\u0430\u043d\u043d\u044b\u0435 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f &#8212; \u043f\u043e\u0434\u0441\u0442\u0430\u0432\u043b\u044f\u0435\u0442 \u0432 <code>xExternalUserId<\/code> \u0438 <code>xExternalTenantId<\/code> \u0437\u0430\u0445\u0430\u0440\u0434\u043a\u043e\u0436\u0435\u043d\u043d\u044b\u0435 \u0441\u043b\u0443\u0447\u0430\u0439\u043d\u044b\u0435 <code>uuid<\/code>-\u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440\u044b;<\/p>\n<\/li>\n<li>\n<p>\u0417\u0430\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u0434\u0430\u043d\u043d\u044b\u0435 \u0430\u0434\u043c\u0438\u043d\u0438\u0441\u0442\u0440\u0430\u0442\u043e\u0440\u0430 &#8212; \u043f\u043e\u0434\u0441\u0442\u0430\u0432\u043b\u044f\u0435\u0442 \u0432 <code>xExternalUserId<\/code> \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u0441 \u0440\u043e\u043b\u044c\u044e <code>Admin<\/code>, \u0431\u044d\u043a\u0435\u043d\u0434 \u043f\u0440\u0438 \u0441\u0442\u0430\u0440\u0442\u0435 \u0441\u043e\u0437\u0434\u0430\u0435\u0442 \u044d\u0442\u043e\u0433\u043e \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f, \u0430 \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u0432\u0441\u0442\u0430\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0432\u043e \u0444\u0440\u043e\u043d\u0442\u0435\u043d\u0434 \u043f\u0440\u0438 \u0441\u0431\u043e\u0440\u043a\u0435 \u0435\u0433\u043e \u0432 <code>CI\\CD<\/code>.<\/p>\n<\/li>\n<\/ol>\n<p>\u0421\u043e\u0437\u0434\u0430\u0435\u043c \u0444\u0430\u0439\u043b <em>libs\/feature\/webhook-angular\/src\/lib\/forms\/webhook-auth-form\/webhook-auth-form.component.ts<\/em><\/p>\n<pre><code class=\"typescript\">import { AsyncPipe, NgIf } from '@angular\/common'; import { ChangeDetectionStrategy, Component, EventEmitter, Inject, Input, OnInit, Optional, Output } from '@angular\/core'; import { FormsModule, ReactiveFormsModule, UntypedFormGroup } from '@angular\/forms'; import { FormlyFieldConfig, FormlyModule } from '@ngx-formly\/core'; import { NzButtonModule } from 'ng-zorro-antd\/button'; import { NzFormModule } from 'ng-zorro-antd\/form'; import { NzInputModule } from 'ng-zorro-antd\/input'; import { NzMessageService } from 'ng-zorro-antd\/message'; import { NZ_MODAL_DATA } from 'ng-zorro-antd\/modal'; import { BehaviorSubject } from 'rxjs'; import { WebhookAuthCredentials, WebhookAuthService } from '..\/..\/services\/webhook-auth.service'; import { WEBHOOK_CONFIGURATION_TOKEN, WebhookConfiguration } from '..\/..\/services\/webhook.configuration';  @Component({   standalone: true,   imports: [FormlyModule, NzFormModule, NzInputModule, NzButtonModule, FormsModule, ReactiveFormsModule, AsyncPipe, NgIf],   selector: 'webhook-auth-form',   templateUrl: '.\/webhook-auth-form.component.html',   changeDetection: ChangeDetectionStrategy.OnPush, }) export class WebhookAuthFormComponent implements OnInit {   @Input()   hideButtons?: boolean;    @Output()   afterSignIn = new EventEmitter&lt;WebhookAuthCredentials&gt;();    form = new UntypedFormGroup({});   formlyModel$ = new BehaviorSubject&lt;object | null&gt;(null);   formlyFields$ = new BehaviorSubject&lt;FormlyFieldConfig[] | null&gt;(null);    constructor(     @Optional()     @Inject(NZ_MODAL_DATA)     private readonly nzModalData: WebhookAuthFormComponent,     @Inject(WEBHOOK_CONFIGURATION_TOKEN)     private readonly webhookConfiguration: WebhookConfiguration,     private readonly webhookAuthService: WebhookAuthService,     private readonly nzMessageService: NzMessageService   ) {}    ngOnInit(): void {     Object.assign(this, this.nzModalData);     this.setFieldsAndModel(this.webhookAuthService.getWebhookAuthCredentials());   }    setFieldsAndModel(     data: Partial&lt;WebhookAuthCredentials&gt; = {},     options: { xExternalTenantIdIsRequired: boolean } = {       xExternalTenantIdIsRequired: true,     }   ) {     this.formlyFields$.next([       {         key: 'xExternalUserId',         type: 'input',         validation: {           show: true,         },         props: {           label: `webhook.form.xExternalUserId`,           placeholder: 'xExternalUserId',           required: true,         },       },       {         key: 'xExternalTenantId',         type: 'input',         validation: {           show: true,         },         props: {           label: `webhook.form.xExternalTenantId`,           placeholder: 'xExternalTenantId',           required: options.xExternalTenantIdIsRequired,         },       },     ]);     this.formlyModel$.next(this.toModel(data));   }    submitForm(): void {     if (this.form.valid) {       const value = this.toJson(this.form.value);       this.afterSignIn.next(value);       this.webhookAuthService.setWebhookAuthCredentials(value);       this.nzMessageService.success('Success');     } else {       console.log(this.form.controls);       this.nzMessageService.warning('Validation errors');     }   }    fillUserCredentials() {     this.setFieldsAndModel({       xExternalTenantId: '2079150a-f133-405c-9e77-64d3ab8aff77',       xExternalUserId: '3072607c-8c59-4fc4-9a37-916825bc0f99',     });   }    fillAdminCredentials() {     this.setFieldsAndModel(       {         xExternalTenantId: '',         xExternalUserId: this.webhookConfiguration.webhookSuperAdminExternalUserId,       },       { xExternalTenantIdIsRequired: false }     );   }    private toModel(data: Partial&lt;WebhookAuthCredentials&gt;): object | null {     return {       xExternalUserId: data['xExternalUserId'],       xExternalTenantId: data['xExternalTenantId'],     };   }    private toJson(data: Partial&lt;WebhookAuthCredentials&gt;) {     return {       xExternalUserId: data['xExternalUserId'],       xExternalTenantId: data['xExternalTenantId'],     };   } } <\/code><\/pre>\n<p>\u0421\u043e\u0437\u0434\u0430\u0435\u043c \u0444\u0430\u0439\u043b <em>libs\/feature\/webhook-angular\/src\/lib\/forms\/webhook-auth-form\/webhook-auth-form.component.html<\/em><\/p>\n<pre><code>@if (formlyFields$ | async; as formlyFields) { &lt;form nz-form [formGroup]=\"form\" (ngSubmit)=\"submitForm()\"&gt;   &lt;formly-form [model]=\"formlyModel$ | async\" [fields]=\"formlyFields\" [form]=\"form\"&gt; &lt;\/formly-form&gt;   @if (!hideButtons) {   &lt;nz-form-control&gt;     &lt;div class=\"flex justify-between\"&gt;       &lt;div&gt;         &lt;button nz-button type=\"button\" (click)=\"fillUserCredentials()\"&gt;Fill user credentials&lt;\/button&gt;         &lt;button nz-button type=\"button\" (click)=\"fillAdminCredentials()\"&gt;Fill admin credentials&lt;\/button&gt;       &lt;\/div&gt;       &lt;button nz-button nzType=\"primary\" type=\"submit\" [disabled]=\"!form.valid\"&gt;Sign-in&lt;\/button&gt;     &lt;\/div&gt;   &lt;\/nz-form-control&gt;   } &lt;\/form&gt; } <\/code><\/pre>\n<p>\u0418\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u0430\u0434\u043c\u0438\u043d\u0438\u0441\u0442\u0440\u0430\u0442\u043e\u0440\u0430 \u043f\u0435\u0440\u0435\u0434\u0430\u0435\u0442\u0441\u044f \u0447\u0435\u0440\u0435\u0437 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e \u0438 \u043f\u0435\u0440\u0435\u043c\u0435\u043d\u043d\u044b\u0435 \u043e\u043a\u0440\u0443\u0436\u0435\u043d\u0438\u044f.<\/p>\n<p>\u041e\u0431\u043d\u043e\u0432\u043b\u044f\u0435\u043c \u0444\u0430\u0439\u043b <em>apps\/client\/src\/environments\/environment.prod.ts<\/em><\/p>\n<pre><code class=\"typescript\">export const serverUrl = ''; export const webhookSuperAdminExternalUserId = '___CLIENT_WEBHOOK_SUPER_ADMIN_EXTERNAL_USER_ID___'; <\/code><\/pre>\n<p>\u041e\u0431\u043d\u043e\u0432\u043b\u044f\u0435\u043c \u0444\u0430\u0439\u043b <em>apps\/client\/src\/app\/app.config.ts<\/em><\/p>\n<pre><code class=\"typescript\">import { provideHttpClient } from '@angular\/common\/http'; import { ApplicationConfig, ErrorHandler, importProvidersFrom, provideZoneChangeDetection } from '@angular\/core'; import { provideClientHydration } from '@angular\/platform-browser'; import { BrowserAnimationsModule } from '@angular\/platform-browser\/animations'; import { provideRouter } from '@angular\/router'; import { RestClientApiModule, RestClientConfiguration } from '@nestjs-mod-fullstack\/app-angular-rest-sdk'; import { WEBHOOK_CONFIGURATION_TOKEN, WebhookConfiguration } from '@nestjs-mod-fullstack\/webhook-angular'; import { FormlyModule } from '@ngx-formly\/core'; import { FormlyNgZorroAntdModule } from '@ngx-formly\/ng-zorro-antd'; import { en_US, provideNzI18n } from 'ng-zorro-antd\/i18n'; import { serverUrl, webhookSuperAdminExternalUserId } from '..\/environments\/environment'; import { AppErrorHandler } from '.\/app.error-handler'; import { appRoutes } from '.\/app.routes';  export const appConfig: ApplicationConfig = {   providers: [     provideClientHydration(),     provideZoneChangeDetection({ eventCoalescing: true }),     provideRouter(appRoutes),     provideHttpClient(),     provideNzI18n(en_US),     {       provide: WEBHOOK_CONFIGURATION_TOKEN,       useValue: new WebhookConfiguration({ webhookSuperAdminExternalUserId }), \/\/ &lt;-- update     },     importProvidersFrom(       BrowserAnimationsModule,       RestClientApiModule.forRoot(         () =&gt;           new RestClientConfiguration({             basePath: serverUrl,           })       ),       FormlyModule.forRoot(),       FormlyNgZorroAntdModule     ),     { provide: ErrorHandler, useClass: AppErrorHandler },   ], }; <\/code><\/pre>\n<p>\u0421\u0442\u0440\u0430\u043d\u0438\u0446\u0443 \u0441 \u0444\u043e\u0440\u043c\u043e\u0439 \u0432\u0445\u043e\u0434\u0430 \u0441\u043e\u0437\u0434\u0430\u0435\u043c \u043d\u0430 \u0443\u0440\u043e\u0432\u043d\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f, \u0442\u0430\u043a \u043a\u0430\u043a \u0443 \u043d\u0430\u0441 \u043d\u0435\u0442 \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e\u0441\u0442\u0438 \u0435\u0435 \u043f\u0435\u0440\u0435\u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c.<\/p>\n<p>\u0421\u043e\u0437\u0434\u0430\u0435\u043c \u0444\u0430\u0439\u043b <em>apps\/client\/src\/app\/pages\/sign-in\/sign-in.component.ts<\/em><\/p>\n<pre><code class=\"typescript\">import { ChangeDetectionStrategy, Component } from '@angular\/core'; import { Router } from '@angular\/router'; import { WebhookAuthFormComponent } from '@nestjs-mod-fullstack\/webhook-angular'; import { NzBreadCrumbModule } from 'ng-zorro-antd\/breadcrumb';  @Component({   standalone: true,   selector: 'app-sign-in',   templateUrl: '.\/sign-in.component.html',   imports: [NzBreadCrumbModule, WebhookAuthFormComponent],   changeDetection: ChangeDetectionStrategy.OnPush, }) export class SignInComponent {   constructor(private readonly router: Router) {}   onAfterSignIn() {     this.router.navigate(['\/webhook']);   } } <\/code><\/pre>\n<p>\u0421\u043e\u0437\u0434\u0430\u0435\u043c \u0444\u0430\u0439\u043b <em>apps\/client\/src\/app\/pages\/sign-in\/sign-in.component.html<\/em><\/p>\n<pre><code>&lt;nz-breadcrumb&gt;   &lt;nz-breadcrumb-item&gt;Sign-in&lt;\/nz-breadcrumb-item&gt; &lt;\/nz-breadcrumb&gt; &lt;div class=\"inner-content\"&gt;   &lt;webhook-auth-form (afterSignIn)=\"onAfterSignIn()\"&gt;&lt;\/webhook-auth-form&gt; &lt;\/div&gt; <\/code><\/pre>\n<p>\u0421\u0442\u0440\u0430\u043d\u0438\u0446\u0430 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438 \u0434\u043e\u043b\u0436\u043d\u0430 \u0431\u044b\u0442\u044c \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430 \u0442\u043e\u043b\u044c\u043a\u043e \u043a\u043e\u0433\u0434\u0430 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c \u043d\u0435 \u0432\u0432\u0435\u043b \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u043e\u043d\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435, \u0434\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u043d\u0430\u043f\u0438\u0448\u0435\u043c <code>Guard<\/code> \u0438 \u0437\u0430\u043a\u0440\u043e\u0435\u043c \u0438\u043c \u043d\u0430\u0448\u0438 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u044b.<\/p>\n<p>\u0421\u043e\u0437\u0434\u0430\u0435\u043c \u0444\u0430\u0439\u043b <em>libs\/feature\/webhook-angular\/src\/lib\/services\/webhook-guard.service.ts<\/em><\/p>\n<pre><code class=\"typescript\">import { Injectable } from '@angular\/core'; import { ActivatedRouteSnapshot, CanActivate } from '@angular\/router'; import { WebhookRoleInterface } from '@nestjs-mod-fullstack\/app-angular-rest-sdk'; import { map, of } from 'rxjs'; import { WebhookAuthService } from '.\/webhook-auth.service';  export const WEBHOOK_GUARD_DATA_ROUTE_KEY = 'webhookGuardData';  export class WebhookGuardData {   roles?: WebhookRoleInterface[];    constructor(options?: WebhookGuardData) {     Object.assign(this, options);   } }  @Injectable({ providedIn: 'root' }) export class WebhookGuardService implements CanActivate {   constructor(private readonly webhookAuthService: WebhookAuthService) {}   canActivate(route: ActivatedRouteSnapshot) {     if (route.data[WEBHOOK_GUARD_DATA_ROUTE_KEY] instanceof WebhookGuardData) {       const webhookGuardData = route.data[WEBHOOK_GUARD_DATA_ROUTE_KEY];       return this.webhookAuthService.loadWebhookUser().pipe(         map((webhookUser) =&gt; {           return Boolean((webhookGuardData.roles &amp;&amp; webhookUser &amp;&amp; webhookGuardData.roles.length &gt; 0 &amp;&amp; webhookGuardData.roles.includes(webhookUser.userRole)) || ((webhookGuardData.roles || []).length === 0 &amp;&amp; !webhookUser?.userRole));         })       );     }     return of(true);   } } <\/code><\/pre>\n<p>\u041e\u0431\u043d\u043e\u0432\u043b\u044f\u0435\u043c \u0444\u0430\u0439\u043b <em>apps\/client\/src\/app\/app.routes.ts<\/em><\/p>\n<pre><code class=\"typescript\">import { Route } from '@angular\/router'; import { WEBHOOK_GUARD_DATA_ROUTE_KEY, WebhookGuardData, WebhookGuardService } from '@nestjs-mod-fullstack\/webhook-angular'; import { HomeComponent } from '.\/pages\/home\/home.component'; import { SignInComponent } from '.\/pages\/sign-in\/sign-in.component'; import { WebhookComponent } from '.\/pages\/webhook\/webhook.component'; import { DemoComponent } from '.\/pages\/demo\/demo.component';  export const appRoutes: Route[] = [   { path: '', redirectTo: '\/home', pathMatch: 'full' },   { path: 'home', component: HomeComponent },   { path: 'demo', component: DemoComponent },   {     path: 'webhook',     component: WebhookComponent,     canActivate: [WebhookGuardService],     data: {       [WEBHOOK_GUARD_DATA_ROUTE_KEY]: new WebhookGuardData({         roles: ['Admin', 'User'],       }),     },   },   {     path: 'sign-in',     component: SignInComponent,     canActivate: [WebhookGuardService],     data: {       [WEBHOOK_GUARD_DATA_ROUTE_KEY]: new WebhookGuardData({ roles: [] }),     },   }, ]; <\/code><\/pre>\n<h4>5. \u041e\u043f\u0438\u0441\u044b\u0432\u0430\u0435\u043c \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0443 \u0441 \u0444\u043e\u0440\u043c\u043e\u0439 \u0438 \u0441\u0435\u0440\u0432\u0438\u0441 \u0434\u043b\u044f \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u044f \u0438 \u0440\u0435\u0434\u0430\u043a\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f \u0441\u0443\u0449\u043d\u043e\u0441\u0442\u0438 Webhook<\/h4>\n<p>\u0422\u0430\u043a \u043a\u0430\u043a \u043c\u0435\u0442\u043e\u0434 \u0434\u043b\u044f \u0440\u0430\u0431\u043e\u0442\u044b \u0441 <code>Webhook<\/code>-\u0441\u0443\u0449\u043d\u043e\u0441\u0442\u044c\u044e \u0442\u0440\u0435\u0431\u0443\u044e\u0442 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u043e\u043d\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435, \u043c\u044b \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0430\u0435\u043c <code>WebhookAuthService<\/code> \u0432 \u0441\u0435\u0440\u0432\u0438\u0441 \u0434\u043b\u044f \u0440\u0430\u0431\u043e\u0442\u044b \u0441 \u0431\u044d\u043a\u0435\u043d\u0434 \u0441\u0443\u0449\u043d\u043e\u0441\u0442\u0438 <code>Webhook<\/code>.<\/p>\n<p>\u0421\u043e\u0437\u0434\u0430\u0435\u043c \u0441\u0435\u0440\u0432\u0438\u0441 <em>libs\/feature\/webhook-angular\/src\/lib\/services\/webhook.service.ts<\/em><\/p>\n<pre><code class=\"typescript\">import { Injectable } from '@angular\/core'; import { CreateWebhookArgsInterface, UpdateWebhookArgsInterface, WebhookRestService } from '@nestjs-mod-fullstack\/app-angular-rest-sdk'; import { RequestMeta } from '@nestjs-mod-fullstack\/common-angular'; import { WebhookAuthService } from '.\/webhook-auth.service';  @Injectable({ providedIn: 'root' }) export class WebhookService {   constructor(private readonly webhookAuthService: WebhookAuthService, private readonly webhookRestService: WebhookRestService) {}    findOne(id: string) {     return this.webhookRestService.webhookControllerFindOne(id, this.webhookAuthService.getWebhookAuthCredentials().xExternalUserId, this.webhookAuthService.getWebhookAuthCredentials().xExternalTenantId);   }    findMany({ filters, meta }: { filters: Record&lt;string, string&gt;; meta?: RequestMeta }) {     return this.webhookRestService.webhookControllerFindMany(       this.webhookAuthService.getWebhookAuthCredentials().xExternalUserId,       this.webhookAuthService.getWebhookAuthCredentials().xExternalTenantId,       meta?.curPage,       meta?.perPage,       filters['search'],       meta?.sort         ? Object.entries(meta?.sort)             .map(([key, value]) =&gt; `${key}:${value}`)             .join(',')         : undefined     );   }    updateOne(id: string, data: UpdateWebhookArgsInterface) {     return this.webhookRestService.webhookControllerUpdateOne(id, data, this.webhookAuthService.getWebhookAuthCredentials().xExternalUserId, this.webhookAuthService.getWebhookAuthCredentials().xExternalTenantId);   }    deleteOne(id: string) {     return this.webhookRestService.webhookControllerDeleteOne(id, this.webhookAuthService.getWebhookAuthCredentials().xExternalUserId, this.webhookAuthService.getWebhookAuthCredentials().xExternalTenantId);   }    createOne(data: CreateWebhookArgsInterface) {     return this.webhookRestService.webhookControllerCreateOne(data, this.webhookAuthService.getWebhookAuthCredentials().xExternalUserId, this.webhookAuthService.getWebhookAuthCredentials().xExternalTenantId);   } } <\/code><\/pre>\n<p>\u0426\u0435\u043b\u044c \u0434\u0430\u043d\u043d\u043e\u0433\u043e \u043f\u043e\u0441\u0442\u0430 \u0441\u043e\u0437\u0434\u0430\u0442\u044c \u043f\u0440\u043e\u0441\u0442\u043e\u0439 \u043f\u0440\u0438\u043c\u0435\u0440 <code>CRUD<\/code> \u043d\u0430 <code>Angular<\/code>, \u0444\u043e\u0440\u043c\u0430 \u0441\u043e\u0441\u0442\u043e\u0438\u0442 \u0438\u0437 \u0441\u0442\u0430\u043d\u0434\u0430\u0440\u0442\u043d\u044b\u0445 \u0442\u0438\u043f\u043e\u0432 \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u043e\u0432 <code>(checkbox, input, select, textarea)<\/code>, \u0430 \u043b\u043e\u0433\u0438\u043a\u0430 \u043f\u043e \u0442\u0440\u0430\u043d\u0441\u0444\u043e\u0440\u043c\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044e \u0434\u0430\u043d\u043d\u044b\u0445 \u0432 <code>formly<\/code> \u0438 \u043e\u0431\u0440\u0430\u0442\u043d\u043e \u043b\u0435\u0436\u0438\u0442 \u0432 \u044d\u0442\u043e\u0439 \u0436\u0435 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0435.<\/p>\n<p>\u0412 \u0434\u0430\u043b\u044c\u043d\u0435\u0439\u0448\u0438\u0445 \u0441\u0442\u0430\u0442\u044c\u044f\u0445 \u0431\u0443\u0434\u0443\u0442 \u0441\u043e\u0437\u0434\u0430\u043d\u044b \u0434\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435 \u043a\u0430\u0441\u0442\u043e\u043c\u043d\u044b\u0435 \u0442\u0438\u043f\u044b \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u043e\u0432 \u0434\u043b\u044f <code>formly<\/code> \u0441 \u0441\u043e\u0431\u0441\u0442\u0432\u0435\u043d\u043d\u044b\u043c\u0438 \u043b\u043e\u0433\u0438\u043a\u0430\u043c\u0438 \u0442\u0440\u0430\u043d\u0441\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438.<\/p>\n<p>\u0421\u043e\u0437\u0434\u0430\u0435\u043c \u043a\u043b\u0430\u0441\u0441 \u0444\u043e\u0440\u043c\u044b <em>libs\/feature\/webhook-angular\/src\/lib\/forms\/webhook-form\/webhook-form.component.ts<\/em><\/p>\n<pre><code class=\"typescript\">import { AsyncPipe, NgIf } from '@angular\/common'; import { ChangeDetectionStrategy, Component, EventEmitter, Inject, Input, OnInit, Optional, Output } from '@angular\/core'; import { FormsModule, ReactiveFormsModule, UntypedFormGroup } from '@angular\/forms'; import { WebhookEventInterface, WebhookObjectInterface } from '@nestjs-mod-fullstack\/app-angular-rest-sdk'; import { safeParseJson } from '@nestjs-mod-fullstack\/common-angular'; import { UntilDestroy, untilDestroyed } from '@ngneat\/until-destroy'; import { FormlyFieldConfig, FormlyModule } from '@ngx-formly\/core'; import { NzButtonModule } from 'ng-zorro-antd\/button'; import { NzFormModule } from 'ng-zorro-antd\/form'; import { NzInputModule } from 'ng-zorro-antd\/input'; import { NzMessageService } from 'ng-zorro-antd\/message'; import { NZ_MODAL_DATA } from 'ng-zorro-antd\/modal'; import { BehaviorSubject, tap } from 'rxjs'; import { WebhookEventsService } from '..\/..\/services\/webhook-events.service'; import { WebhookService } from '..\/..\/services\/webhook.service';  @UntilDestroy() @Component({   standalone: true,   imports: [FormlyModule, NzFormModule, NzInputModule, NzButtonModule, FormsModule, ReactiveFormsModule, AsyncPipe, NgIf],   selector: 'webhook-form',   templateUrl: '.\/webhook-form.component.html',   changeDetection: ChangeDetectionStrategy.OnPush, }) export class WebhookFormComponent implements OnInit {   @Input()   id?: string;    @Input()   hideButtons?: boolean;    @Output()   afterFind = new EventEmitter&lt;WebhookObjectInterface&gt;();    @Output()   afterCreate = new EventEmitter&lt;WebhookObjectInterface&gt;();    @Output()   afterUpdate = new EventEmitter&lt;WebhookObjectInterface&gt;();    form = new UntypedFormGroup({});   formlyModel$ = new BehaviorSubject&lt;object | null&gt;(null);   formlyFields$ = new BehaviorSubject&lt;FormlyFieldConfig[] | null&gt;(null);    events: WebhookEventInterface[] = [];    constructor(     @Optional()     @Inject(NZ_MODAL_DATA)     private readonly nzModalData: WebhookFormComponent,     private readonly webhookService: WebhookService,     private readonly webhookEventsService: WebhookEventsService,     private readonly nzMessageService: NzMessageService   ) {}    ngOnInit(): void {     Object.assign(this, this.nzModalData);     this.webhookEventsService       .findMany()       .pipe(         tap((events) =&gt; {           this.events = events;            if (this.id) {             this.findOne()               .pipe(                 tap((result) =&gt; this.afterFind.next(result)),                 untilDestroyed(this)               )               .subscribe();           } else {             this.setFieldsAndModel();           }         }),         untilDestroyed(this)       )       .subscribe();   }    setFieldsAndModel(data: Partial&lt;WebhookObjectInterface&gt; = {}) {     this.formlyFields$.next([       {         key: 'enabled',         type: 'checkbox',         validation: {           show: true,         },         props: {           label: `webhook.form.enabled`,           placeholder: 'enabled',           required: true,         },       },       {         key: 'endpoint',         type: 'input',         validation: {           show: true,         },         props: {           label: `webhook.form.endpoint`,           placeholder: 'endpoint',           required: true,         },       },       {         key: 'eventName',         type: 'select',         validation: {           show: true,         },         props: {           label: `webhook.form.eventName`,           placeholder: 'eventName',           required: true,           options: this.events.map((e) =&gt; ({             value: e.eventName,             label: e.description,           })),         },       },       {         key: 'headers',         type: 'textarea',         validation: {           show: true,         },         props: {           label: `webhook.form.headers`,           placeholder: 'headers',           required: true,         },       },       {         key: 'requestTimeout',         type: 'input',         validation: {           show: true,         },         props: {           label: `webhook.form.requestTimeout`,           placeholder: 'requestTimeout',           required: false,         },       },     ]);     this.formlyModel$.next(this.toModel(data));   }    submitForm(): void {     if (this.form.valid) {       if (this.id) {         this.updateOne()           .pipe(             tap((result) =&gt; {               this.nzMessageService.success('Success');               this.afterUpdate.next(result);             }),             untilDestroyed(this)           )           .subscribe();       } else {         this.createOne()           .pipe(             tap((result) =&gt; {               this.nzMessageService.success('Success');               this.afterCreate.next(result);             }),              untilDestroyed(this)           )           .subscribe();       }     } else {       console.log(this.form.controls);       this.nzMessageService.warning('Validation errors');     }   }    createOne() {     return this.webhookService.createOne(this.toJson(this.form.value));   }    updateOne() {     if (!this.id) {       throw new Error('id not set');     }     return this.webhookService.updateOne(this.id, this.toJson(this.form.value));   }    findOne() {     if (!this.id) {       throw new Error('id not set');     }     return this.webhookService.findOne(this.id).pipe(       tap((result) =&gt; {         this.setFieldsAndModel(result);       })     );   }    private toModel(data: Partial&lt;WebhookObjectInterface&gt;): object | null {     return {       enabled: (data['enabled'] as unknown as string) === 'true' || data['enabled'] === true,       endpoint: data['endpoint'],       eventName: data['eventName'],       headers: data['headers'] ? JSON.stringify(data['headers']) : '',       requestTimeout: data['requestTimeout'] &amp;&amp; !isNaN(+data['requestTimeout']) ? data['requestTimeout'] : '',     };   }    private toJson(data: Partial&lt;WebhookObjectInterface&gt;) {     return {       enabled: data['enabled'] === true,       endpoint: data['endpoint'] || '',       eventName: data['eventName'] || '',       headers: data['headers'] ? safeParseJson(data['headers']) : null,       requestTimeout: data['requestTimeout'] &amp;&amp; !isNaN(+data['requestTimeout']) ? +data['requestTimeout'] : undefined,     };   } } <\/code><\/pre>\n<p>\u0420\u0430\u0437\u043c\u0435\u0442\u043a\u0430 \u0444\u043e\u0440\u043c\u044b \u0438\u043c\u0435\u0435\u0442 \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0430\u0442\u044c \u0435\u0435 \u0432 \u0432\u0438\u0434\u0435 <code>inline<\/code> \u043d\u0430 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0435 \u0441 \u0432\u0441\u0442\u0440\u043e\u0435\u043d\u043d\u044b\u043c\u0438 \u043a\u043d\u043e\u043f\u043a\u0430\u043c\u0438, \u0430 \u0442\u0430\u043a\u0436\u0435 \u0435\u0435 \u043c\u043e\u0436\u043d\u043e \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0430\u0442\u044c \u0432 \u043c\u043e\u0434\u0430\u043b\u044c\u043d\u043e\u043c \u043e\u043a\u043d\u0435 \u0443 \u043a\u043e\u0442\u043e\u0440\u043e\u0433\u043e \u0441\u0432\u043e\u044f \u0440\u0430\u0437\u043c\u0435\u0442\u043a\u0430 \u0434\u043b\u044f \u043a\u043d\u043e\u043f\u043e\u043a.<\/p>\n<p>\u0421\u043e\u0437\u0434\u0430\u0435\u043c \u0440\u0430\u0437\u043c\u0435\u0442\u043a\u0443 \u0444\u043e\u0440\u043c\u044b <em>libs\/feature\/webhook-angular\/src\/lib\/forms\/webhook-form\/webhook-form.component.html<\/em><\/p>\n<pre><code>@if (formlyFields$ | async; as formlyFields) { &lt;form nz-form [formGroup]=\"form\" (ngSubmit)=\"submitForm()\"&gt;   &lt;formly-form [model]=\"formlyModel$ | async\" [fields]=\"formlyFields\" [form]=\"form\"&gt; &lt;\/formly-form&gt;   @if (!hideButtons) {   &lt;nz-form-control&gt;     &lt;button nzBlock nz-button nzType=\"primary\" type=\"submit\" [disabled]=\"!form.valid\"&gt;{{ id ? 'Save' : 'Create' }}&lt;\/button&gt;   &lt;\/nz-form-control&gt;   } &lt;\/form&gt; } <\/code><\/pre>\n<h4>5. \u041e\u043f\u0438\u0441\u044b\u0432\u0430\u0435\u043c \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0443 \u0441 \u0442\u0430\u0431\u043b\u0438\u0446\u0435\u0439 \u0434\u043b\u044f \u043f\u0440\u043e\u0441\u043c\u043e\u0442\u0440\u0430, \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u044f \u0438 \u0440\u0435\u0434\u0430\u043a\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f \u0441\u0443\u0449\u043d\u043e\u0441\u0442\u0435\u0439 Webhook<\/h4>\n<p>\u0422\u0430\u0431\u043b\u0438\u0446\u0430 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442 \u0441\u0435\u0440\u0432\u0435\u0440\u043d\u0443\u044e \u043f\u0430\u0433\u0438\u043d\u0430\u0446\u0438\u044e, \u0441\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u043a\u0443 \u0438 \u043f\u043e\u0438\u0441\u043a \u0432 \u0442\u0435\u043a\u0441\u0442\u043e\u0432\u044b\u0445 \u043f\u043e\u043b\u044f\u0445.<\/p>\n<p>\u041f\u043e\u0441\u043b\u0435 \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u044f\/\u0440\u0435\u0434\u0430\u043a\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f\/\u0443\u0434\u0430\u043b\u0435\u043d\u0438\u044f \u043f\u0440\u043e\u0438\u0441\u0445\u043e\u0434\u0438\u0442 \u0437\u0430\u043f\u0440\u043e\u0441 \u0442\u0435\u043a\u0443\u0449\u0438\u0439 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u044b \u0442\u0430\u0431\u043b\u0438\u0446\u044b.<\/p>\n<p>\u0421\u043e\u0437\u0434\u0430\u043d\u0438\u0435 \u0438 \u0440\u0435\u0434\u0430\u043a\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u0437\u0430\u043f\u0438\u0441\u0435\u0439 \u043f\u0440\u043e\u0438\u0441\u0445\u043e\u0434\u0438\u0442 \u0432 \u043c\u043e\u0434\u0430\u043b\u044c\u043d\u043e\u043c \u043e\u043a\u043d\u0435 \u0441 \u0444\u043e\u0440\u043c\u043e\u0439.<\/p>\n<p>\u041f\u0440\u0438 \u0443\u0434\u0430\u043b\u0435\u043d\u0438\u0438 \u0437\u0430\u043f\u0438\u0441\u0438 \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0430\u0435\u0442\u0441\u044f \u043c\u043e\u0434\u0430\u043b\u044c\u043d\u043e\u0435 \u043e\u043a\u043d\u043e \u0441 \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u0438\u0435\u043c \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u044f.<\/p>\n<p>\u0421\u043e\u0437\u0434\u0430\u0435\u043c \u043a\u043b\u0430\u0441\u0441 \u0442\u0430\u0431\u043b\u0438\u0446\u044b <em>libs\/feature\/webhook-angular\/src\/lib\/grids\/webhook-grid\/webhook-grid.component.ts<\/em><\/p>\n<pre><code class=\"typescript\">import { CommonModule } from '@angular\/common'; import { ChangeDetectionStrategy, Component, Input, OnInit, ViewContainerRef } from '@angular\/core'; import { FormControl, FormsModule, ReactiveFormsModule } from '@angular\/forms'; import { RouterModule } from '@angular\/router'; import { WebhookObjectInterface } from '@nestjs-mod-fullstack\/app-angular-rest-sdk'; import { UntilDestroy, untilDestroyed } from '@ngneat\/until-destroy'; import isEqual from 'lodash\/fp\/isEqual'; import omit from 'lodash\/fp\/omit'; import { NzButtonModule } from 'ng-zorro-antd\/button'; import { NzDividerModule } from 'ng-zorro-antd\/divider'; import { NzGridModule } from 'ng-zorro-antd\/grid'; import { NzIconModule } from 'ng-zorro-antd\/icon'; import { NzInputModule } from 'ng-zorro-antd\/input'; import { NzLayoutModule } from 'ng-zorro-antd\/layout'; import { NzMenuModule } from 'ng-zorro-antd\/menu'; import { NzModalModule, NzModalService } from 'ng-zorro-antd\/modal'; import { NzTableModule, NzTableQueryParams } from 'ng-zorro-antd\/table'; import { BehaviorSubject, debounceTime, distinctUntilChanged, tap } from 'rxjs';  import { WebhookScalarFieldEnumInterface } from '@nestjs-mod-fullstack\/app-angular-rest-sdk'; import { getQueryMeta, getQueryMetaByParams, NzTableSortOrderDetectorPipe, RequestMeta } from '@nestjs-mod-fullstack\/common-angular'; import { WebhookFormComponent } from '..\/..\/forms\/webhook-form\/webhook-form.component'; import { WebhookService } from '..\/..\/services\/webhook.service';  @UntilDestroy() @Component({   standalone: true,   imports: [NzGridModule, NzMenuModule, NzLayoutModule, NzTableModule, NzDividerModule, CommonModule, RouterModule, NzModalModule, NzButtonModule, NzInputModule, NzIconModule, FormsModule, ReactiveFormsModule, NzTableSortOrderDetectorPipe],   selector: 'webhook-grid',   templateUrl: '.\/webhook-grid.component.html',   changeDetection: ChangeDetectionStrategy.OnPush, }) export class WebhookGridComponent implements OnInit {   items$ = new BehaviorSubject&lt;WebhookObjectInterface[]&gt;([]);   meta$ = new BehaviorSubject&lt;RequestMeta | undefined&gt;(undefined);   searchField = new FormControl('');   selectedIds$ = new BehaviorSubject&lt;string[]&gt;([]);   columns = ['id', 'enabled', 'endpoint', 'eventName', 'headers', 'requestTimeout'];    private filters?: Record&lt;string, string&gt;;    constructor(private readonly webhookService: WebhookService, private readonly nzModalService: NzModalService, private readonly viewContainerRef: ViewContainerRef) {     this.searchField.valueChanges       .pipe(         debounceTime(700),         distinctUntilChanged(),         tap(() =&gt; this.loadMany({ force: true })),         untilDestroyed(this)       )       .subscribe();   }    ngOnInit(): void {     this.loadMany();   }    loadMany(args?: { filters?: Record&lt;string, string&gt;; meta?: RequestMeta; queryParams?: NzTableQueryParams; force?: boolean }) {     let meta = { meta: {}, ...(args || {}) }.meta as RequestMeta;     const { queryParams, filters } = { filters: {}, ...(args || {}) };      if (!args?.force &amp;&amp; queryParams) {       meta = getQueryMetaByParams(queryParams);     }      meta = getQueryMeta(meta, this.meta$.value);      if (!filters['search'] &amp;&amp; this.searchField.value) {       filters['search'] = this.searchField.value;     }      if (       !args?.force &amp;&amp;       isEqual(         omit(['totalResults'], { ...meta, ...filters }),         omit(['totalResults'], {           ...this.meta$.value,           ...this.filters,         })       )     ) {       return;     }      this.webhookService       .findMany({ filters, meta })       .pipe(         tap((result) =&gt; {           this.items$.next(             result.webhooks.map((item) =&gt; ({               ...item,               \/\/ eslint-disable-next-line @typescript-eslint\/no-explicit-any               headers: JSON.stringify(item.headers) as any,             }))           );           this.meta$.next({ ...result.meta, ...meta });           this.filters = filters;           this.selectedIds$.next([]);         }),         untilDestroyed(this)       )       .subscribe();   }    showCreateOrUpdateModal(id?: string): void {     const modal = this.nzModalService.create&lt;WebhookFormComponent, WebhookFormComponent&gt;({       nzTitle: id ? 'Update webhook' : 'Create webhook',       nzContent: WebhookFormComponent,       nzViewContainerRef: this.viewContainerRef,       nzData: {         hideButtons: true,         id,       } as WebhookFormComponent,       nzFooter: [         {           label: 'Cancel',           onClick: () =&gt; {             modal.close();           },         },         {           label: id ? 'Save' : 'Create',           onClick: () =&gt; {             modal.componentInstance?.afterUpdate               .pipe(                 tap(() =&gt; {                   modal.close();                   this.loadMany({ force: true });                 }),                 untilDestroyed(modal.componentInstance)               )               .subscribe();              modal.componentInstance?.afterCreate               .pipe(                 tap(() =&gt; {                   modal.close();                   this.loadMany({ force: true });                 }),                 untilDestroyed(modal.componentInstance)               )               .subscribe();              modal.componentInstance?.submitForm();           },           type: 'primary',         },       ],     });   }    showDeleteModal(id: string) {     this.nzModalService.confirm({       nzTitle: `Delete webhook #${id}`,       nzOkText: 'Yes',       nzCancelText: 'No',       nzOnOk: () =&gt; {         this.webhookService           .deleteOne(id)           .pipe(             tap(() =&gt; {               this.loadMany({ force: true });             }),             untilDestroyed(this)           )           .subscribe();       },     });   } } <\/code><\/pre>\n<p>\u0421\u043e\u0437\u0434\u0430\u0435\u043c \u0440\u0430\u0437\u043c\u0435\u0442\u043a\u0443 \u0442\u0430\u0431\u043b\u0438\u0446\u044b <em>libs\/feature\/webhook-angular\/src\/lib\/grids\/webhook-grid\/webhook-grid.component.html<\/em><\/p>\n<pre><code>&lt;div class=\"table-operations\" nz-row nzJustify=\"space-between\"&gt;   &lt;div nz-col nzSpan=\"4\"&gt;     &lt;button nz-button nzType=\"primary\" (click)=\"showCreateOrUpdateModal()\"&gt;Create new&lt;\/button&gt;   &lt;\/div&gt;   &lt;div nz-col nzSpan=\"4\"&gt;     &lt;nz-input-group nzSearch [nzAddOnAfter]=\"suffixIconButton\"&gt;       &lt;input type=\"text\" [formControl]=\"searchField\" nz-input placeholder=\"input search text\" \/&gt;     &lt;\/nz-input-group&gt;     &lt;ng-template #suffixIconButton&gt;       &lt;button (click)=\"loadMany({ force: true })\" nz-button nzType=\"primary\" nzSearch&gt;         &lt;span nz-icon nzType=\"search\"&gt;&lt;\/span&gt;       &lt;\/button&gt;     &lt;\/ng-template&gt;   &lt;\/div&gt; &lt;\/div&gt; @if ((meta$ | async); as meta){ &lt;nz-table   #basicTable   [nzBordered]=\"true\"   [nzOuterBordered]=\"true\"   nzShowPagination   nzShowSizeChanger   [nzFrontPagination]=\"false\"   [nzPageSizeOptions]=\"[1, 5, 10, 20, 30, 40]\"   [nzPageIndex]=\"meta.curPage\"   [nzPageSize]=\"meta.perPage\"   [nzTotal]=\"meta.totalResults || 0\"   (nzQueryParams)=\"     loadMany({       queryParams: $event     })   \"   [nzData]=\"(items$ | async) || []\" &gt;   &lt;thead&gt;     &lt;tr&gt;       @for (key of columns; track $index) {       &lt;th [nzColumnKey]=\"key\" [nzSortFn]=\"true\" [nzSortOrder]=\"meta.sort[key] | nzTableSortOrderDetector\"&gt;webhook.grid.{{ key }}&lt;\/th&gt;       }       &lt;th&gt;Action&lt;\/th&gt;     &lt;\/tr&gt;   &lt;\/thead&gt;   @if (selectedIds$ | async; as selectedIds) {   &lt;tbody&gt;     @for (data of basicTable.data; track $index) {     &lt;tr (click)=\"selectedIds$.next(selectedIds[0] === data.id ? [] : [data.id])\" [class.selected]=\"selectedIds[0] === data.id\"&gt;       @for (key of columns; track $index) {       &lt;td&gt;{{ data[key] }}&lt;\/td&gt;       }       &lt;td&gt;         &lt;a (click)=\"showCreateOrUpdateModal(data.id)\"&gt;Edit&lt;\/a&gt;         &lt;nz-divider nzType=\"vertical\"&gt;&lt;\/nz-divider&gt;         &lt;a (click)=\"showDeleteModal(data.id)\"&gt;Delete&lt;\/a&gt;       &lt;\/td&gt;     &lt;\/tr&gt;     }   &lt;\/tbody&gt;   } &lt;\/nz-table&gt; } <\/code><\/pre>\n<h4>6. \u0421\u043e\u0437\u0434\u0430\u0435\u043c E2E-\u0442\u0435\u0441\u0442 \u0434\u043b\u044f \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0438 \u0440\u0430\u0431\u043e\u0442\u044b \u0444\u043e\u0440\u043c\u044b \u0438 \u0442\u0430\u0431\u043b\u0438\u0446\u044b<\/h4>\n<p>\u0412 \u0442\u0435\u043a\u0443\u0449\u0435\u043c \u0442\u0435\u0441\u0442\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u044f\u0435\u043c \u0434\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435 \u043e\u043f\u0446\u0438\u0438 \u0434\u043b\u044f \u0437\u0430\u043f\u0438\u0441\u0438 \u043f\u0440\u043e\u0438\u0441\u0445\u043e\u0434\u044f\u0449\u0435\u0433\u043e \u043d\u0430 \u0432\u0438\u0434\u0435\u043e, \u0432\u0438\u0434\u0435\u043e \u043f\u043e\u043c\u043e\u0433\u0430\u044e\u0442 \u0431\u044b\u0441\u0442\u0440\u0435\u0435 \u043f\u043e\u043d\u044f\u0442\u044c \u0432\u043e\u0437\u043d\u0438\u043a\u0430\u044e\u0449\u0438\u0435 \u043e\u0448\u0438\u0431\u043a\u0438.<\/p>\n<p>\u0421\u043e\u0437\u0434\u0430\u0435\u043c \u0444\u0430\u0439\u043b <em>apps\/client-e2e\/src\/webhook-crud-as-user.spec.ts<\/em><\/p>\n<pre><code class=\"typescript\">import { getRandomExternalHeaders } from '@nestjs-mod-fullstack\/testing'; import { expect, Page, test } from '@playwright\/test'; import { join } from 'path'; import { setTimeout } from 'timers\/promises';  test.describe('CRUD operations with Webhook as \"User\" role', () =&gt; {   const user1Headers = getRandomExternalHeaders();    test.describe.configure({ mode: 'serial' });    let page: Page;   let webhookId: string | null;    test.beforeAll(async ({ browser }) =&gt; {     page = await browser.newPage({       viewport: { width: 1920, height: 1080 },       recordVideo: {         dir: join(__dirname, 'video'),         size: { width: 1920, height: 1080 },       },     });   });    test.afterAll(async () =&gt; {     await page.close();   });    test('sign in as user', async () =&gt; {     await page.goto('\/sign-in', {       timeout: 5000,     });      await page.locator('webhook-auth-form').locator('[placeholder=xExternalUserId]').click();     await page.keyboard.type(user1Headers['x-external-user-id'], {       delay: 50,     });     await expect(page.locator('webhook-auth-form').locator('[placeholder=xExternalUserId]')).toHaveValue(user1Headers['x-external-user-id']);      await page.locator('webhook-auth-form').locator('[placeholder=xExternalTenantId]').click();     await page.keyboard.type(user1Headers['x-external-tenant-id'], {       delay: 50,     });     await expect(page.locator('webhook-auth-form').locator('[placeholder=xExternalTenantId]')).toHaveValue(user1Headers['x-external-tenant-id']);      await expect(page.locator('webhook-auth-form').locator('button[type=submit]')).toHaveText('Sign-in');      await page.locator('webhook-auth-form').locator('button[type=submit]').click();   });    test('should create new webhook', async () =&gt; {     await page.locator('webhook-grid').locator('button').first().click();      await setTimeout(5000);      await page.locator('webhook-form').locator('[placeholder=eventName]').click();     await page.keyboard.press('Enter', { delay: 100 });     await expect(page.locator('webhook-form').locator('[placeholder=eventName]')).toContainText('create');      await page.locator('webhook-form').locator('[placeholder=endpoint]').click();     await page.keyboard.type('http:\/\/example.com', { delay: 50 });     await expect(page.locator('webhook-form').locator('[placeholder=endpoint]').first()).toHaveValue('http:\/\/example.com');      await page.locator('webhook-form').locator('[placeholder=headers]').click();     await page.keyboard.type(JSON.stringify(user1Headers), { delay: 50 });     await expect(page.locator('webhook-form').locator('[placeholder=headers]')).toHaveValue(JSON.stringify(user1Headers));      await page.locator('[nz-modal-footer]').locator('button').last().click();      await setTimeout(3000);      webhookId = await page.locator('webhook-grid').locator('td').nth(0).textContent();     await expect(page.locator('webhook-grid').locator('td').nth(1)).toContainText('false');     await expect(page.locator('webhook-grid').locator('td').nth(2)).toContainText('http:\/\/example.com');     await expect(page.locator('webhook-grid').locator('td').nth(3)).toContainText('app-demo.create');     await expect(page.locator('webhook-grid').locator('td').nth(4)).toContainText(JSON.stringify(user1Headers));     await expect(page.locator('webhook-grid').locator('td').nth(5)).toContainText('');   });    test('should update webhook endpoint', async () =&gt; {     await page.locator('webhook-grid').locator('td').last().locator('a').first().click();      await setTimeout(5000);      await expect(page.locator('webhook-form').locator('[placeholder=eventName]')).toContainText('create');      await expect(page.locator('webhook-form').locator('[placeholder=endpoint]').first()).toHaveValue('http:\/\/example.com');      await expect(page.locator('webhook-form').locator('[placeholder=headers]')).toHaveValue(JSON.stringify(user1Headers));      await page.locator('webhook-form').locator('[placeholder=endpoint]').click();     await page.keyboard.press('Control+a');     await page.keyboard.type('http:\/\/example.com\/new', { delay: 100 });     await expect(page.locator('webhook-form').locator('[placeholder=endpoint]').first()).toHaveValue('http:\/\/example.com\/new');      await page.locator('[nz-modal-footer]').locator('button').last().click();      await setTimeout(3000);      await expect(page.locator('webhook-grid').locator('td').nth(0)).toContainText(webhookId || 'empty');     await expect(page.locator('webhook-grid').locator('td').nth(1)).toContainText('false');     await expect(page.locator('webhook-grid').locator('td').nth(2)).toContainText('http:\/\/example.com\/new');     await expect(page.locator('webhook-grid').locator('td').nth(3)).toContainText('app-demo.create');     await expect(page.locator('webhook-grid').locator('td').nth(4)).toContainText(JSON.stringify(user1Headers));     await expect(page.locator('webhook-grid').locator('td').nth(5)).toContainText('');   });    test('should delete updated webhook', async () =&gt; {     await page.locator('webhook-grid').locator('td').last().locator('a').last().click();      await setTimeout(5000);      await expect(page.locator('nz-modal-confirm-container').locator('.ant-modal-confirm-title')).toContainText(`Delete webhook #${webhookId}`);      await page.locator('nz-modal-confirm-container').locator('.ant-modal-body').locator('button').last().click();      await setTimeout(3000);      await expect(page.locator('webhook-grid').locator('nz-embed-empty')).toContainText(`No Data`);   });    test('sign out', async () =&gt; {     await expect(page.locator('nz-header').locator('[nz-submenu]')).toContainText(`You are logged in as User`);     await page.locator('nz-header').locator('[nz-submenu]').first().click();      await expect(page.locator('[nz-submenu-none-inline-child]').locator('[nz-menu-item]')).toContainText(`Sign-out`);      await page.locator('[nz-submenu-none-inline-child]').locator('[nz-menu-item]').first().click();      await setTimeout(3000);      await expect(page.locator('nz-header').locator('[nz-menu-item]').last()).toContainText(`Sign-in`);   }); }); <\/code><\/pre>\n<h4>\u0417\u0430\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435<\/h4>\n<p>\u0412 \u043f\u0440\u043e\u0446\u0435\u0441\u0441\u0435 \u043d\u0430\u043f\u0438\u0441\u0430\u043d\u0438\u044f \u043a\u043e\u0434\u0430 \u0434\u043b\u044f \u0434\u0430\u043d\u043d\u043e\u0439 \u0441\u0442\u0430\u0442\u044c\u0438 \u0442\u0430\u043a\u0436\u0435 \u0431\u044b\u043b\u0438 \u0441\u043e\u0437\u0434\u0430\u043d\u044b: \u0442\u0430\u0431\u043b\u0438\u0446\u0430 \u0434\u043b\u044f \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u044f \u0434\u0435\u043c\u043e \u0434\u0430\u043d\u043d\u044b\u0445 \u0438 \u0444\u043e\u0440\u043c\u0430 \u0434\u043b\u044f \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u044f \u0434\u0435\u043c\u043e \u0434\u0430\u043d\u043d\u044b\u0445, \u0430 \u0442\u0430\u043a\u0436\u0435 \u0440\u0430\u0437\u043b\u0438\u0447\u043d\u044b\u0435 \u0443\u0442\u0438\u043b\u0438\u0442\u044b \u0438 \u0445\u0435\u043b\u043f\u0435\u0440\u044b.<\/p>\n<p>\u0422\u0438\u043f\u043e\u0432\u0430\u044f \u0440\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u043a\u0430 \u043f\u0440\u043e\u0435\u043a\u0442\u0430 \u0432\u043a\u043b\u044e\u0447\u0430\u0435\u0442 \u0432 \u0441\u0435\u0431\u044f \u043c\u043d\u043e\u0433\u043e <code>\u0431\u043e\u0439\u043b\u0435\u0440\u043f\u043b\u0435\u0439\u0442<\/code>-\u043a\u043e\u0434\u0430, \u0440\u0435\u0448\u0435\u043d\u0438\u0435 \u043f\u0440\u043e\u0431\u043b\u0435\u043c \u0441 <code>\u0431\u043e\u0439\u043b\u0435\u0440\u043f\u043b\u0435\u0439\u0442<\/code>-\u043a\u043e\u0434\u043e\u043c \u0432 \u0442\u0435\u043a\u0443\u0449\u0438\u0445 \u0441\u0442\u0430\u0442\u044c\u044f\u0445 \u043d\u0435 \u043e\u043f\u0438\u0441\u044b\u0432\u0430\u044e\u0442\u0441\u044f, \u0441\u0435\u0439\u0447\u0430\u0441 \u043e\u043f\u0438\u0441\u044b\u0432\u0430\u044e\u0442\u0441\u044f \u0440\u0430\u0437\u043b\u0438\u0447\u043d\u044b\u0435 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0439 \u043a\u0430\u043a \u0432\u043d\u0435\u0448\u043d\u0438\u0435, \u0442\u0430\u043a \u0438 \u0432\u043d\u0443\u0442\u0440\u0435\u043d\u043d\u0438\u0435.<\/p>\n<h4>\u041f\u043b\u0430\u043d\u044b<\/h4>\n<p>\u0412 \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0435\u043c \u043f\u043e\u0441\u0442\u0435 \u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0443 \u0432\u043d\u0435\u0448\u043d\u0438\u0439 \u0441\u0435\u0440\u0432\u0435\u0440 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438 <a href=\"https:\/\/authorizer.dev\" rel=\"noopener noreferrer nofollow\">https:\/\/authorizer.dev<\/a> \u0432 \u043f\u0440\u043e\u0435\u043a\u0442&#8230;<\/p>\n<h4>\u0421\u0441\u044b\u043b\u043a\u0438<\/h4>\n<p><a href=\"https:\/\/nestjs.com\" rel=\"noopener noreferrer nofollow\">https:\/\/nestjs.com<\/a> &#8212; \u043e\u0444\u0438\u0446\u0438\u0430\u043b\u044c\u043d\u044b\u0439 \u0441\u0430\u0439\u0442 \u0444\u0440\u0435\u0439\u043c\u0432\u043e\u0440\u043a\u0430<br \/> <a href=\"https:\/\/nestjs-mod.com\" rel=\"noopener noreferrer nofollow\">https:\/\/nestjs-mod.com<\/a> &#8212; \u043e\u0444\u0438\u0446\u0438\u0430\u043b\u044c\u043d\u044b\u0439 \u0441\u0430\u0439\u0442 \u0434\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0445 \u0443\u0442\u0438\u043b\u0438\u0442<br \/> <a href=\"https:\/\/fullstack.nestjs-mod.com\" rel=\"noopener noreferrer nofollow\">https:\/\/fullstack.nestjs-mod.com<\/a> &#8212; \u0441\u0430\u0439\u0442 \u0438\u0437 \u043f\u043e\u0441\u0442\u0430<br \/> <a href=\"https:\/\/github.com\/nestjs-mod\/nestjs-mod-fullstack\" rel=\"noopener noreferrer nofollow\">https:\/\/github.com\/nestjs-mod\/nestjs-mod-fullstack<\/a> &#8212; \u043f\u0440\u043e\u0435\u043a\u0442 \u0438\u0437 \u043f\u043e\u0441\u0442\u0430<br \/> <a href=\"https:\/\/github.com\/nestjs-mod\/nestjs-mod-fullstack\/compare\/ec8de9d574a6dbcef3c3339e876ce156a3974aae..414980df21e585cb798e1ff756300c4547e68a42\" rel=\"noopener noreferrer nofollow\">https:\/\/github.com\/nestjs-mod\/nestjs-mod-fullstack\/compare\/ec8de9d574a6dbcef3c3339e876ce156a3974aae..414980df21e585cb798e1ff756300c4547e68a42<\/a> &#8212; \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u044f<br \/> <a href=\"https:\/\/github.com\/nestjs-mod\/nestjs-mod-fullstack\/actions\/runs\/11523894922\/artifacts\/2105784301\" rel=\"noopener noreferrer nofollow\">https:\/\/github.com\/nestjs-mod\/nestjs-mod-fullstack\/actions\/runs\/11523894922\/artifacts\/2105784301<\/a> &#8212; \u0432\u0438\u0434\u0435\u043e \u0442\u0435\u0441\u0442\u043e\u0432<\/p>\n<\/div>\n<\/div>\n<\/div>\n<p><!----><!----><\/div>\n<p><!----><!----><br \/> \u0441\u0441\u044b\u043b\u043a\u0430 \u043d\u0430 \u043e\u0440\u0438\u0433\u0438\u043d\u0430\u043b \u0441\u0442\u0430\u0442\u044c\u0438 <a href=\"https:\/\/habr.com\/ru\/articles\/853582\/\"> https:\/\/habr.com\/ru\/articles\/853582\/<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<div><!--[--><!--]--><\/div>\n<div id=\"post-content-body\">\n<div>\n<div class=\"article-formatted-body article-formatted-body article-formatted-body_version-2\">\n<div xmlns=\"http:\/\/www.w3.org\/1999\/xhtml\">\n<p>\u041f\u0440\u0435\u0434\u044b\u0434\u0443\u0449\u0430\u044f \u0441\u0442\u0430\u0442\u044c\u044f: <a href=\"https:\/\/habr.com\/ru\/articles\/848634\/\" rel=\"noopener noreferrer nofollow\">\u0421\u043e\u0437\u0434\u0430\u043d\u0438\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0443\u0435\u043c\u043e\u0433\u043e Webhook-\u043c\u043e\u0434\u0443\u043b\u044f \u0434\u043b\u044f NestJS-\u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0438<\/a><\/p>\n<p>\u0412 \u044d\u0442\u043e\u0439 \u0441\u0442\u0430\u0442\u044c\u0435 \u044f \u043e\u043f\u0438\u0448\u0443 \u0441\u043e\u0437\u0434\u0430\u043d\u0438\u0435 \u0442\u0430\u0431\u043b\u0438\u0447\u043a\u0438 \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0430\u044e\u0449\u0435\u0439 \u0434\u0430\u043d\u043d\u044b\u0435 \u0438 \u0444\u043e\u0440\u043c\u044b \u0434\u043b\u044f \u0435\u0435 \u0437\u0430\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f, \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u044b \u0441\u0442\u0440\u043e\u044f\u0442\u0441\u044f \u043d\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430\u0445 \u043e\u0442 <a href=\"https:\/\/ng.ant.design\" rel=\"noopener noreferrer nofollow\">https:\/\/ng.ant.design<\/a>, \u0444\u043e\u0440\u043c\u044b \u0441\u043e\u0437\u0434\u0430\u044e\u0442\u0441\u044f \u0438 \u0443\u043f\u0440\u0430\u0432\u043b\u044f\u044e\u0442\u0441\u044f \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e <a href=\"https:\/\/formly.dev\" rel=\"noopener noreferrer nofollow\">https:\/\/formly.dev<\/a>, \u0434\u043b\u044f \u0441\u0442\u0438\u043b\u0435\u0439 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f <a href=\"https:\/\/tailwindcss.com\" rel=\"noopener noreferrer nofollow\">https:\/\/tailwindcss.com<\/a>, \u0441\u0442\u0435\u0439\u0442 \u043c\u0430\u0448\u0438\u043d\u044b \u043d\u0435\u0442.<\/p>\n<h4>1. \u0421\u043e\u0437\u0434\u0430\u0435\u043c \u043f\u0443\u0441\u0442\u0443\u044e Angular \u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a\u0443<\/h4>\n<p>\u0412 \u044d\u0442\u043e\u0439 \u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a\u0435 \u043b\u0435\u0436\u0430\u0442 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u044b \u0434\u043b\u044f \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u044f \u0438 \u0440\u0430\u0431\u043e\u0442\u044b \u0441 \u0434\u0430\u043d\u043d\u044b\u043c\u0438 \u0441\u0443\u0449\u043d\u043e\u0441\u0442\u0438 <code>Webhook<\/code>.<\/p>\n<p><em>Commands<\/em><\/p>\n<pre><code class=\"bash\"># Create Angular library .\/node_modules\/.bin\/nx g @nx\/angular:library webhook-angular --buildable --publishable --directory=libs\/feature\/webhook-angular --simpleName=true --projectNameAndRootFormat=as-provided --strict=true --prefix= --standalone=true --selector= --changeDetection=OnPush --importPath=@nestjs-mod-fullstack\/webhook-angular  # Change file with test options rm -rf libs\/feature\/webhook-angular\/src\/test-setup.ts cp apps\/client\/src\/test-setup.ts libs\/feature\/webhook-angular\/src\/test-setup.ts <\/code><\/pre>\n<details class=\"spoiler\">\n<summary>\u0412\u044b\u0432\u043e\u0434 \u043a\u043e\u043d\u0441\u043e\u043b\u0438<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"bash\">$ .\/node_modules\/.bin\/nx g @nx\/angular:library webhook-angular --buildable --publishable --directory=libs\/feature\/webhook-angular --simpleName=true --projectNameAndRootFormat=as-provided --strict=true --prefix= --standalone=true --selector= --changeDetection=OnPush --importPath=@nestjs-mod-fullstack\/webhook-angular   NX  Generating @nx\/angular:library  CREATE libs\/feature\/webhook-angular\/project.json CREATE libs\/feature\/webhook-angular\/README.md CREATE libs\/feature\/webhook-angular\/ng-package.json CREATE libs\/feature\/webhook-angular\/package.json CREATE libs\/feature\/webhook-angular\/tsconfig.json CREATE libs\/feature\/webhook-angular\/tsconfig.lib.json CREATE libs\/feature\/webhook-angular\/tsconfig.lib.prod.json CREATE libs\/feature\/webhook-angular\/src\/index.ts CREATE libs\/feature\/webhook-angular\/jest.config.ts CREATE libs\/feature\/webhook-angular\/src\/test-setup.ts CREATE libs\/feature\/webhook-angular\/tsconfig.spec.json CREATE libs\/feature\/webhook-angular\/src\/lib\/webhook-angular\/webhook-angular.component.css CREATE libs\/feature\/webhook-angular\/src\/lib\/webhook-angular\/webhook-angular.component.html CREATE libs\/feature\/webhook-angular\/src\/lib\/webhook-angular\/webhook-angular.component.spec.ts CREATE libs\/feature\/webhook-angular\/src\/lib\/webhook-angular\/webhook-angular.component.ts CREATE libs\/feature\/webhook-angular\/.eslintrc.json UPDATE package.json UPDATE tsconfig.base.json  &gt; @nestjs-mod-fullstack\/source@0.0.9 prepare &gt; npx -y husky install  install command is DEPRECATED  removed 2 packages, changed 5 packages, and audited 2726 packages in 13s  332 packages are looking for funding   run `npm fund` for details  33 vulnerabilities (4 low, 12 moderate, 17 high)  To address issues that do not require attention, run:   npm audit fix  To address all issues possible (including breaking changes), run:   npm audit fix --force  Some issues need review, and may require choosing a different dependency.  Run `npm audit` for details.   NX   \ud83d\udc40 View Details of webhook-angular  Run \"nx show project webhook-angular\" to view details about this project. <\/code><\/pre>\n<\/div>\n<\/details>\n<h4>2. \u0421\u043e\u0437\u0434\u0430\u0435\u043c \u043e\u0431\u0449\u0443\u044e Angular \u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a\u0443<\/h4>\n<p>\u041e\u0431\u0449\u0430\u044f \u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a\u0430 \u0441\u043e\u0434\u0435\u0440\u0436\u0438\u0442 \u0444\u0443\u043d\u043a\u0446\u0438\u0438 \u0438 \u043a\u043b\u0430\u0441\u0441\u044b \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u044e\u0442\u0441\u044f \u0434\u0440\u0443\u0433\u0438\u043c\u0438 <code>Angular<\/code>-\u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a\u0430\u043c\u0438.<\/p>\n<p><em>Commands<\/em><\/p>\n<pre><code class=\"bash\"># Create Angular library .\/node_modules\/.bin\/nx g @nx\/angular:library common-angular --buildable --publishable --directory=libs\/common-angular --simpleName=true --projectNameAndRootFormat=as-provided --strict=true --prefix= --standalone=true --selector= --changeDetection=OnPush --importPath=@nestjs-mod-fullstack\/common-angular  # Change file with test options rm -rf libs\/common-angular\/src\/test-setup.ts cp apps\/client\/src\/test-setup.ts libs\/common-angular\/src\/test-setup.ts <\/code><\/pre>\n<details class=\"spoiler\">\n<summary>\u0412\u044b\u0432\u043e\u0434 \u043a\u043e\u043d\u0441\u043e\u043b\u0438<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"bash\">$ .\/node_modules\/.bin\/nx g @nx\/angular:library common-angular --buildable --publishable --directory=libs\/common-angular --simpleName=true --projectNameAndRootFormat=as-provided --strict=true --prefix= --standalone=true --selector= --changeDetection=OnPush --importPath=@nestjs-mod-fullstack\/common-angular   NX  Generating @nx\/angular:library  CREATE libs\/common-angular\/project.json CREATE libs\/common-angular\/README.md CREATE libs\/common-angular\/ng-package.json CREATE libs\/common-angular\/package.json CREATE libs\/common-angular\/tsconfig.json CREATE libs\/common-angular\/tsconfig.lib.json CREATE libs\/common-angular\/tsconfig.lib.prod.json CREATE libs\/common-angular\/src\/index.ts CREATE libs\/common-angular\/jest.config.ts CREATE libs\/common-angular\/src\/test-setup.ts CREATE libs\/common-angular\/tsconfig.spec.json CREATE libs\/common-angular\/src\/lib\/common-angular\/common-angular.component.css CREATE libs\/common-angular\/src\/lib\/common-angular\/common-angular.component.html CREATE libs\/common-angular\/src\/lib\/common-angular\/common-angular.component.spec.ts CREATE libs\/common-angular\/src\/lib\/common-angular\/common-angular.component.ts CREATE libs\/common-angular\/.eslintrc.json UPDATE tsconfig.base.json   NX   \ud83d\udc40 View Details of common-angular  Run \"nx show project common-angular\" to view details about this project. <\/code><\/pre>\n<\/div>\n<\/details>\n<h4>3. \u0423\u0441\u0442\u0430\u043d\u0430\u0432\u043b\u0438\u0432\u0430\u0435\u043c \u0434\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435 \u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a\u0438<\/h4>\n<p>\u0423\u0441\u0442\u0430\u043d\u0430\u0432\u043b\u0438\u0432\u0430\u0435\u043c \u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a\u0443 \u0432\u0438\u0437\u0443\u0430\u043b\u044c\u043d\u044b\u0445 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u043e\u0432 <code>ng-zorro-antd<\/code>, \u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a\u0443 \u0434\u043b\u044f \u0440\u0430\u0431\u043e\u0442\u044b \u0441 \u0444\u043e\u0440\u043c\u0430\u043c\u0438 <code>@ngx-formly\/core @ngx-formly\/ng-zorro-antd<\/code>, \u0443\u0442\u0438\u043b\u0438\u0442\u0443 \u0434\u043b\u044f \u0430\u0432\u0442\u043e-\u043e\u0442\u043f\u0438\u0441\u043a\u0438 <code>@ngneat\/until-destroy<\/code> \u0438 \u043a\u043e\u043b\u0435\u043a\u0446\u0438\u044e \u0443\u0442\u0438\u043b\u0438\u0442 <code>lodash<\/code>.<\/p>\n<p><em>Commands<\/em><\/p>\n<pre><code class=\"bash\">npm install --save ng-zorro-antd @ngx-formly\/core @ngx-formly\/ng-zorro-antd @ngneat\/until-destroy lodash <\/code><\/pre>\n<details class=\"spoiler\">\n<summary>\u0412\u044b\u0432\u043e\u0434 \u043a\u043e\u043d\u0441\u043e\u043b\u0438<\/summary>\n<div class=\"spoiler__content\">\n<pre><code class=\"bash\">$ npm install --save ng-zorro-antd @ngx-formly\/core @ngx-formly\/ng-zorro-antd @ngneat\/until-destroy  added 8 packages, removed 2 packages, and audited 2794 packages in 25s  343 packages are looking for funding   run `npm fund` for details  38 vulnerabilities (8 low, 12 moderate, 18 high)  To address issues that do not require attention, run:   npm audit fix  To address all issues (including breaking changes), run:   npm audit fix --force  Run `npm audit` for details. <\/code><\/pre>\n<\/div>\n<\/details>\n<h4>4. \u0422\u0430\u043a \u043a\u0430\u043a \u0434\u043b\u044f \u0440\u0430\u0431\u043e\u0442\u044b \u043c\u043e\u0434\u0443\u043b\u044f \u043d\u0443\u0436\u0435\u043d \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440\u044b \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u0438 \u043a\u043e\u043c\u043f\u0430\u043d\u0438\u0438, \u0442\u043e \u043d\u0443\u0436\u043d\u043e \u0441\u043e\u0437\u0434\u0430\u0442\u044c \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u044b \u0434\u043b\u044f \u043f\u0435\u0440\u0435\u0434\u0430\u0447\u0438 \u044d\u0442\u0438\u0445 \u0434\u0430\u043d\u043d\u044b\u0445<\/h4>\n<p>\u0421\u043e\u0437\u0434\u0430\u0435\u043c \u0444\u043e\u0440\u043c\u0443 \u0438 \u0441\u0435\u0440\u0432\u0438\u0441 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438 \u0432 <code>Webhook<\/code> &#8212; \u043c\u043e\u0434\u0443\u043b\u0435.<\/p>\n<p>\u0421\u0435\u0440\u0432\u0438\u0441 \u0438\u043c\u0435\u0435\u0442 \u043c\u0435\u0442\u043e\u0434\u044b \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u043f\u0440\u043e\u0444\u0438\u043b\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u043f\u043e \u043f\u0435\u0440\u0435\u0434\u0430\u043d\u043d\u044b\u043c <code>xExternalUserId<\/code> \u0438 <code>xExternalTenantId<\/code>, \u0430 \u0442\u0430\u043a\u0436\u0435 \u0445\u0440\u0430\u043d\u0438\u0442\u044c \u0438\u0445 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f \u0438 \u0434\u0430\u043d\u043d\u044b\u0435 \u043f\u043e \u043f\u0440\u043e\u0444\u0438\u043b\u044e \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f.<\/p>\n<p>\u0418\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u0430\u0434\u043c\u0438\u043d\u0438\u0441\u0442\u0440\u0430\u0442\u043e\u0440\u0430 \u043f\u0440\u043e\u043a\u0438\u0434\u044b\u0432\u0430\u0435\u0442\u0441\u044f \u0438\u0437 \u043f\u0435\u0440\u0435\u043c\u0435\u043d\u043d\u044b\u0445 \u043e\u043a\u0440\u0443\u0436\u0435\u043d\u0438\u044f <code>CI\/CD<\/code>.<\/p>\n<p>\u0414\u043b\u044f \u0437\u0430\u0449\u0438\u0442\u044b \u0441\u0442\u0440\u0430\u043d\u0438\u0446 \u0441\u043e\u0437\u0434\u0430\u0434\u0438\u043c \u0441\u043f\u0435\u0446\u0438\u0430\u043b\u044c\u043d\u044b\u0439 <code>Guard<\/code>.<\/p>\n<p>\u0421\u043e\u0437\u0434\u0430\u0435\u043c \u0441\u0435\u0440\u0432\u0438\u0441 <em>libs\/feature\/webhook-angular\/src\/lib\/services\/webhook-auth.service.ts<\/em><\/p>\n<pre><code class=\"typescript\">import { Injectable } from '@angular\/core'; import { WebhookErrorInterface, WebhookRestService, WebhookUserObjectInterface } from '@nestjs-mod-fullstack\/app-angular-rest-sdk'; import { UntilDestroy, untilDestroyed } from '@ngneat\/until-destroy'; import { BehaviorSubject, catchError, of, tap, throwError } from 'rxjs';  export type WebhookAuthCredentials = {   xExternalUserId?: string;   xExternalTenantId?: string; };  @UntilDestroy() @Injectable({ providedIn: 'root' }) export class WebhookAuthService {   private webhookAuthCredentials$ = new BehaviorSubject&lt;WebhookAuthCredentials&gt;({});   private webhookUser$ = new BehaviorSubject&lt;WebhookUserObjectInterface | null&gt;(null);    constructor(private readonly webhookRestService: WebhookRestService) {}    getWebhookAuthCredentials() {     return this.webhookAuthCredentials$.value;   }    getWebhookUser() {     return this.webhookUser$.value;   }    setWebhookAuthCredentials(webhookAuthCredentials: WebhookAuthCredentials) {     this.webhookAuthCredentials$.next(webhookAuthCredentials);     this.loadWebhookUser().pipe(untilDestroyed(this)).subscribe();   }    loadWebhookUser() {     return this.webhookRestService.webhookControllerProfile(this.getWebhookAuthCredentials().xExternalUserId, this.getWebhookAuthCredentials().xExternalTenantId).pipe(       tap((profile) =&gt; this.webhookUser$.next(profile)),       catchError((err: { error?: WebhookErrorInterface }) =&gt; {         if (err.error?.code === 'WEBHOOK-002') {           return of(null);         }         return throwError(() =&gt; err);       })     );   }    webhookAuthCredentialsUpdates() {     return this.webhookAuthCredentials$.asObservable();   }    webhookUserUpdates() {     return this.webhookUser$.asObservable();   } } <\/code><\/pre>\n<p>\u041f\u0441\u0435\u0432\u0434\u043e \u0444\u043e\u0440\u043c\u0430 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438 \u0438\u043c\u0435\u0435\u0442 \u0434\u0432\u0430 \u043f\u043e\u043b\u044f <code>xExternalUserId<\/code> \u0438 <code>xExternalTenantId<\/code>, \u043f\u043e\u0441\u0442\u0440\u043e\u0435\u043d\u0438\u0435 \u0438 \u0432\u0430\u043b\u0438\u0434\u0430\u0446\u0438\u044f \u0444\u043e\u0440\u043c\u044b \u043f\u0440\u043e\u0438\u0441\u0445\u043e\u0434\u0438\u0442 \u0447\u0435\u0440\u0435\u0437 \u0431\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a\u0443 <a href=\"https:\/\/formly.dev\" rel=\"noopener noreferrer nofollow\">https:\/\/formly.dev<\/a>.<\/p>\n<p>\u041a\u0440\u043e\u043c\u0435 \u043a\u043d\u043e\u043f\u043a\u0438 \u0432\u043e\u0439\u0442\u0438, \u043d\u0430 \u0444\u043e\u0440\u043c\u0435 \u0442\u0430\u043a\u0436\u0435 \u0435\u0441\u0442\u044c \u0435\u0449\u0435 \u0434\u0432\u0435 \u043a\u043d\u043e\u043f\u043a\u0438:<\/p>\n<ol>\n<li>\n<p>\u0417\u0430\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u0434\u0430\u043d\u043d\u044b\u0435 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f &#8212; \u043f\u043e\u0434\u0441\u0442\u0430\u0432\u043b\u044f\u0435\u0442 \u0432 <code>xExternalUserId<\/code> \u0438 <code>xExternalTenantId<\/code> \u0437\u0430\u0445\u0430\u0440\u0434\u043a\u043e\u0436\u0435\u043d\u043d\u044b\u0435 \u0441\u043b\u0443\u0447\u0430\u0439\u043d\u044b\u0435 <code>uuid<\/code>-\u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440\u044b;<\/p>\n<\/li>\n<li>\n<p>\u0417\u0430\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u0434\u0430\u043d\u043d\u044b\u0435 \u0430\u0434\u043c\u0438\u043d\u0438\u0441\u0442\u0440\u0430\u0442\u043e\u0440\u0430 &#8212; \u043f\u043e\u0434\u0441\u0442\u0430\u0432\u043b\u044f\u0435\u0442 \u0432 <code>xExternalUserId<\/code> \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u0441 \u0440\u043e\u043b\u044c\u044e <code>Admin<\/code>, \u0431\u044d\u043a\u0435\u043d\u0434 \u043f\u0440\u0438 \u0441\u0442\u0430\u0440\u0442\u0435 \u0441\u043e\u0437\u0434\u0430\u0435\u0442 \u044d\u0442\u043e\u0433\u043e \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f, \u0430 \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440 \u0432\u0441\u0442\u0430\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0432\u043e \u0444\u0440\u043e\u043d\u0442\u0435\u043d\u0434 \u043f\u0440\u0438 \u0441\u0431\u043e\u0440\u043a\u0435 \u0435\u0433\u043e \u0432 <code>CI\\CD<\/code>.<\/p>\n<\/li>\n<\/ol>\n<p>\u0421\u043e\u0437\u0434\u0430\u0435\u043c \u0444\u0430\u0439\u043b <em>libs\/feature\/webhook-angular\/src\/lib\/forms\/webhook-auth-form\/webhook-auth-form.component.ts<\/em><\/p>\n<pre><code class=\"typescript\">import { AsyncPipe, NgIf } from '@angular\/common'; import { ChangeDetectionStrategy, Component, EventEmitter, Inject, Input, OnInit, Optional, Output } from '@angular\/core'; import { FormsModule, ReactiveFormsModule, UntypedFormGroup } from '@angular\/forms'; import { FormlyFieldConfig, FormlyModule } from '@ngx-formly\/core'; import { NzButtonModule } from 'ng-zorro-antd\/button'; import { NzFormModule } from 'ng-zorro-antd\/form'; import { NzInputModule } from 'ng-zorro-antd\/input'; import { NzMessageService } from 'ng-zorro-antd\/message'; import { NZ_MODAL_DATA } from 'ng-zorro-antd\/modal'; import { BehaviorSubject } from 'rxjs'; import { WebhookAuthCredentials, WebhookAuthService } from '..\/..\/services\/webhook-auth.service'; import { WEBHOOK_CONFIGURATION_TOKEN, WebhookConfiguration } from '..\/..\/services\/webhook.configuration';  @Component({   standalone: true,   imports: [FormlyModule, NzFormModule, NzInputModule, NzButtonModule, FormsModule, ReactiveFormsModule, AsyncPipe, NgIf],   selector: 'webhook-auth-form',   templateUrl: '.\/webhook-auth-form.component.html',   changeDetection: ChangeDetectionStrategy.OnPush, }) export class WebhookAuthFormComponent<\/code><\/pre>\n<\/div>\n<\/div>\n<\/div>\n<\/div>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[],"tags":[],"class_list":["post-436758","post","type-post","status-publish","format-standard","hentry"],"_links":{"self":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts\/436758","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=436758"}],"version-history":[{"count":0,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts\/436758\/revisions"}],"wp:attachment":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=436758"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=436758"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=436758"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}