Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
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
32 changes: 16 additions & 16 deletions AStar.Web.slnx
Original file line number Diff line number Diff line change
@@ -1,34 +1,34 @@
<Solution>
<Folder Name="/Solution Items/" />
<Folder Name="/Solution Items/"/>
<Folder Name="/Solution Items/.github/">
<File Path=".github\codeql.yml" />
<File Path=".github\copilot-instructions.md" />
<File Path=".github\dependabot.yml" />
<File Path=".github\codeql.yml"/>
<File Path=".github\copilot-instructions.md"/>
<File Path=".github\dependabot.yml"/>
</Folder>
<Folder Name="/Solution Items/.github/instructions/">
<File Path=".github\instructions\blazor.instructions.md" />
<File Path=".github\instructions\blazor.instructions.md"/>
</Folder>
<Folder Name="/Solution Items/.github/prompts/">
<File Path=".github\prompts\astar.prompt.md" />
<File Path=".github\prompts\xunit.prompt.md" />
<File Path=".github\prompts\astar.prompt.md"/>
<File Path=".github\prompts\xunit.prompt.md"/>
</Folder>
<Folder Name="/Solution Items/.github/workflows/">
<File Path=".github\workflows\main_astar-dev.yml" />
<File Path=".github\workflows\main_astar-dev.yml"/>
</Folder>
<Folder Name="/src/" />
<Folder Name="/src/"/>
<Folder Name="/src/aspire/">
<Project Path="src/aspire/AStar.Web.AppHost/AStar.Web.AppHost.csproj" />
<Project Path="src/aspire/AStar.Web.ServiceDefaults/AStar.Web.ServiceDefaults.csproj" />
<Project Path="src/aspire/AStar.Web.AppHost/AStar.Web.AppHost.csproj"/>
<Project Path="src/aspire/AStar.Web.ServiceDefaults/AStar.Web.ServiceDefaults.csproj"/>
</Folder>
<Folder Name="/src/modules/" />
<Folder Name="/src/modules/"/>
<Folder Name="/src/modules/apis/">
<Project Path="src/modules/apis/AStar.Web.ApiService/AStar.Web.ApiService.csproj" />
<Project Path="src/modules/apis/AStar.Web.ApiService/AStar.Web.ApiService.csproj"/>
</Folder>
<Folder Name="/src/uis/">
<Project Path="src\uis\AStar.Dev.Web\AStar.Dev.Web.csproj" />
<Project Path="src\uis\AStar.Dev.Web\AStar.Dev.Web.csproj"/>
</Folder>
<Folder Name="/test/" />
<Folder Name="/test/"/>
<Folder Name="/test/aspire/">
<Project Path="test/aspire/AStar.Web.Tests/AStar.Web.Tests.csproj" />
<Project Path="test/aspire/AStar.Web.Tests/AStar.Web.Tests.csproj"/>
</Folder>
</Solution>
22 changes: 22 additions & 0 deletions Directory.Build.props
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<Project>
<PropertyGroup>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>true</ImplicitUsings>
<InterceptorsNamespaces>
$(InterceptorsNamespaces);Microsoft.AspNetCore.OpenApi.Generated
</InterceptorsNamespaces>
<Features>$(Features);InterceptorsPreview</Features>
</PropertyGroup>

<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<TreatWarningsAsErrors>True</TreatWarningsAsErrors>
<NoWarn>1701;1702;</NoWarn>
</PropertyGroup>

<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<TreatWarningsAsErrors>True</TreatWarningsAsErrors>
<NoWarn>1701;1702;</NoWarn>
</PropertyGroup>
</Project>
57 changes: 57 additions & 0 deletions blazor auth notes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
✅ Blazor + Entra ID App Role Access Control Checklist

🔧 App Registration Setup

