Aktivity „LinchArnold“

Hi, ABP teams:

  1. I use the newest v7.2.2 suite to generate Angular UI/ Tiered Project with Lepton Theme.
  2. Create new Foobar entity with Required Name property.
  3. For angular side, I removed the Validators.Required validator of name in the buildForm function.
  4. Start the projects and angular, add new foobar with the Name input blank. Click the save button I got the below errors: Error detail not sent by server.

the server return the validation errors as follow:

The expected error message should something like The Name field is required.

If you're creating a bug/problem report, please include followings:

  • ABP Framework version: v7.2.2
  • UI type: Angular
  • DB provider: EF Core
  • Tiered (MVC) or Identity Server Separated (Angular): yes

Thanks. @masum.ulu

Hi, I came across this problem months ago.

After some research, I fixed it with the following steps:

In AuthServer Module.ts :

public override void PreConfigureServices(ServiceConfigurationContext context)
{
    // other codes.

    PreConfigure<AbpOpenIddictAspNetCoreOptions>(options =>
            {
                options.AddDevelopmentEncryptionAndSigningCertificate = false;
            });

            PreConfigure<OpenIddictServerBuilder>(builder =>
            {
                builder.AddSigningCertificate(GetSigningCertificate(hostingEnvironment, configuration));
                builder.AddEncryptionCertificate(GetSigningCertificate(hostingEnvironment, configuration));
                if (!hostingEnvironment.IsProduction())
                {
                    builder.SetIssuer(new Uri(configuration["AuthServer:Authority"]!));
                }
            });
            
                PreConfigure<AbpOpenIddictWildcardDomainOptions>(options =>
                {
                    options.EnableWildcardDomainSupport = true;
                    options.WildcardDomainsFormat.Add(configuration["App:AngularWildcardUrl"]);
                });

                PreConfigure<OpenIddictServerOptions>(options =>
                {
                    options.TokenValidationParameters.IssuerValidator = TokenWildcardIssuerValidator.IssuerValidator;
                    options.TokenValidationParameters.ValidIssuers = new[]
                    {
                        configuration["AuthServer:Authority"].EnsureEndsWith('/'),
                        configuration["AuthServer:WildcardAuthority"].EnsureEndsWith('/')
                    };
                });
      
}

Note: TokenWildcardIssuerValidator.IssuerValidator is from package <PackageReference Include="Owl.TokenWildcardIssuerValidator" Version="1.0.0" />

In HttpApi.Host module ts:

public override void ConfigureServices(ServiceConfigurationContext context)
{
    // other codes
    context.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
            .AddJwtBearer(options =>
            {
                options.Authority = configuration["AuthServer:Authority"];
                options.RequireHttpsMetadata = Convert.ToBoolean(configuration["AuthServer:RequireHttpsMetadata"]);
                options.Audience = "PayloadLimitPfmNext";

                if (hostingEnvironment.IsProduction())
                {
                    options.TokenValidationParameters.IssuerValidator = TokenWildcardIssuerValidator.IssuerValidator;
                    options.TokenValidationParameters.ValidIssuers = new[]
                    {
                        configuration["AuthServer:Authority"].EnsureEndsWith('/'),
                        configuration["AuthServer:WildcardAuthority"].EnsureEndsWith('/')
                    };
                }
            });
}

Note: TokenWildcardIssuerValidator.IssuerValidator is from package <PackageReference Include="Owl.TokenWildcardIssuerValidator" Version="1.0.0" />

In Angular:

  1. Create a multi-tenancy-utils.ts file, this will be override the default implementation of ABP framework:
import {
  ABP,
  CORE_OPTIONS,
  Environment,
  EnvironmentService,
  createTokenParser,
  getRemoteEnv,
} from '@abp/ng.core';
import { Injector } from '@angular/core';
import clone from 'just-clone';

const tenancyPlaceholder = '{0}';

