Skip to content
Open
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
3319e9b
Add configurable controller frontend version
bjorntore May 19, 2026
a89b2a4
Rename frontend dev server flag
bjorntore May 20, 2026
8af13cf
Merge branch 'main' into feat/controller-frontend-version-appsetting
bjorntore May 20, 2026
d75d9d1
fix: use localtest service owner org number
bjorntore May 20, 2026
28ea932
Removing lots of versioning stuff we no longer need when v9 versions …
May 20, 2026
19722fc
Removing assertions on cookie that is no longer in use in v9
May 20, 2026
575e42c
Hiding the frontend version switcher when app is v9+, as it's no long…
May 20, 2026
8d5e57e
Showing an error page when app is not started with --dev-frontend
May 20, 2026
d5cd5db
Removing frontendUrl config from cypress (no longer in use)
May 20, 2026
73c7fa9
Adding --dev-frontend to `studioctl build` as well, and using those w…
May 20, 2026
ac3e965
Updating test now that outdated frontend assets are not loaded by def…
May 20, 2026
450a218
Merge branch 'refs/heads/main' into feat/controller-frontend-version-…
May 20, 2026
8727a45
Updating changed paths
May 20, 2026
ca272e0
gofmt
May 20, 2026
989f6b8
Fixing comments from codeql
May 20, 2026
fbfb8de
Fixing formatting and linting issues
May 20, 2026
9e5df5e
Revert "Adding --dev-frontend to `studioctl build` as well, and using…
May 21, 2026
f4dea78
Simplifying app-run-local-env, as overriding the port is not possible…
May 21, 2026
50c58e9
Running gofmt again
May 21, 2026
2fcca5d
Merge branch 'main' into feat/controller-frontend-version-appsetting
May 21, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/actions/app-run-local-env/action.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -103,11 +103,13 @@ runs:
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
env:
APP_RUN_TARGETS_JSON: ${{ inputs.app-run-targets-json }}
RUN_FRONTEND: ${{ inputs.run-frontend }}
STUDIOCTL_REGISTRY_CACHE_WRITE: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }}
with:
script: |
const workspace = process.env.GITHUB_WORKSPACE;
const maxParallelStarts = 4;
const runFrontend = process.env.RUN_FRONTEND === 'true';

const targetsJson = process.env.APP_RUN_TARGETS_JSON;
let targets;
Expand All @@ -122,6 +124,9 @@ runs:

