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.
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?)