function getCurrentTenancyName(appBaseUrl: string): string {
  if (appBaseUrl.charAt(appBaseUrl.length - 1) !== '/') appBaseUrl += '/';
  const parseTokens = createTokenParser(appBaseUrl);
  const token = tenancyPlaceholder.replace(/[}{]/g, '');
  return parseTokens(window.location.href)[token]?.[0];
}

export function cleanPlaceholderFromHostUrl(injector: Injector) {
  const fn = async () => {
    const environmentService = injector.get(EnvironmentService);
    const options = injector.get(CORE_OPTIONS) as ABP.Root;
    environmentService.setState(options.environment as Environment);
    await getRemoteEnv(injector, options.environment);
    const baseUrl =
      environmentService.getEnvironment()['appBaseUrl'] ||
      environmentService.getEnvironment().application?.baseUrl ||
      '';
    const tenancyName = getCurrentTenancyName(baseUrl);

    if (!tenancyName) {
      /**
       * If there is no tenant, we still have to clean up {0}. from baseUrl to avoid incorrect http requests.
       */
      replaceTenantNameWithinEnvironment(injector, '', tenancyPlaceholder + '.');
      replaceTenantNameWithinEnvironment(injector, '', tenancyPlaceholder);
    }

    return Promise.resolve();
  };

  return fn;
}

function replaceTenantNameWithinEnvironment(
  injector: Injector,
  tenancyName: string,
  placeholder = tenancyPlaceholder
) {
  const environmentService = injector.get(EnvironmentService);

  const environment = clone(environmentService.getEnvironment()) as Environment;

  if (environment.application.baseUrl) {
    environment.application.baseUrl = environment.application.baseUrl.replace(
      placeholder,
      tenancyName
    );
  }

  if (environment.oAuthConfig?.redirectUri) {
    environment.oAuthConfig.redirectUri = environment.oAuthConfig.redirectUri.replace(
      placeholder,
      tenancyName
    );
  }

  if (!environment.oAuthConfig) {
    environment.oAuthConfig = {};
  }
  environment.oAuthConfig.issuer = (environment.oAuthConfig.issuer || '').replace(
    placeholder,
    tenancyName
  );

  environment.oAuthConfig.clientId = (environment.oAuthConfig.clientId || '').replace(
    placeholder,
    tenancyName
  );

  Object.keys(environment.apis).forEach(api => {
    Object.keys(environment.apis[api]).forEach(key => {
      environment.apis[api][key] = (environment.apis[api][key] || '').replace(
        placeholder,
        tenancyName
      );
    });
  });

  return environmentService.setState(environment);
}
  1. Create domain-resolver.module.ts file:
import { CommonModule } from '@angular/common';
import { HttpClientModule } from '@angular/common/http';
import { APP_INITIALIZER, Injector, NgModule } from '@angular/core';
import { cleanPlaceholderFromHostUrl } from './multi-tenancy-utils';

@NgModule({
  imports: [CommonModule, HttpClientModule],
  providers: [
    {
      provide: APP_INITIALIZER,
      multi: true,
      deps: [Injector],
      useFactory: cleanPlaceholderFromHostUrl,
    },
  ],
})
export class DomainResolverModule {}
  1. Import the DomainResolverModule in your AppModule:
import { CoreModule } from '@abp/ng.core';
import { GdprConfigModule } from '@volo/abp.ng.gdpr/config';
import { SettingManagementConfigModule } from '@abp/ng.setting-management/config';
import { HTTP_ERROR_HANDLER, ThemeSharedModule } from '@abp/ng.theme.shared';
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { CommercialUiConfigModule } from '@volo/abp.commercial.ng.ui/config';
import { AccountAdminConfigModule } from '@volo/abp.ng.account/admin/config';
import { AccountPublicConfigModule } from '@volo/abp.ng.account/public/config';
import { AuditLoggingConfigModule } from '@volo/abp.ng.audit-logging/config';
import { IdentityConfigModule } from '@volo/abp.ng.identity/config';
import { LanguageManagementConfigModule } from '@volo/abp.ng.language-management/config';
import { registerLocale } from '@volo/abp.ng.language-management/locale';
import { SaasConfigModule } from '@volo/abp.ng.saas/config';
import { TextTemplateManagementConfigModule } from '@volo/abp.ng.text-template-management/config';
import { HttpErrorComponent, ThemeLeptonModule } from '@volo/abp.ng.theme.lepton';
import { environment } from '../environments/environment';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { APP_ROUTE_PROVIDER } from './route.provider';
import { OpeniddictproConfigModule } from '@volo/abp.ng.openiddictpro/config';
import { FeatureManagementModule } from '@abp/ng.feature-management';
import { AbpOAuthModule } from '@abp/ng.oauth';
import { ServiceWorkerModule } from '@angular/service-worker';
import { APP_VALIDATION_PROVIDER } from './validation/app.validation.provider';
import { DomainResolverModule } from './customization';