- App role defined in the App Registration manifest (e.g., "Admin").
- App Registration is treated as an API:
- Application ID URI is set (e.g., api://{client-id}).
- At least one scope is defined under “Expose an API”.
- accessTokenAcceptedVersion is set to 2 in the manifest.



👥 Role Assignment

- Users or groups are assigned to the app role via Enterprise Applications → Users and groups.
- Role assignment is done on the service principal of the API App Registration.

🔐 Token Request Configuration

- Blazor app requests token for the API scope:

- Example: api://{API-App-ClientId}/.default


- MSAL or Microsoft Identity Web is configured to request scopes correctly.

🧾 Token Validation

- Token contains the roles claim:

- Use https://jwt.ms to inspect the token.
- Confirm "roles": ["Admin"] is present.



🧰 Blazor Authorization Setup

- Authentication is configured using AddMicrosoftIdentityWebApp.
- Authorization policy is defined:

```C#
options.AddPolicy("AdminOnly", policy => policy.RequireRole("Admin"));Show more lines
```

Razor components/pages are protected:

```C#
@attribute [Authorize(Roles = "Admin")]
```

🧪 Testing

- Test with a user assigned to the role.
- Confirm access is granted to protected pages.
- Test with a user not assigned to the role.
- Confirm access is denied or redirected.

2 changes: 1 addition & 1 deletion src/aspire/AStar.Web.AppHost/AStar.Web.AppHost.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

<ItemGroup>
<ProjectReference Include="..\..\modules\apis\AStar.Web.ApiService\AStar.Web.ApiService.csproj"/>
<ProjectReference Include="..\..\uis\AStar.Dev.Web\AStar.Dev.Web.csproj" />
<ProjectReference Include="..\..\uis\AStar.Dev.Web\AStar.Dev.Web.csproj"/>
</ItemGroup>

</Project>
2 changes: 1 addition & 1 deletion src/aspire/AStar.Web.AppHost/AppHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,4 @@
.WithReference(apiService)
.WaitFor(apiService);

builder.Build().Run();
builder.Build().Run();
7 changes: 3 additions & 4 deletions src/aspire/AStar.Web.ServiceDefaults/Extensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,8 @@ public static TBuilder ConfigureOpenTelemetry<TBuilder>(this TBuilder builder)
tracing.AddSource(builder.Environment.ApplicationName)
.AddAspNetCoreInstrumentation(tracing =>
// Exclude health check requests from tracing
tracing.Filter = context =>
!context.Request.Path.StartsWithSegments(HealthEndpointPath)
&& !context.Request.Path.StartsWithSegments(AlivenessEndpointPath)
tracing.Filter = context => !context.Request.Path.StartsWithSegments(HealthEndpointPath)
&& !context.Request.Path.StartsWithSegments(AlivenessEndpointPath)
)
// Uncomment the following line to enable gRPC instrumentation (requires the OpenTelemetry.Instrumentation.GrpcNetClient package)
//.AddGrpcClientInstrumentation()
Expand Down Expand Up @@ -123,4 +122,4 @@ public static WebApplication MapDefaultEndpoints(this WebApplication app)

return app;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Asp.Versioning.Mvc" Version="8.1.0"/>
<PackageReference Include="Asp.Versioning.Mvc.ApiExplorer" Version="8.1.0"/>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.0"/>
</ItemGroup>

Expand Down
28 changes: 20 additions & 8 deletions src/modules/apis/AStar.Web.ApiService/Program.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,23 @@
using Asp.Versioning;

var builder = WebApplication.CreateBuilder(args);

// Add service defaults & Aspire client integrations.
builder.AddServiceDefaults();

// Add services to the container.
builder.Services.AddProblemDetails();
_ = builder.Services.AddApiVersioning(options =>
{
options.DefaultApiVersion = new ApiVersion(1, 0);
options.AssumeDefaultVersionWhenUnspecified = true;
options.ReportApiVersions = true;
})
.AddApiExplorer(options =>
{
options.GroupNameFormat = "'v'VVV";
options.SubstituteApiVersionInUrl = true;
});

// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
builder.Services.AddOpenApi();
Expand All @@ -23,13 +36,12 @@

app.MapGet("/weatherforecast", () =>
{
var forecast = Enumerable.Range(1, 5).Select(index =>
new WeatherForecast
(
DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
Random.Shared.Next(-20, 55),
summaries[Random.Shared.Next(summaries.Length)]
))
var forecast = Enumerable.Range(1, 5).Select(index => new WeatherForecast
(
DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
Random.Shared.Next(-20, 55),
summaries[Random.Shared.Next(summaries.Length)]
))
.ToArray();
return forecast;
})
Expand All @@ -42,4 +54,4 @@
internal record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary)
{
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}
}
34 changes: 34 additions & 0 deletions src/uis/AStar.Dev.Web/AStar.Dev.Web.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,44 @@
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<Deterministic>true</Deterministic>
<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
<UserSecretsId>d7e61e34-084f-4c2b-9be8-5925170dc3b7</UserSecretsId>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Azure.Monitor.OpenTelemetry.AspNetCore" Version="1.4.0"/>
<PackageReference Include="FluentUI.Demo.Shared" Version="4.10.4"/>
<PackageReference Include="JetBrains.Annotations" Version="2025.2.2"/>
<PackageReference Include="Markdig" Version="0.43.0"/>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="10.0.0"/>
<PackageReference Include="Microsoft.ApplicationInsights.AspNetCore" Version="2.23.0"/>
<PackageReference Include="Microsoft.Extensions.Logging.ApplicationInsights" Version="2.23.0"/>
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="10.0.0"/>
<PackageReference Include="Microsoft.FluentUI.AspNetCore.Components" Version="4.13.1"/>
<PackageReference Include="Microsoft.FluentUI.AspNetCore.Components.Icons" Version="4.13.1"/>
<PackageReference Include="Microsoft.Identity.Web" Version="4.0.1"/>
<PackageReference Include="Microsoft.Identity.Web.UI" Version="4.0.1"/>
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.14.0"/>
<PackageReference Include="Scalar.AspNetCore" Version="2.10.3"/>
<PackageReference Include="TestableIO.System.IO.Abstractions.Wrappers" Version="22.0.16"/>
<PackageReference Include="Testably.Abstractions.FileSystem.Interface" Version="10.0.0"/>
<PackageReference Include="Microsoft.Extensions.Http.Resilience" Version="10.0.0"/>
<PackageReference Include="NetEscapades.AspNetCore.SecurityHeaders" Version="1.2.0"/>
<PackageReference Include="AStar.Dev.Source.Generators" Version="0.2.6-alpha"/>
</ItemGroup>

