Base solution for your next web application

Activities of "cyklussoftware"

@alper @rbreton This probably isn't related, but there was an issue that I came across the other day while generating an Entity called EventCategory.

This was the problematic code before:

modelBuilder.Entity<EventCategory>(e =>
{
    e.HasIndex(e => new { e.TenantId });
});

And after I changed the name of my entity:

modelBuilder.Entity<SchoolEventCategory>(s =>
{
    s.HasIndex(e => new { e.TenantId });
});

The tool picks what to call the first variable based on the first letter of the entity name. So if anything starts with E, there will be a compile error.

So, like I said, it's unrelated but a problem.

@rbreton Did you try building your project after generating all of the code? The specific error would likely show up then.

I am using version ASP.NET Core + Angular v6.5.0.

I basically copied the image uploading logic from the Profile Picture file upload component and I am having an issue when my custom modal is closing. My code is attached below.

When my close() function runs the uploader.clearQueue(); line, an exception that says TypeError: Cannot read property 'isUploading' of undefined gets thrown. This is part of the file-uploader.class.js file. I think the function that is throwing the exception looks like this:

FileUploader.prototype.removeFromQueue = function (value) {
        var index = this.getIndexOfItem(value);
        var item = this.queue[index];
        if (item.isUploading) {
            item.cancel();
        }
        this.queue.splice(index, 1);
        this.progress = this._getTotalProgress();
};

What's odd is that when I debug this in VS Code, the item variable is defined and isUploading is also defined.

My image upload works and I can see the images reflected on my server, but after running the this.uploader.uploadAll(); function, the queue is not empty. If I check the queue after uploading in the Profile Picture section, the queue is empty. I'm not sure what could be going wrong given that my code is practically a copy. Maybe I'm missing an import somewhere that doesn't throw an error when it's missing?

My component code is below.

import { IAjaxResponse } from '@abp/abpHttpInterceptor';
import { TokenService } from '@abp/auth/token.service';
import { Component, Injector, ViewChild } from '@angular/core';
import { AppConsts } from '@shared/AppConsts';
import { AppComponentBase } from '@shared/common/app-component-base';
import { FileUploader, FileUploaderOptions, FileItem } from 'ng2-file-upload';
import { ModalDirective } from 'ngx-bootstrap';
import { finalize } from 'rxjs/operators';

@Component({
    selector: 'changeRewardImageModal',
    templateUrl: './change-reward-image-modal.component.html'
})
export class ChangeRewardImageModalComponent extends AppComponentBase {

    @ViewChild('changeRewardImageModal') modal: ModalDirective;

    public active = false;
    public uploader: FileUploader;
    public temporaryPictureUrl: string;
    public saving = false;

    private maxProfilPictureBytesUserFriendlyValue = 5;
    private temporaryPictureFileName: string;
    private _uploaderOptions: FileUploaderOptions = {};

    imageChangedEvent: any = '';

    constructor(
        injector: Injector,
        private _tokenService: TokenService
    ) {
        super(injector);
    }

    initializeModal(): void {
        this.active = true;
        this.temporaryPictureUrl = '';
        this.temporaryPictureFileName = '';
        this.initFileUploader();
    }

    show(): void {
        this.initializeModal();
        this.modal.show();
    }

    close(): void {
        console.log("close");
        this.active = false;
        this.imageChangedEvent = '';

        // Clearing the queue gets rid of the files that need to be uploaded.
        // Idk if we actually need to do this if the modal is closing.
        //console.log(this.uploader.queue.length);
        //if(this.uploader.queue.length > 0){
        try {
            this.uploader.clearQueue(); // todo this was causing an exception to be thrown
        } catch (exception){
            console.log(exception);
        }
        //}

        this.modal.hide();
    }

    fileChangeEvent(event: any): void {
        this.imageChangedEvent = event;
    }

    imageCroppedFile(file: File) {
        console.log("cropped file");
        let files: File[] = [file];
        this.uploader.clearQueue();
        this.uploader.addToQueue(files);
    }