@NgModule({
  declarations: [AppComponent],
  imports: [
    BrowserModule,
    BrowserAnimationsModule,
    AppRoutingModule,
    DomainResolverModule,
    CoreModule.forRoot({
      environment,
      registerLocaleFn: registerLocale(),
    }),
    AbpOAuthModule.forRoot(),
    ThemeSharedModule.forRoot({
      httpErrorConfig: {
        errorScreen: {
          component: HttpErrorComponent,
          forWhichErrors: [401, 403, 404, 500],
          hideCloseIcon: true,
        },
      },
    }),
    // Other import statements
  ],
  providers: [
    APP_ROUTE_PROVIDER,
    APP_VALIDATION_PROVIDER
  ],
  bootstrap: [AppComponent],
})
export class AppModule {}

After this, if the website wildcard is web{0}.sample.com, the web.sample.com will be host, webtenant1.sample.com will be tenant tenant1.

Hello alper, article helped to solve redirect_uri issue, but now I face angular problem where if you go to https://testtenant.ng.abp.net:4200 angular doesn't load because if he thinks that user is not authenticated no matter authserver returned valid token and issuer matching angular issuer environment config.

I do have all this abp commercial microservices tenant subdomain mapping config in separate test repository which im ok to share with you if needed.

I still need help with finishing this abp commercial microservices feature work.

Hi, kirotech, if you have problem at the angular side, you can try the following steps, maybe it will work:

  1. Create a multi-tenancy-utils.ts file, this will be override the default implementation of ABP framework:
import {
  ABP,
  CORE_OPTIONS,
  Environment,
  EnvironmentService,
  createTokenParser,
  getRemoteEnv,
} from '@abp/ng.core';
import { Injector } from '@angular/core';
import clone from 'just-clone';

const tenancyPlaceholder = '{0}';

function getCurrentTenancyName(appBaseUrl: string): string {
  if (appBaseUrl.charAt(appBaseUrl.length - 1) !== '/') appBaseUrl += '/';
  const parseTokens = createTokenParser(appBaseUrl);
  const token = tenancyPlaceholder.replace(/[}{]/g, '');
  return parseTokens(window.location.href)[token]?.[0];
}

export function cleanPlaceholderFromHostUrl(injector: Injector) {
  const fn = async () => {
    const environmentService = injector.get(EnvironmentService);
    const options = injector.get(CORE_OPTIONS) as ABP.Root;
    environmentService.setState(options.environment as Environment);
    await getRemoteEnv(injector, options.environment);
    const baseUrl =
      environmentService.getEnvironment()['appBaseUrl'] ||
      environmentService.getEnvironment().application?.baseUrl ||
      '';
    const tenancyName = getCurrentTenancyName(baseUrl);

    if (!tenancyName) {
      /**
       * If there is no tenant, we still have to clean up {0}. from baseUrl to avoid incorrect http requests.
       */
      replaceTenantNameWithinEnvironment(injector, '', tenancyPlaceholder + '.');
      replaceTenantNameWithinEnvironment(injector, '', tenancyPlaceholder);
    }

    return Promise.resolve();
  };

  return fn;
}