<ItemGroup>
<Folder Include="wwwroot\assets\"/>
</ItemGroup>

<ItemGroup>
<Using Include="JetBrains.Annotations"/>
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\aspire\AStar.Web.ServiceDefaults\AStar.Web.ServiceDefaults.csproj"/>
<ProjectReference Include="..\..\modules\apis\AStar.Web.ApiService\AStar.Web.ApiService.csproj"/>
</ItemGroup>

</Project>
47 changes: 40 additions & 7 deletions src/uis/AStar.Dev.Web/Components/App.razor
Original file line number Diff line number Diff line change
@@ -1,21 +1,54 @@
<!DOCTYPE html>
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<base href="/"/>
<link rel="stylesheet" href="@Assets["lib/bootstrap/dist/css/bootstrap.min.css"]"/>
<link rel="stylesheet" href="@Assets["app.css"]"/>
<link rel="stylesheet" href="@Assets["AStar.Dev.Web.styles.css"]"/>
<ImportMap/>
<link rel="icon" type="image/png" href="favicon.png"/>
<HeadOutlet/>
<link rel="icon" type="image/x-icon" href="favicon.ico"/>
<HeadOutlet @rendermode="InteractiveServer"/>
<script src="_content/Microsoft.FluentUI.AspNetCore.Components/js/loading-theme.js" type="text/javascript"></script>
<loading-theme storage-name="theme"></loading-theme>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
<script>
window.highlightMarkdown = () => {
if (window.hljs) {
window.hljs.highlightAll();
}
};
</script>
<!-- Add marked (Markdown parser) and DOMPurify (sanitizer) -->
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/purify.min.js"></script>
<script>
// md: markdown text
// allowHtml: when true, bypass sanitizer (only do this if you trust the source)
window.renderMarkdown = (md, allowHtml) => {
const html = window.marked?.parse(md ?? "") ?? "";
if (allowHtml) {
return html;
}
// Sanitize, but allow common code/formatting tags and classes used by highlight.js
const cfg = {
ALLOWED_TAGS: [
'pre', 'code', 'span', 'p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
'ul', 'ol', 'li', 'a', 'strong', 'em', 'table', 'thead', 'tbody',
'tr', 'th', 'td', 'blockquote', 'hr', 'br', 'img'
],
ALLOWED_ATTR: ['href', 'title', 'alt', 'class', 'rel', 'target', 'src']
};
return window.DOMPurify ? window.DOMPurify.sanitize(html, cfg) : html;
};
</script>
</head>

<body>
<Routes/>
<script src="@Assets["_framework/blazor.web.js"]"></script>
<Routes @rendermode="InteractiveServer"/>
<script src="_framework/blazor.web.js"></script>
</body>

</html>
</html>
6 changes: 6 additions & 0 deletions src/uis/AStar.Dev.Web/Components/App.razor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace AStar.Dev.Web.Components;

public partial class App
{
public static string PageTitle(string page) => $"{page} - AStar Development";
}
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

48 changes: 30 additions & 18 deletions src/uis/AStar.Dev.Web/Components/Layout/MainLayout.razor
Original file line number Diff line number Diff line change
@@ -1,23 +1,35 @@
@inherits LayoutComponentBase
@using Microsoft.FluentUI.AspNetCore.Components.Icons.Regular
@inherits LayoutComponentBase

<div class="page">
<div class="sidebar">
<NavMenu/>
</div>
<FluentLayout>
<FluentHeader>
<img src="@Assets["assets/astar-logo.png"]" alt="AStar.Dev" style="height: 40px;"/>
<FluentSpacer/>
<FluentButton Appearance="Appearance.Stealth"
IconStart="@(new Size20.Settings())"
@onclick="OpenSettingsDialog"
Title="Settings"/>

<main>
<div class="top-row px-4">
<a href="https://learn.microsoft.com/aspnet/core/" target="_blank">About</a>
</div>
</FluentHeader>
<FluentStack Class="main" Orientation="Orientation.Horizontal" Width="100%">
<NavMenu/>
<FluentBodyContent Class="body-content">
<div class="content">
@Body
</div>
</FluentBodyContent>
</FluentStack>
<FluentFooter>
<FluentStack Orientation="Orientation.Horizontal" HorizontalAlignment="HorizontalAlignment.Center" Width="100%">
Copyright ©: AStar Development, 2009 - @(DateTime.Now.Year)
</FluentStack>
</FluentFooter>
</FluentLayout>

<article class="content px-4">
@Body
</article>
</main>
</div>
<SettingsPanel IsOpen="_settingsPanelOpen" OnClose="CloseSettingsPanel"/>

<div id="blazor-error-ui">
<div id="blazor-error-ui" data-nosnippet>
An unhandled error has occurred.
<a href="" class="reload">Reload</a>
<a class="dismiss">🗙</a>
</div>
<a href="." class="reload">Reload</a>
<span class="dismiss">🗙</span>
</div>
Loading
Loading