    initFileUploader(): void {
        this.uploader = new FileUploader({ url: AppConsts.remoteServiceBaseUrl + '/Reward/UploadRewardImage' });
        this._uploaderOptions.autoUpload = false;
        this._uploaderOptions.authToken = 'Bearer ' + this._tokenService.getToken();
        this._uploaderOptions.removeAfterUpload = true;
        this.uploader.onAfterAddingFile = (file) => {
            file.withCredentials = false;
        };

        this.uploader.onBuildItemForm = (fileItem: FileItem, form: any) => {
            form.append('FileType', fileItem.file.type);
            form.append('FileName', 'RewardImage');
            form.append('FileToken', this.guid());
        };

        this.uploader.onSuccessItem = (item, response, status) => {
            const resp = <IAjaxResponse>JSON.parse(response);
            if (resp.success) {
                this.saving = false;
                abp.event.trigger('rewardImageUploaded', {
                    fileToken: resp.result.fileToken,
                    x: 0,
                    y: 0,
                    width: 0,
                    height: 0
                });
                this.close();
            } else {
                this.message.error(resp.error.message);
            }
        };

        this.uploader.setOptions(this._uploaderOptions);
    }

    guid(): string {
        function s4() {
            return Math.floor((1 + Math.random()) * 0x10000)
                .toString(16)
                .substring(1);
        }
        return s4() + s4() + '-' + s4() + '-' + s4() + '-' + s4() + '-' + s4() + s4() + s4();
    }

    save(): void {
        if(this.uploader.queue.length > 0){
            this.saving = true;
            this.uploader.uploadAll();
        }
    }
}

I put together a little "hack" to get this to work.

This is my new "getApplicationConfig" method from the AppPreBootstrap.ts file in my Angular project:

private static getApplicationConfig(appRootUrl: string, callback: () => void) {
let type = 'GET';
let url = appRootUrl + 'assets/' + environment.appConfig;
let customHeaders = [
{
name: 'Abp.TenantId',
value: abp.multiTenancy.getTenantIdCookie() + ''
}];


XmlHttpRequestHelper.ajax(type, url, customHeaders, null, (result) => {
    const subdomainTenancyNameFinder = new SubdomainTenancyNameFinder();
    const tenancyName = subdomainTenancyNameFinder.getCurrentTenancyNameOrNull(result.appBaseUrl);

    // CUSTOM ADDITION HERE
    let getTenantIdURL = result.remoteServiceBaseUrl + '/api/services/app/Account/IsTenantAvailable';
    let getTenantIdData =
    {
        tenancyName: tenancyName
    };
    XmlHttpRequestHelper.ajax('POST', getTenantIdURL, null, JSON.stringify(getTenantIdData), (tenantResult) => {
        abp.multiTenancy.setTenantIdCookie(tenantResult.result.tenantId);

        // pulled from surrounding ajax method
        AppConsts.appBaseUrlFormat = result.appBaseUrl;
        AppConsts.remoteServiceBaseUrlFormat = result.remoteServiceBaseUrl;
        AppConsts.localeMappings = result.localeMappings;

        if (tenancyName == null) {
            AppConsts.appBaseUrl = result.appBaseUrl.replace(AppConsts.tenancyNamePlaceHolderInUrl + '.', '');
            AppConsts.remoteServiceBaseUrl = result.remoteServiceBaseUrl.replace(AppConsts.tenancyNamePlaceHolderInUrl + '.', '');
        } else {
            AppConsts.appBaseUrl = result.appBaseUrl.replace(AppConsts.tenancyNamePlaceHolderInUrl, tenancyName);
            AppConsts.remoteServiceBaseUrl = result.remoteServiceBaseUrl.replace(AppConsts.tenancyNamePlaceHolderInUrl, tenancyName);
        }
        callback();
    });
    // END CUSTOM ADDITION
});

Now my appconfig.json in my Angular project includes this:

"remoteServiceBaseUrl": "https://api.websitename.io",
"appBaseUrl": "https://{TENANCY_NAME}.websitename.io",

and my appsettings.json file for the Host project includes this:

"ServerRootAddress": "https://api.websitename.io",
"ClientRootAddress": "https://dev.websitename.io",
"CorsOrigins": "https://*.websitename.io"

With this implementation, I can navigate my browser to default.websitename.io and I am logged into the default tenant. I can go to host.websitename.io or any other URL that contains an invalid tenant name and I am logged in the Host.

It's probably not the best way to do things, but it seems to work just fine.

@ryancyq I actually found that post a couple of days ago. I was having that problem and fixed it before I tried switching to the {TENANCY_NAME} based tenant resolution.

Anyways, my proxy and the whole system works just fine if I don't get the TENANCY_NAME from the URL and manually specify it in the Login page instead.

@ismcagdas

You are correct. The Host project and Angular project are completely separated apps. I'm using Nginx instead of IIS, but it's the same concept. If I navigate my browser to api.websitename.com, I see the Swagger UI.

Everything works fine between Angular and the Host in this configuration if I manually set the tenant name on the Login page of the Angular app. This is because all of the API requests get an HTTP header of Abp.TenantId added to them.

If I try to resolve the tenant name from the URL, the Angular app is aware that my tenant name is Default, but because the URL to the Host is just api.websitename.com, the Host doesn't know what tenant I am. I am logged into the Host rather than the Default Tenant.

Basically, using {TENANCY_NAME}.websitename.com for Angular and api.websitename.com for the Host project logs me into the Host rather than the Default tenant because Angular isn't sending the Abp.TenantId header and the Host project isn't expecting the tenant name to be in the URL.

Hopefully that makes sense.

I guess my questions is this: How do I use a common API URL (rather than a {TENANCY_NAME} based URL) for the Host project and a {TENANCY_NAME} based URL for the Angular project?

@emadah Did you run the NSwag utility using nswag/refresh.bat in the Angular project? I ran into a problem with Angular earlier this week where my service files were not generated properly, so when I ran Angular for the first time it wouldn't compile.

On second glance this might not be the problem, but it's an important step anyways.

Question

I am running ASP.NET Core + Angular v6.3.1 behind an Nginx proxy.

Similar to this question, https://support.aspnetzero.com/QA/Questions/5665, I want to use {TENANCY_URL}.websitename.com to resolve tenants on the Angular site, but I want to use api.websitename.com to access my API. When I use default.websitename.com and log in, I end up logging into the Host project. Based on your description from the post above, this is intended behavior. Please correct me if I'm wrong.

I added some debugging lines and default is being pulled correctly from the URL. Where is the best place to use the _accountService.isTenantAvailable function to resolve the Tenant ID based on the name?

Please note the following:

In the Angular appconfig.json files I have set the appBaseUrl to https://{TENANCY_NAME}.websitename.com and the removeServiceBaseUrl to https://api.websitename.com.

In the Host appsettings.json files I have set the https://api.websitename.com to be https://api.websitename.com, the ClientRootAddress to https://{TENANCY_NAME}.websitename.com and the CorsOrigins to https://*.websitename.com.

Like I said, I can log into the Host just fine with no errors.

I got it working. I guess I overlooked the AppConfigurations.Get() method in the MigratorModule the first time I tried this. Now I just pass the name of my environment to that method and it works great.

Thanks!

I am running ASP.NET Core + Angular v6.3.1. The Angular and Host projects are running in Docker containers behind an Nginx proxy.

I can log in as the Host just fine with no errors in the Google Chrome console. When I try to change to the Default tenant, I can click "Save", the dialog disappears, and the page refreshes, but the tenant doesn't change. I am still the Host. There are no errors in the Google Chrome console when I try to change tenants.

I get a message saying "There is no tenant defined with name asdf" if I try to log in with an invalid tenant called "asdf".

I also tried switching the project to resolve tenants based on the URL and this always resulted in logging in as the Host as well.

I am able to change tenants when I run the same Docker images in Docker on my local machine WITHOUT an Nginx Proxy, so I believe it is some sort of Nginx Proxy configuration problem.

Here is my site configuration for Angular:

location / {
    proxy_pass http://192.168.1.224:4200;
}

Here is my site configuration for the Host:

location / {
    proxy_pass http://192.168.1.224:9901;
}

location /signalr {
    proxy_pass http://192.168.1.224:9901;

    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "Upgrade";
}

I guess I'd like to know if you know what I am missing.

Thanks

The Web.Host project has the ability to use multiple appsettings.json files based on the ASPNETCORE_ENVIRONMENT environment variable. Would it be possible to have multiple appsettings.json files for the Migrator application like the Web.Host project has? I tried looking into this sort of functionality through separate files and separate entries in the single appsettings.json file, but the Migrator application ONLY uses the Default connection from the appsettings.json file.

If you could point me in the correct direction, that would be great.

Showing 11 to 20 of 42 entries