function replaceTenantNameWithinEnvironment(
  injector: Injector,
  tenancyName: string,
  placeholder = tenancyPlaceholder
) {
  const environmentService = injector.get(EnvironmentService);

  const environment = clone(environmentService.getEnvironment()) as Environment;

  if (environment.application.baseUrl) {
    environment.application.baseUrl = environment.application.baseUrl.replace(
      placeholder,
      tenancyName
    );
  }

  if (environment.oAuthConfig?.redirectUri) {
    environment.oAuthConfig.redirectUri = environment.oAuthConfig.redirectUri.replace(
      placeholder,
      tenancyName
    );
  }

  if (!environment.oAuthConfig) {
    environment.oAuthConfig = {};
  }
  environment.oAuthConfig.issuer = (environment.oAuthConfig.issuer || '').replace(
    placeholder,
    tenancyName
  );

  environment.oAuthConfig.clientId = (environment.oAuthConfig.clientId || '').replace(
    placeholder,
    tenancyName
  );

  Object.keys(environment.apis).forEach(api => {
    Object.keys(environment.apis[api]).forEach(key => {
      environment.apis[api][key] = (environment.apis[api][key] || '').replace(
        placeholder,
        tenancyName
      );
    });
  });

  return environmentService.setState(environment);
}

  1. Create domain-resolver.module.ts file:
import { CommonModule } from '@angular/common';
import { HttpClientModule } from '@angular/common/http';
import { APP_INITIALIZER, Injector, NgModule } from '@angular/core';
import { cleanPlaceholderFromHostUrl } from './multi-tenancy-utils';

@NgModule({
  imports: [CommonModule, HttpClientModule],
  providers: [
    {
      provide: APP_INITIALIZER,
      multi: true,
      deps: [Injector],
      useFactory: cleanPlaceholderFromHostUrl,
    },
  ],
})
export class DomainResolverModule {}
  1. Import the DomainResolverModule in your AppModule:
import { CoreModule } from '@abp/ng.core';
import { GdprConfigModule } from '@volo/abp.ng.gdpr/config';
import { SettingManagementConfigModule } from '@abp/ng.setting-management/config';
import { HTTP_ERROR_HANDLER, ThemeSharedModule } from '@abp/ng.theme.shared';
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { CommercialUiConfigModule } from '@volo/abp.commercial.ng.ui/config';
import { AccountAdminConfigModule } from '@volo/abp.ng.account/admin/config';
import { AccountPublicConfigModule } from '@volo/abp.ng.account/public/config';
import { AuditLoggingConfigModule } from '@volo/abp.ng.audit-logging/config';
import { IdentityConfigModule } from '@volo/abp.ng.identity/config';
import { LanguageManagementConfigModule } from '@volo/abp.ng.language-management/config';
import { registerLocale } from '@volo/abp.ng.language-management/locale';
import { SaasConfigModule } from '@volo/abp.ng.saas/config';
import { TextTemplateManagementConfigModule } from '@volo/abp.ng.text-template-management/config';
import { HttpErrorComponent, ThemeLeptonModule } from '@volo/abp.ng.theme.lepton';
import { environment } from '../environments/environment';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { APP_ROUTE_PROVIDER } from './route.provider';
import { OpeniddictproConfigModule } from '@volo/abp.ng.openiddictpro/config';
import { FeatureManagementModule } from '@abp/ng.feature-management';
import { AbpOAuthModule } from '@abp/ng.oauth';
import { ServiceWorkerModule } from '@angular/service-worker';
import { APP_VALIDATION_PROVIDER } from './validation/app.validation.provider';
import { DomainResolverModule } from './customization';

@NgModule({
  declarations: [AppComponent],
  imports: [
    BrowserModule,
    BrowserAnimationsModule,
    AppRoutingModule,
    DomainResolverModule,
    CoreModule.forRoot({
      environment,
      registerLocaleFn: registerLocale(),
    }),
    AbpOAuthModule.forRoot(),
    ThemeSharedModule.forRoot({
      httpErrorConfig: {
        errorScreen: {
          component: HttpErrorComponent,
          forWhichErrors: [401, 403, 404, 500],
          hideCloseIcon: true,
        },
      },
    }),
    // Other import statements
  ],
  providers: [
    APP_ROUTE_PROVIDER,
    APP_VALIDATION_PROVIDER
  ],
  bootstrap: [AppComponent],
})
export class AppModule {}

