Skip to content

Commit be8b390

Browse files
RolandGuijtRoland Guijt
andauthored
roland/BFF4Angular (#292)
* Introduce sample and update .NET part * Upgrade to Angular 20 * Upgrade to Angular 21, add projects to slnx --------- Co-authored-by: Roland Guijt <roland.guijt@duendesoftware.com>
1 parent 1ecb47a commit be8b390

58 files changed

Lines changed: 11946 additions & 0 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<Project Sdk="Microsoft.NET.Sdk.Web">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net10.0</TargetFramework>
5+
<Nullable>enable</Nullable>
6+
<ImplicitUsings>enable</ImplicitUsings>
7+
<InvariantGlobalization>true</InvariantGlobalization>
8+
</PropertyGroup>
9+
10+
<ItemGroup>
11+
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.2" />
12+
</ItemGroup>
13+
14+
</Project>
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
// Copyright (c) Duende Software. All rights reserved.
2+
// Licensed under the MIT License. See LICENSE in the project root for license information.
3+
4+
using Angular.Api;
5+
6+
var builder = WebApplication.CreateBuilder(args);
7+
8+
builder.Services.AddAuthentication("token")
9+
.AddJwtBearer("token", options =>
10+
{
11+
options.Authority = "https://demo.duendesoftware.com";
12+
options.Audience = "api";
13+
14+
options.MapInboundClaims = false;
15+
});
16+
17+
builder.Services.AddAuthorization(options =>
18+
{
19+
options.AddPolicy("ApiCaller", policy =>
20+
{
21+
policy.RequireClaim("scope", "api");
22+
});
23+
24+
options.AddPolicy("InteractiveUser", policy =>
25+
{
26+
policy.RequireClaim("sub");
27+
});
28+
});
29+
30+
var app = builder.Build();
31+
32+
app.UseHttpsRedirection();
33+
34+
app.MapGroup("/todos")
35+
.ToDoGroup()
36+
.RequireAuthorization("ApiCaller", "InteractiveUser");
37+
38+
39+
app.Run();
40+
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"$schema": "http://json.schemastore.org/launchsettings.json",
3+
"profiles": {
4+
"https": {
5+
"commandName": "Project",
6+
"dotnetRunMessages": true,
7+
"launchBrowser": false,
8+
"applicationUrl": "https://localhost:7001",
9+
"environmentVariables": {
10+
"ASPNETCORE_ENVIRONMENT": "Development"
11+
}
12+
}
13+
}
14+
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
// Copyright (c) Duende Software. All rights reserved.
2+
// Licensed under the MIT License. See LICENSE in the project root for license information.
3+
4+
using System.Security.Claims;
5+
using Microsoft.AspNetCore.Http.Extensions;
6+
7+
namespace Angular.Api;
8+
9+
public static class TodoEndpointGroup
10+
{
11+
private static readonly List<ToDo> data = new List<ToDo>()
12+
{
13+
new ToDo { Id = ToDo.NewId(), Date = DateTimeOffset.UtcNow, Name = "Demo ToDo API", User = "2 (Bob Smith)" },
14+
new ToDo { Id = ToDo.NewId(), Date = DateTimeOffset.UtcNow.AddHours(1), Name = "Stop Demo", User = "2 (Bob Smith)" },
15+
new ToDo { Id = ToDo.NewId(), Date = DateTimeOffset.UtcNow.AddHours(4), Name = "Have Dinner", User = "1 (Alice Smith)" },
16+
};
17+
18+
public static RouteGroupBuilder ToDoGroup(this RouteGroupBuilder group)
19+
{
20+
// GET
21+
group.MapGet("/", () => data);
22+
group.MapGet("/{id}", (int id) =>
23+
{
24+
var item = data.FirstOrDefault(x => x.Id == id);
25+
});
26+
27+
// POST
28+
group.MapPost("/", (ToDo model, ClaimsPrincipal user, HttpContext context) =>
29+
{
30+
model.Id = ToDo.NewId();
31+
model.User = $"{user.FindFirst("sub")?.Value} ({user.FindFirst("name")?.Value})";
32+
33+
data.Add(model);
34+
35+
var url = new Uri($"{context.Request.GetEncodedUrl()}/{model.Id}");
36+
37+
return Results.Created(url, model);
38+
});
39+
40+
// PUT
41+
group.MapPut("/{id}", (int id, ToDo model, ClaimsPrincipal User) =>
42+
{
43+
var item = data.FirstOrDefault(x => x.Id == id);
44+
if (item == null) return Results.NotFound();
45+
46+
item.Date = model.Date;
47+
item.Name = model.Name;
48+
49+
return Results.NoContent();
50+
});
51+
52+
// DELETE
53+
group.MapDelete("/{id}", (int id) =>
54+
{
55+
var item = data.FirstOrDefault(x => x.Id == id);
56+
if (item == null) return Results.NotFound();
57+
58+
data.Remove(item);
59+
60+
return Results.NoContent();
61+
});
62+
63+
return group;
64+
}
65+
}
66+
67+
public class ToDo
68+
{
69+
static int _nextId = 1;
70+
public static int NewId()
71+
{
72+
return _nextId++;
73+
}
74+
75+
public int Id { get; set; }
76+
public DateTimeOffset Date { get; set; }
77+
public string? Name { get; set; }
78+
public string? User { get; set; }
79+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"Logging": {
3+
"LogLevel": {
4+
"Default": "Information",
5+
"Microsoft.AspNetCore": "Warning"
6+
}
7+
}
8+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"Logging": {
3+
"LogLevel": {
4+
"Default": "Information",
5+
"Microsoft.AspNetCore": "Warning"
6+
}
7+
},
8+
"AllowedHosts": "*"
9+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<Project Sdk="Microsoft.NET.Sdk.Web">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net10.0</TargetFramework>
5+
<Nullable>enable</Nullable>
6+
<ImplicitUsings>enable</ImplicitUsings>
7+
<InvariantGlobalization>true</InvariantGlobalization>
8+
<SpaRoot>..\angular.ssr</SpaRoot>
9+
<SpaProxyLaunchCommand>npm start</SpaProxyLaunchCommand>
10+
<SpaProxyServerUrl>https://localhost:4200</SpaProxyServerUrl>
11+
<RootNamespace>Angular.Bff</RootNamespace>
12+
</PropertyGroup>
13+
14+
<ItemGroup>
15+
<ProjectReference Include="..\angular.ssr\angular.ssr.esproj">
16+
<ReferenceOutputAssembly>false</ReferenceOutputAssembly>
17+
</ProjectReference>
18+
</ItemGroup>
19+
20+
<ItemGroup>
21+
<PackageReference Include="Duende.Bff.Yarp" Version="4.0.2" />
22+
<PackageReference Include="Microsoft.AspNetCore.SpaProxy">
23+
<Version>10.0.2</Version>
24+
</PackageReference>
25+
</ItemGroup>
26+
27+
</Project>
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
// Copyright (c) Duende Software. All rights reserved.
2+
// Licensed under the MIT License. See LICENSE in the project root for license information.
3+
4+
using Angular.Bff;
5+
using Duende.Bff;
6+
using Duende.Bff.DynamicFrontends;
7+
using Duende.Bff.Yarp;
8+
9+
var builder = WebApplication.CreateBuilder(args);
10+
11+
builder.Services.AddAuthentication(options =>
12+
{
13+
options.DefaultScheme = BffAuthenticationSchemes.BffCookie;
14+
options.DefaultChallengeScheme = BffAuthenticationSchemes.BffOpenIdConnect;
15+
options.DefaultSignOutScheme = BffAuthenticationSchemes.BffOpenIdConnect;
16+
});
17+
18+
builder.Services.AddBff()
19+
.AddRemoteApis()
20+
.ConfigureOpenIdConnect(options =>
21+
{
22+
options.Authority = "https://demo.duendesoftware.com";
23+
options.ClientId = "interactive.confidential";
24+
options.ClientSecret = "secret";
25+
options.ResponseType = "code";
26+
options.ResponseMode = "query";
27+
28+
options.GetClaimsFromUserInfoEndpoint = true;
29+
options.MapInboundClaims = false;
30+
options.SaveTokens = true;
31+
32+
options.Scope.Clear();
33+
options.Scope.Add("openid");
34+
options.Scope.Add("profile");
35+
options.Scope.Add("api");
36+
options.Scope.Add("offline_access");
37+
38+
options.TokenValidationParameters = new()
39+
{
40+
NameClaimType = "name",
41+
RoleClaimType = "role"
42+
};
43+
})
44+
.ConfigureCookies(options =>
45+
{
46+
options.Cookie.Name = "__Host-bff";
47+
options.Cookie.SameSite = SameSiteMode.Strict;
48+
});
49+
50+
builder.Services.AddAuthorization();
51+
52+
var app = builder.Build();
53+
54+
app.UseDefaultFiles();
55+
app.MapStaticAssets();
56+
57+
// Configure the HTTP request pipeline.
58+
59+
app.UseHttpsRedirection();
60+
app.UseAuthentication();
61+
app.UseBff();
62+
app.UseAuthorization();
63+
app.MapBffManagementEndpoints();
64+
65+
// Comment this out to use the external api
66+
app.MapGroup("/todos")
67+
.ToDoGroup()
68+
.RequireAuthorization()
69+
.AsBffApiEndpoint();
70+
71+
// Comment this in to use the external api
72+
//app.MapRemoteBffApiEndpoint("/todos", "https://localhost:7001/todos")
73+
// .RequireAccessToken(Duende.Bff.TokenType.User);
74+
75+
app.MapFallbackToFile("/index.html");
76+
77+
app.Run();
78+
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"$schema": "http://json.schemastore.org/launchsettings.json",
3+
"profiles": {
4+
"https": {
5+
"commandName": "Project",
6+
"dotnetRunMessages": true,
7+
"launchBrowser": true,
8+
"applicationUrl": "https://localhost:6001",
9+
"environmentVariables": {
10+
"ASPNETCORE_ENVIRONMENT": "Development",
11+
"ASPNETCORE_HOSTINGSTARTUPASSEMBLIES": "Microsoft.AspNetCore.SpaProxy"
12+
}
13+
}
14+
}
15+
}
16+
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
// Copyright (c) Duende Software. All rights reserved.
2+
// Licensed under the MIT License. See LICENSE in the project root for license information.
3+
4+
using System.Security.Claims;
5+
using Microsoft.AspNetCore.Http.Extensions;
6+
7+
namespace Angular.Bff;
8+
9+
public static class TodoEndpointGroup
10+
{
11+
12+
private static readonly List<ToDo> data = new List<ToDo>()
13+
{
14+
new ToDo { Id = ToDo.NewId(), Date = DateTimeOffset.UtcNow, Name = "Demo ToDo API", User = "2 (Bob Smith)" },
15+
new ToDo { Id = ToDo.NewId(), Date = DateTimeOffset.UtcNow.AddHours(1), Name = "Stop Demo", User = "2 (Bob Smith)" },
16+
new ToDo { Id = ToDo.NewId(), Date = DateTimeOffset.UtcNow.AddHours(4), Name = "Have Dinner", User = "1 (Alice Smith)" },
17+
};
18+
19+
public static RouteGroupBuilder ToDoGroup(this RouteGroupBuilder group)
20+
{
21+
// GET
22+
group.MapGet("/", () => data);
23+
group.MapGet("/{id}", (int id) =>
24+
{
25+
var item = data.FirstOrDefault(x => x.Id == id);
26+
});
27+
28+
// POST
29+
group.MapPost("/", (ToDo model, ClaimsPrincipal user, HttpContext context) =>
30+
{
31+
model.Id = ToDo.NewId();
32+
model.User = $"{user.FindFirst("sub")?.Value} ({user.FindFirst("name")?.Value})";
33+
34+
data.Add(model);
35+
36+
var url = new Uri($"{context.Request.GetEncodedUrl()}/{model.Id}");
37+
38+
return Results.Created(url, model);
39+
});
40+
41+
// PUT
42+
group.MapPut("/{id}", (int id, ToDo model, ClaimsPrincipal User) =>
43+
{
44+
var item = data.FirstOrDefault(x => x.Id == id);
45+
if (item == null) return Results.NotFound();
46+
47+
item.Date = model.Date;
48+
item.Name = model.Name;
49+
50+
return Results.NoContent();
51+
});
52+
53+
// DELETE
54+
group.MapDelete("/{id}", (int id) =>
55+
{
56+
var item = data.FirstOrDefault(x => x.Id == id);
57+
if (item == null) return Results.NotFound();
58+
59+
data.Remove(item);
60+
61+
return Results.NoContent();
62+
});
63+
64+
return group;
65+
}
66+
}
67+
68+
public class ToDo
69+
{
70+
static int _nextId = 1;
71+
public static int NewId()
72+
{
73+
return _nextId++;
74+
}
75+
76+
public int Id { get; set; }
77+
public DateTimeOffset Date { get; set; }
78+
public string? Name { get; set; }
79+
public string? User { get; set; }
80+
}

0 commit comments

Comments
 (0)