I am running an Angular Client (v14) with a .Net 6 WebAPI. These are in separate .Net projects running on a Raspberry Pi. It is a standalone kiosk, so the front and backend run on the same box.
I want to be able to access the front end from a PC on the same network, via the browser. When I remote to the address of the Raspberry Pi, I can see the loading screen of the Angular App, but it can’t resolve localhost, as it looks to the PC’s localhost for the backend, not the Kiosk.
I also want to be able to access the API remotely to control the functions of the unit, this might be via Postman or a third party application. But this maybe a separate issue, which I can solve later.
I need the angular application to rewrite the localhost to the current IP address. I’ve struggled to find example of how this is done and things have got quite convoluted along the way. Below are sections of the configuration, I wonder if someone can point me in the right direction or point me to an example of how I can make this work?
launchSettings.json
{
"profiles": {
"Kiosk_ClientApp": {
"commandName": "Project",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "https://0.0.0.0:4200"
}
}
}
nginx
server {
listen 443 ssl;
listen [::]:443 ssl;
include snippets/self-signed.conf;
include snippets/ssl-params.conf;
server_name kiosk;
location / {
proxy_pass https://localhost:4200;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection keep-alive;
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
proxy_cookie_path / "/; SameSite = None' secure";
proxy_read_timeout 18000s;
proxy_send_timeout 18000s;
}
location /api {
proxy_pass https://localhost:4901;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
}
proxy.conf.json
{
"/api": {
"target": "https://0.0.0.0:4901",
"secure": true,
"changeOrigin": true,
"logLevel": "debug"
}
}
environment.ts
export const environment = {
production: false,
urlAddress: "https://localhost:4901"
};
environment.prod.ts
export const environment = {
production: true,
urlAddress: "https://localhost:4901"
};
angular.json
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "projects",
"projects": {
"Kiosk": {
"root": "",
"sourceRoot": "src",
"projectType": "application",
"prefix": "app",
"schematics": {},
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"progress": false,
"outputPath": "dist",
"index": "src/index.html",
"main": "src/main.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "src/tsconfig.app.json",
"assets": [
"src/assets"
],
"styles": [
"node_modules/bootstrap/dist/css/bootstrap.min.css",
"src/styles.css",
"node_modules/sweetalert2/src/sweetalert2.scss",
"node_modules/datatables.net-bs4/css/dataTables.bootstrap4.min.css",
"node_modules/datatables.net-dt/css/jquery.dataTables.css"
],
"scripts": [
"./node_modules/jquery/dist/jquery.min.js",
"./node_modules/popper.js/dist/umd/popper.min.js",
"./node_modules/bootstrap/dist/js/bootstrap.min.js",
"./node_modules/chart.js/dist/Chart.js",
"node_modules/jquery/dist/jquery.js",
"node_modules/datatables.net/js/jquery.dataTables.js",
"node_modules/datatables.net-bs4/js/dataTables.bootstrap4.min.js"
],
"vendorChunk": true,
"extractLicenses": false,
"buildOptimizer": false,
"sourceMap": true,
"optimization": false,
"namedChunks": true
},
"configurations": {
"production": {
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
}
],
"optimization": true,
"outputHashing": "all",
"sourceMap": false,
"namedChunks": false,
"extractLicenses": true,
"vendorChunk": false,
"buildOptimizer": true,
"budgets": [
{
"type": "anyComponentStyle",
"maximumWarning": "6kb"
}
]
}
},
"defaultConfiguration": ""
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"options": {
"browserTarget": "Kiosk:build",
"proxyConfig": "src/proxy.conf.json",
"ssl": true,
"sslCert": "ssl/server.crt",
"sslKey": "ssl/server.key"
},
"configurations": {
"production": {
"browserTarget": "Kiosk:build:production"
}
}
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"browserTarget": "Kiosk:build"
}
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"main": "src/test.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "src/tsconfig.spec.json",
"karmaConfig": "src/karma.conf.js",
"styles": [
"src/styles.css"
],
"scripts": [],
"assets": [
"src/assets"
]
}
},
"server": {
"builder": "@angular-devkit/build-angular:server",
"options": {
"outputPath": "dist-server",
"main": "src/main.ts",
"tsConfig": "src/tsconfig.server.json",
"sourceMap": true,
"optimization": false
},
"configurations": {
"dev": {
"optimization": true,
"outputHashing": "all",
"sourceMap": false,
"namedChunks": false,
"extractLicenses": true
},
"production": {
"optimization": true,
"outputHashing": "all",
"sourceMap": false,
"namedChunks": false,
"extractLicenses": true
}
},
"defaultConfiguration": ""
}
}
},
"Kiosk-e2e": {
"root": "e2e/",
"projectType": "application",
"architect": {
"e2e": {
"builder": "@angular-devkit/build-angular:protractor",
"options": {
"protractorConfig": "e2e/protractor.conf.js",
"devServerTarget": "Kiosk:serve"
}
}
}
}
},
"cli": {
"analytics": false
}
}
Example Post:
saveRecipe(recipe: IRecipe): Observable<number> {
this.convertRecipeToMetric(recipe);
return this.http
.post<number>(this.createCompleteRoute("api/Recipe/AddRecipe", this.envUrl.urlAddress),
recipe,
{ withCredentials: true }).pipe(map((s: number) => { return s; })
);
}
private createCompleteRoute = (route: string, envAddress: string) => {
return `${envAddress}/${route}`;
};
package.json – script section:
"scripts": {
"ng": "ng",
"start": "ng serve --proxy-config proxy.config.json",
"build": "ng build",
"build:ssr": "ng run Kiosk:server:dev",
"test": "ng test",
"lint": "ng lint",
"e2e": "ng e2e"
},
C# Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddCors(options =>
{
options.AddPolicy(
"AllowMyOrigins",
builder =>
builder
.AllowCredentials()
.WithOrigins("https://localhost:4200")
.SetIsOriginAllowed(host => true)
.SetIsOriginAllowedToAllowWildcardSubdomains()
.AllowAnyHeader()
.AllowAnyMethod());
});
services.AddControllersWithViews();
// In production, the Angular files will be served from this directory
services.AddSpaStaticFiles(configuration => { configuration.RootPath = "ClientApp/dist"; });
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseForwardedHeaders(new ForwardedHeadersOptions
{
ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto,
});
app.UseHttpsRedirection();
app.UseStaticFiles();
if (!env.IsDevelopment())
{
app.UseSpaStaticFiles();
}
app.UseRouting();
app.UseCors("AllowMyOrigins");
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
"default",
"{controller}/{action=Index}/{id?}");
});
app.UseSpa(spa =>
{
// To learn more about options for serving an Angular SPA from ASP.NET Core,
// see https://go.microsoft.com/fwlink/?linkid=864501
spa.Options.SourcePath = "ClientApp";
if (env.IsDevelopment())
{
spa.UseAngularCliServer("start");
}
});
}
The IP address could be anything as there would be many of these units, placed anywhere, so I need the IP address dynamic, not static.
3
Answers
If you’re using nginx as an ingress proxy then you shouldn’t use absolute paths in your front-end code.
If you use relative paths to call the API (i.e. "/api/something") from javascript then the browser will hit whatever server you’re using to load the FE (which presumably is a server URL that hits the nginx proxy?)
I think your problem analysis is quite on point with one exception. Your Front end fails to know which IP address to send its request towards and this needs to be corrected by your .NET back-end. Not as you mention your angular application:
I need the angular application to rewrite the localhost to the current IP address
Since your front-end is served by a back-end you will need your back-end to first figure out what IP it is available on. Which can be done as shown in the example below (credits for this go to: https://stackoverflow.com/a/27376368/2761355)
Since your ip address only rarely changes you can think about only retrieving it on startup since this solution does depend on an outside source (being Google DNS server 8.8.8.8).
All that remains is for your .NET backend to set this IP address in your served angular file when it is requested.
The issue appears to be that the Angular client is unable to make requests to an API hosted on a separate C# backend that is running on localhost. This is likely due to CORS restrictions, which prevent web pages from making requests to a different domain than the one that served the page.
To fix this issue, you will need to configure the C# backend to allow cross-origin requests from the Angular client’s domain. One way to do this is by adding CORS middleware to the C# backend. Here’s an example of how to do this using the Microsoft.AspNetCore.Cors package:
Install the Microsoft.AspNetCore.Cors package using NuGet.
In your C# backend’s Startup.cs file, add the following code to the ConfigureServices method to configure CORS:
This will allow any origin to make requests to your API and allow any HTTP method and headers.
In the Configure method of Startup.cs, add the following code to enable CORS:
This will enable CORS for all endpoints in your application.
With these changes, your C# backend should now allow requests from the Angular client’s domain. Note that you should be cautious about allowing any origin to make requests to your API, as this can pose a security risk. It’s generally a good idea to limit the origins that are allowed to make requests to your API.