function targetRunArgs(path, image) {
const args = ['run', '--mode', 'container', '--detach', '--random-host-port', '--path', path];
if (runFrontend) {
args.push('--dev-frontend');
}
if (image) {
args.push('--image-tag', image, '--pull', '--skip-build');
}
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/app-frontend-cypress.yml
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ jobs:
while (nextTarget < targets.length) {
const target = targets[nextTarget++];
core.info(`Worker ${workerId}: building ${target.path}`);
await exec.exec('studioctl', ['app', 'build', '--path', target.path, '--image-tag', target.image, '--push']);
await exec.exec('studioctl', ['app', 'build', '--path', target.path, '--image-tag', target.image, '--push', '--dev-frontend']);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ data:
PlatformSettings__ApiWorkflowEngineEndpoint: http://workflow-engine-app.runtime-workflow-engine-app.svc.cluster.local/api/v1/
GeneralSettings__ExternalAppBaseUrl: https://{org}.apps.{hostName}/{org}/{app}/
PlatformFrontendSettings__PostalCodesUrl: https://altinncdn.no/postcodes/registry.json
PlatformFrontendSettings__AppFrontendCdnBaseUrl: https://altinncdn.no/toolkits/altinn-app-frontend
PlatformFrontendSettings__AltinnLogoUrl: https://altinncdn.no/img/Altinn-logo-blue.svg
PlatformFrontendSettings__HelpCircleIllustrationUrl: https://altinncdn.no/img/illustration-help-circle.svg
---
Expand Down
1 change: 0 additions & 1 deletion infra/runtime/apps-config/base/apps-runtime-common.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ data:
},
"PlatformFrontendSettings": {
"PostalCodesUrl": "https://altinncdn.no/postcodes/registry.json",
"AppFrontendCdnBaseUrl": "https://altinncdn.no/toolkits/altinn-app-frontend",
"AltinnLogoUrl": "https://altinncdn.no/img/Altinn-logo-blue.svg",
"HelpCircleIllustrationUrl": "https://altinncdn.no/img/illustration-help-circle.svg"
}
Expand Down
11 changes: 5 additions & 6 deletions src/App/backend/src/Altinn.App.Api/Controllers/HomeController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -118,18 +118,17 @@ public async Task<IActionResult> Index(
return PartialView("Index");
}

string? frontendVersionOverride = null;
if (_env.IsDevelopment() && HttpContext.Request.Cookies.TryGetValue("frontendVersion", out var cookie))
{
frontendVersionOverride = cookie.TrimEnd('/');
}
string? appFrontendAssetBaseUrlOverride =
_env.IsDevelopment() && !string.IsNullOrWhiteSpace(_appSettings.AppFrontendAssetBaseUrl)
? _appSettings.AppFrontendAssetBaseUrl.Trim().TrimEnd('/')
: null;

var appGlobalState = await _bootstrapGlobalService.GetGlobalState(_appId.Org, _appId.App, returnUrl, lang);
var html = await _indexPageGenerator.Generate(
_appId.Org,
_appId.App,
appGlobalState,
frontendVersionOverride
appFrontendAssetBaseUrlOverride
);
return Content(html, "text/html; charset=utf-8");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,15 @@ public class AppSettings
public string DefaultBootstrapUrl { get; set; } =
"https://stackpath.bootstrapcdn.com/bootstrap/4.1.0/css/bootstrap.min.css";

/// <summary>
/// Gets or sets the frontend asset URL used by the generated controller index page.
/// </summary>
/// <remarks>
/// This setting is only honored when the host runs in the Development environment. PDF rendering runs in a
/// container, so avoid loopback URLs such as <c>localhost</c> because they resolve inside the container.
/// </remarks>
public string? AppFrontendAssetBaseUrl { get; set; }

/// <summary>
/// Open Id Connect Well known endpoint
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,6 @@ internal class PlatformFrontendSettings
/// </summary>
public Uri PostalCodesUrl { get; set; } = new("https://altinncdn.no/postcodes/registry.json");

/// <summary>
/// Base URL for the app frontend CDN.
/// </summary>
public Uri AppFrontendCdnBaseUrl { get; set; } = new("https://altinncdn.no/toolkits/altinn-app-frontend");

/// <summary>
/// URL for the Altinn logo SVG.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@
using Altinn.App.Core.Internal.Auth;
using Altinn.App.Core.Internal.Pdf;
using Altinn.App.Core.Models.Pdf;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using OpenTelemetry.Context.Propagation;
Expand All @@ -32,8 +30,6 @@ public class PdfGeneratorClient : IPdfGeneratorClient
private readonly PdfGeneratorSettings _pdfGeneratorSettings;
private readonly PlatformSettings _platformSettings;
private readonly IUserTokenProvider _userTokenProvider;
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly IHostEnvironment _hostEnvironment;
private readonly Telemetry? _telemetry;

/// <summary>
Expand All @@ -46,17 +42,13 @@ public class PdfGeneratorClient : IPdfGeneratorClient
/// </param>
/// <param name="platformSettings">Links to platform services</param>
/// <param name="userTokenProvider">A service able to identify the JWT for currently authenticated user.</param>
/// <param name="httpContextAccessor">http context</param>
/// <param name="hostEnvironment">The host environment.</param>
/// <param name="telemetry">Telemetry service</param>
public PdfGeneratorClient(
ILogger<PdfGeneratorClient> logger,
HttpClient httpClient,
IOptions<PdfGeneratorSettings> pdfGeneratorSettings,
IOptions<PlatformSettings> platformSettings,
IUserTokenProvider userTokenProvider,
IHttpContextAccessor httpContextAccessor,
IHostEnvironment hostEnvironment,
Telemetry? telemetry = null
)
{
Expand All @@ -65,8 +57,6 @@ public PdfGeneratorClient(
_userTokenProvider = userTokenProvider;
_pdfGeneratorSettings = pdfGeneratorSettings.Value;
_platformSettings = platformSettings.Value;
_httpContextAccessor = httpContextAccessor;
_hostEnvironment = hostEnvironment;
_telemetry = telemetry;
}

Expand Down Expand Up @@ -125,25 +115,6 @@ public async Task<Stream> GeneratePdf(Uri uri, string? footerContent, Cancellati
new PdfGeneratorCookieOptions { Value = _userTokenProvider.GetUserToken(), Domain = uri.Host }
);

if (
_hostEnvironment.IsDevelopment()
&& _httpContextAccessor.HttpContext?.Request.Cookies.TryGetValue("frontendVersion", out var frontendVersion)
== true
&& !string.IsNullOrEmpty(frontendVersion)
)
{
frontendVersion = frontendVersion.Replace("localhost", "host.containers.internal");
generatorRequest.Cookies.Insert(
0,
new PdfGeneratorCookieOptions
{
Name = "frontendVersion",
Domain = uri.Host,
Value = frontendVersion,
}
);
}

string requestContent = JsonSerializer.Serialize(generatorRequest, _jsonSerializerOptions);
using StringContent stringContent = new(requestContent, Encoding.UTF8, "application/json");
var httpResponseMessage = await _httpClient.PostAsync(_platformSettings.ApiPdf2Endpoint, stringContent, ct);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,12 @@ internal interface IIndexPageGenerator
/// <param name="org">The organization identifier.</param>
/// <param name="app">The application identifier.</param>
/// <param name="appGlobalState">The bootstrap global state for the app.</param>
/// <param name="frontendVersionOverride">Optional frontend version URL override (only used in development).</param>
/// <param name="appFrontendAssetBaseUrlOverride">Optional app frontend asset base URL override (only used in development).</param>
/// <returns>The generated HTML content.</returns>
Task<string> Generate(
string org,
string app,
BootstrapGlobalResponse appGlobalState,
string? frontendVersionOverride = null
string? appFrontendAssetBaseUrlOverride = null
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,35 @@ public async Task<string> Generate(
string org,
string app,
BootstrapGlobalResponse appGlobalState,
string? frontendVersionOverride = null
string? appFrontendAssetBaseUrl = null
)
{
var frontendUrl = frontendVersionOverride ?? "https://altinncdn.no/toolkits/altinn-app-frontend/4";
if (appFrontendAssetBaseUrl is null)
{
var htmlContentError = $$"""
<!DOCTYPE html>
<html lang="no">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>{{org}} - {{app}}</title>
<link rel="icon" href="https://altinncdn.no/favicon.ico">
</head>
<body>
<h1>Not implemented yet</h1>
<p>Sorry, loading our built-in frontend is not yet supported. Please build and host frontend from the monorepo code yourself.</p>
<p>To serve a production-level/faster build with no hot-reloads:</p>
<pre>cd src/App/frontend; yarn build; yarn serve 8080</pre>
<p>To serve a slightly slower build with hot-reloads tailored for development:</p>
<pre>cd src/App/frontend; yarn start</pre>
<p>Then make sure to restart this app with:</p>
<pre>studioctl run --dev-frontend</pre>
</body>
</html>
""";
return htmlContentError;
}

var featureToggles = await _frontendFeatures.GetFrontendFeatures();
var featureTogglesJson = JsonSerializer.Serialize(featureToggles, _jsonSerializerOptions);
Expand Down Expand Up @@ -71,7 +96,7 @@ public async Task<string> Generate(
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>{{org}} - {{app}}</title>
<link rel="icon" href="https://altinncdn.no/favicon.ico">
<link rel="stylesheet" type="text/css" href="{{frontendUrl}}/altinn-app-frontend.css">
<link rel="stylesheet" type="text/css" href="{{appFrontendAssetBaseUrl}}/altinn-app-frontend.css">
{{externalStylesheets}}{{customCssLinks}}</head>
<body>
<div id="root"></div>
Expand All @@ -81,7 +106,7 @@ public async Task<string> Generate(
window.featureToggles = {{featureTogglesJson}};
window.altinnAppGlobalData = {{globalDataJson}};
</script>
<script src="{{frontendUrl}}/altinn-app-frontend.js" crossorigin></script>
<script src="{{appFrontendAssetBaseUrl}}/altinn-app-frontend.js" crossorigin></script>
{{externalScripts}}{{customJsScripts}}</body>
</html>
""";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ CancellationToken cancellationToken
ApplicationMetadata appMetadata = await _appMetadata.GetApplicationMetadata();
string formattedScopes = MaskinportenClient.GetFormattedScopes(request.Scopes);
string url =
$"{_localtestBaseUrl}/Home/GetTestOrgToken?org={appMetadata.Org}&orgNumber=991825827&authenticationLevel=3&scopes={Uri.EscapeDataString(formattedScopes)}";
$"{_localtestBaseUrl}/Home/GetTestOrgToken?org={appMetadata.Org}&orgNumber=405003309&authenticationLevel=3&scopes={Uri.EscapeDataString(formattedScopes)}";

using var client = _httpClientFactory.CreateClient();
var response = await client.GetAsync(url, cancellationToken);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
using System.Net;
using Altinn.App.Core.Models;
using Altinn.Platform.Storage.Interface.Models;
using App.IntegrationTests.Mocks.Services;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Hosting;
using Moq;
using Xunit.Abstractions;

namespace Altinn.App.Api.Tests.Controllers;

public class HomeControllerTest_AppFrontendAssetBaseUrl : ApiTestBase, IClassFixture<WebApplicationFactory<Program>>
{
private const string Org = "tdd";
private const string App = "contributer-restriction";

public HomeControllerTest_AppFrontendAssetBaseUrl(
WebApplicationFactory<Program> factory,
ITestOutputHelper outputHelper
)
: base(factory, outputHelper)
{
OverrideEnvironment = Environments.Development;
SendAsync = _ =>
Task.FromResult(
new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent("""{"orgs":{}}""") }
);
}

[Fact]
public async Task Index_UsesAppFrontendAssetBaseUrlAppSetting()
{
OverrideAppSetting("AppSettings:AppFrontendAssetBaseUrl", "/configured/frontend/");

using var client = GetRootedClient(Org, App, configureServices: ConfigureStatelessAnonymousApp);
using var response = await client.GetAsync($"{Org}/{App}/");
var html = await response.Content.ReadAsStringAsync();

Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Contains("href=\"/configured/frontend/altinn-app-frontend.css\"", html);
Assert.Contains("src=\"/configured/frontend/altinn-app-frontend.js\"", html);
}

[Fact]
public async Task Index_FailsByDefault()
{
using var client = GetRootedClient(Org, App, configureServices: ConfigureStatelessAnonymousApp);
using var response = await client.GetAsync($"{Org}/{App}/");
var html = await response.Content.ReadAsStringAsync();

Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Contains("loading our built-in frontend is not yet supported", html);
}

private static void ConfigureStatelessAnonymousApp(IServiceCollection services)
{
var webHostEnvironmentMock = new Mock<IWebHostEnvironment>();
webHostEnvironmentMock.SetupGet(e => e.EnvironmentName).Returns(Environments.Development);
webHostEnvironmentMock.SetupGet(e => e.ApplicationName).Returns("Altinn.App.Api");
webHostEnvironmentMock.SetupGet(e => e.ContentRootPath).Returns(Directory.GetCurrentDirectory());
webHostEnvironmentMock
.SetupGet(e => e.WebRootPath)
.Returns(Path.Join(Directory.GetCurrentDirectory(), "wwwroot"));
webHostEnvironmentMock.SetupGet(e => e.ContentRootFileProvider).Returns(new NullFileProvider());
webHostEnvironmentMock.SetupGet(e => e.WebRootFileProvider).Returns(new NullFileProvider());
services.Replace(ServiceDescriptor.Singleton(webHostEnvironmentMock.Object));

services.AddSingleton(
new AppMetadataMutationHook(appMetadata =>
{
appMetadata.OnEntry = new OnEntry { Show = "Task_1" };
appMetadata.DataTypes.Find(d => d.Id == "default")!.AppLogic!.AllowAnonymousOnStateless = true;
})
);
}
}
Loading
Loading