Hi, ABP team:

Could you kindly send me the latest source code of @volo/abp.commercial.ng.ui package?

Hello Anjali_Musmade, Thanks a lot, this works.

Thank you Linch, what problem you been solving by rewriting angular domain resolver? As i understand it supposed to work out of the box without rewriting it?

Hi, kirotech, I just updated the code of multi-tenancy-utils.ts , because it is work for datdv1 https://support.abp.io/QA/Questions/5650#answer-3a0d82ab-0969-e910-4824-cab59dc00e91

Maybe you can try this to solve your problem. Hope it is work for you.

import {
  ABP,
  CORE_OPTIONS,
  Environment,
  EnvironmentService,
  createTokenParser,
  getRemoteEnv,
} from '@abp/ng.core';
import { Injector } from '@angular/core';
import clone from 'just-clone';

const tenancyPlaceholder = '{0}';

function getCurrentTenancyName(appBaseUrl: string): string {
  if (appBaseUrl.charAt(appBaseUrl.length - 1) !== '/') appBaseUrl += '/';
  const parseTokens = createTokenParser(appBaseUrl);
  const token = tenancyPlaceholder.replace(/[}{]/g, '');
  return parseTokens(window.location.href)[token]?.[0];
}

export function cleanPlaceholderFromHostUrl(injector: Injector) {
  const fn = async () => {
    const environmentService = injector.get(EnvironmentService);
    const options = injector.get(CORE_OPTIONS) as ABP.Root;
    environmentService.setState(options.environment as Environment);
    await getRemoteEnv(injector, options.environment);
    const baseUrl =
      environmentService.getEnvironment()['appBaseUrl'] ||
      environmentService.getEnvironment().application?.baseUrl ||
      '';
    const tenancyName = getCurrentTenancyName(baseUrl);

    if (!tenancyName) {
      /**
       * If there is no tenant, we still have to clean up {0}. from baseUrl to avoid incorrect http requests.
       */
      replaceTenantNameWithinEnvironment(injector, '', tenancyPlaceholder + '.');
      replaceTenantNameWithinEnvironment(injector, '', tenancyPlaceholder);
    }

    return Promise.resolve();
  };

  return fn;
}

function replaceTenantNameWithinEnvironment(
  injector: Injector,
  tenancyName: string,
  placeholder = tenancyPlaceholder
) {
  const environmentService = injector.get(EnvironmentService);

  const environment = clone(environmentService.getEnvironment()) as Environment;

  if (environment.application.baseUrl) {
    environment.application.baseUrl = environment.application.baseUrl.replace(
      placeholder,
      tenancyName
    );
  }

  if (environment.oAuthConfig?.redirectUri) {
    environment.oAuthConfig.redirectUri = environment.oAuthConfig.redirectUri.replace(
      placeholder,
      tenancyName
    );
  }

  if (!environment.oAuthConfig) {
    environment.oAuthConfig = {};
  }
  environment.oAuthConfig.issuer = (environment.oAuthConfig.issuer || '').replace(
    placeholder,
    tenancyName
  );

  environment.oAuthConfig.clientId = (environment.oAuthConfig.clientId || '').replace(
    placeholder,
    tenancyName
  );

  Object.keys(environment.apis).forEach(api => {
    Object.keys(environment.apis[api]).forEach(key => {
      environment.apis[api][key] = (environment.apis[api][key] || '').replace(
        placeholder,
        tenancyName
      );
    });
  });

  return environmentService.setState(environment);
}

Zobrazených 21 až 27 z 27 záznamov
Made with ❤️ on ABP v8.2.0-preview Updated on marca 25, 2024, 15:11