Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(backoffice): require authorization for pages #158

Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion src/Eurofurence.App.Backoffice/App.razor
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<RedirectToLogin />
}
else
{
{
<p role="alert">You are not authorized to access this resource.</p>
}
</NotAuthorized>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace Eurofurence.App.Backoffice.Authentication
{
public class AuthorizationSettings
{
public List<string> KnowledgeBaseEditor { get; set; } = new List<string>();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
using System.Security.Claims;
using Eurofurence.App.Backoffice.Services;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication.Internal;

namespace Eurofurence.App.Backoffice.Authentication
{
public class BackendAccountClaimsFactory : AccountClaimsPrincipalFactory<RemoteUserAccount>
{
public IServiceProvider _serviceProvider { get; set; }

public BackendAccountClaimsFactory(IServiceProvider serviceProvider, IAccessTokenProviderAccessor accessor) : base(accessor)
{
_serviceProvider = serviceProvider;
}

public override async ValueTask<ClaimsPrincipal> CreateUserAsync(RemoteUserAccount account, RemoteAuthenticationUserOptions options)
{
var userAccount = await base.CreateUserAsync(account, options);

if (!(userAccount.Identity?.IsAuthenticated ?? false))
{
return userAccount;
}

if (userAccount.Identity is ClaimsIdentity identity)
{
var usersService = _serviceProvider.GetRequiredService<IUsersService>();
var userRecord = await usersService.GetUserSelf();
foreach (var role in userRecord.Roles)
{
identity.AddClaim(new Claim(identity.RoleClaimType, role));
}
}

return userAccount;

}
}

}
5 changes: 3 additions & 2 deletions src/Eurofurence.App.Backoffice/Layout/NavMenu.razor
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
<MudNavMenu>
<MudNavLink Href="./" Icon="@Icons.Material.Outlined.Home" Match="NavLinkMatch.All">Home</MudNavLink>
<AuthorizeView>
<MudNavLink Href="knowledgebase" Icon="@Icons.Material.Outlined.Info" Match="NavLinkMatch.Prefix">Knowledge Base</MudNavLink>
<AuthorizeView Policy="RequireKnowledgeBaseEditor">
<MudNavLink Href="knowledgebase" Icon="@Icons.Material.Outlined.Info" Match="NavLinkMatch.Prefix">Knowledge Base
</MudNavLink>
<MudNavLink Href="images" Icon="@Icons.Material.Outlined.Image" Match="NavLinkMatch.Prefix">Images</MudNavLink>
</AuthorizeView>
</MudNavMenu>
44 changes: 44 additions & 0 deletions src/Eurofurence.App.Backoffice/Pages/DebugClaims.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
@page "/debug-claims"
@using Microsoft.AspNetCore.Components.Authorization
@using System.Security.Claims
@inject AuthenticationStateProvider AuthenticationStateProvider

<h3>User Claims Debugging</h3>

@if (claims != null && claims.Any())
{
<ul>
@foreach (var claim in claims)
{
<li>@claim.Type: @claim.Value</li>
}
</ul>
}
else
{
<p>No claims found or user is not authenticated.</p>
}

@code {
private IEnumerable<Claim> claims = new List<Claim>();

protected override async Task OnInitializedAsync()
{
// Get the authentication state
var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
var user = authState.User;

// Check if the user is authenticated
if (user?.Identity?.IsAuthenticated ?? false)
{
// Retrieve and print all claims
claims = user.Claims;

// Log claims to the console (for browser console)
foreach (var claim in claims)
{
Console.WriteLine($"Claim Type: {claim.Type}, Claim Value: {claim.Value}");
}
}
}
}
3 changes: 2 additions & 1 deletion src/Eurofurence.App.Backoffice/Pages/Home.razor
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
</MudCardHeader>
<MudCardContent>
<MudText>
Welcome to the "Backoffice" of our Eurofurence mobile app. Here, you can manage data like knowledge base articles and images.
Welcome to the backoffice of the Eurofurence mobile app. Once authenticated, you will be able to perform
various tasks like managing the articles in our knowledge base, depending on your permissions.
</MudText>
<AuthorizeView>
<NotAuthorized>
Expand Down
49 changes: 28 additions & 21 deletions src/Eurofurence.App.Backoffice/Pages/Images.razor
Original file line number Diff line number Diff line change
@@ -1,40 +1,44 @@
@page "/images"
@attribute [Authorize(Policy = "RequireKnowledgeBaseEditor")]
@using Eurofurence.App.Backoffice.Services
@using Microsoft.AspNetCore.Authorization
@using Eurofurence.App.Domain.Model.Images
@using Eurofurence.App.Backoffice.Components
@attribute [Authorize]
@inject ISnackbar Snackbar
@inject IImageService ImageService
@inject IDialogService DialogService

<MudToolBar>
<MudText Typo="Typo.h6">Images</MudText>
<MudSpacer/>
<MudSpacer/>
<MudTextField T="string" ValueChanged="Search" Label="Search" Variant="Variant.Outlined" Margin="Margin.Dense" Adornment="Adornment.End" AdornmentIcon="@Icons.Material.Filled.Search" AdornmentColor="Color.Secondary"/>
<MudButton Class="ml-4" Variant="Variant.Filled" Color="Color.Primary" StartIcon="@Icons.Material.Filled.Add" @onclick="AddImage">New image</MudButton>
<MudSpacer />
<MudSpacer />
<MudTextField T="string" ValueChanged="Search" Label="Search" Variant="Variant.Outlined" Margin="Margin.Dense"
Adornment="Adornment.End" AdornmentIcon="@Icons.Material.Filled.Search" AdornmentColor="Color.Secondary" />
<MudButton Class="ml-4" Variant="Variant.Filled" Color="Color.Primary" StartIcon="@Icons.Material.Filled.Add"
@onclick="AddImage">New image</MudButton>
</MudToolBar>
<MudDataGrid T="ImageWithRelationsResponse" @ref="_dataGrid" LoadingProgressColor="Color.Primary" ServerData="@GetImages" SortMode="@SortMode.None" Groupable="false">
<MudDataGrid T="ImageWithRelationsResponse" @ref="_dataGrid" LoadingProgressColor="Color.Primary"
ServerData="@GetImages" SortMode="@SortMode.None" Groupable="false">
<Columns>
<TemplateColumn Title="Preview" CellClass="d-flex justify-center">
<CellTemplate>
@if (!string.IsNullOrEmpty(@context.Item.Url))
{
<MudLink Href="@context.Item.Url">
<MudImage Class="ml-2" Width="100" Height="100" ObjectFit="ObjectFit.Contain" Src="@context.Item.Url" />
<MudImage Class="ml-2" Width="100" Height="100" ObjectFit="ObjectFit.Contain"
Src="@context.Item.Url" />
</MudLink>
}
else
{
<MudProgressCircular Color="Color.Primary" Indeterminate="true"/>
<MudProgressCircular Color="Color.Primary" Indeterminate="true" />
}
</CellTemplate>
</TemplateColumn>
<PropertyColumn Title="ID" Property="image => image.Id"/>
<PropertyColumn Title="Name" Property="image => image.InternalReference"/>
<PropertyColumn Title="Size (Bytes)" Property="image => image.SizeInBytes"/>
<PropertyColumn Title="Last Changed" Property="image => image.LastChangeDateTimeUtc"/>
<PropertyColumn Title="ID" Property="image => image.Id" />
<PropertyColumn Title="Name" Property="image => image.InternalReference" />
<PropertyColumn Title="Size (Bytes)" Property="image => image.SizeInBytes" />
<PropertyColumn Title="Last Changed" Property="image => image.LastChangeDateTimeUtc" />
<TemplateColumn Title="Usage">
<CellTemplate>
@if (context.Item?.FursuitBadgeIds.Count > 0)
Expand All @@ -61,7 +65,8 @@
<MudIcon Icon="@Icons.Material.Outlined.Map"></MudIcon>
</MudTooltip>
}
@if (context.Item?.DealerArtPreviewIds.Count > 0 || context.Item?.DealerArtistIds.Count > 0 || context.Item?.DealerArtistThumbnailIds.Count > 0)
@if (context.Item?.DealerArtPreviewIds.Count > 0 || context.Item?.DealerArtistIds.Count > 0 ||
context.Item?.DealerArtistThumbnailIds.Count > 0)
{
<MudTooltip Text="Dealers Den">
<MudIcon Icon="@Icons.Material.Outlined.ShoppingCart"></MudIcon>
Expand All @@ -85,14 +90,15 @@
<CellTemplate>
@if (context.Item != null)
{
<MudIconButton Icon="@Icons.Material.Filled.Edit" @onclick="() => UpdateImage(context.Item)"></MudIconButton>
<MudIconButton Icon="@Icons.Material.Filled.Delete" OnClick="@(() => DeleteImage(@context.Item.Id))"/>
<MudIconButton Icon="@Icons.Material.Filled.Edit" @onclick="() => UpdateImage(context.Item)">
</MudIconButton>
<MudIconButton Icon="@Icons.Material.Filled.Delete" OnClick="@(() => DeleteImage(@context.Item.Id))" />
}
</CellTemplate>
</TemplateColumn>
</Columns>
<PagerContent>
<MudDataGridPager T="ImageWithRelationsResponse"/>
<MudDataGridPager T="ImageWithRelationsResponse" />
</PagerContent>
</MudDataGrid>

Expand Down Expand Up @@ -131,13 +137,14 @@
_dataGrid?.ReloadServerData();
}

private IEnumerable<ImageWithRelationsResponse> FilterImages(IEnumerable<ImageWithRelationsResponse> entries, string? searchString)
private IEnumerable<ImageWithRelationsResponse> FilterImages(IEnumerable<ImageWithRelationsResponse> entries, string?
searchString)
{
return string.IsNullOrEmpty(searchString)
? entries
: entries.Where(entry =>
entry.InternalReference.ToLower().Contains(searchString.ToLower())
|| entry.Id.ToString().ToLower().Contains(searchString.ToLower()));
? entries
: entries.Where(entry =>
entry.InternalReference.ToLower().Contains(searchString.ToLower())
|| entry.Id.ToString().ToLower().Contains(searchString.ToLower()));
}

private async Task AddImage()
Expand Down
2 changes: 1 addition & 1 deletion src/Eurofurence.App.Backoffice/Pages/KnowledgeBase.razor
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
@page "/knowledgebase"
@attribute [Authorize(Policy = "RequireKnowledgeBaseEditor")]
@using Eurofurence.App.Backoffice.Services
@using Eurofurence.App.Domain.Model.Knowledge
@using Microsoft.AspNetCore.Authorization
@using Eurofurence.App.Backoffice.Components
@using Eurofurence.App.Domain.Model.Images
@using Ganss.Xss
@using Markdig
@attribute [Authorize]
@inject ISnackbar Snackbar
@inject IKnowledgeService KnowledgeService
@inject IImageService ImageService
Expand Down
15 changes: 14 additions & 1 deletion src/Eurofurence.App.Backoffice/Program.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
using System.Security.Claims;
using Eurofurence.App.Backoffice;
using Eurofurence.App.Backoffice.Authentication;
using Eurofurence.App.Backoffice.Services;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using MudBlazor.Services;

Expand All @@ -16,6 +18,7 @@
builder.Services.AddScoped<IArtistAlleyService, ArtistAlleyService>();
builder.Services.AddScoped<IFursuitService, FursuitService>();
builder.Services.AddScoped<IDealerService, DealerService>();
builder.Services.AddScoped<IUsersService, UsersService>();

builder.Services.AddScoped<TokenAuthorizationMessageHandler>();
builder.Services.AddHttpClient("api", options =>
Expand All @@ -30,6 +33,16 @@
builder.Configuration.Bind("Oidc", options.ProviderOptions);
options.ProviderOptions.ResponseType = "code";
options.ProviderOptions.DefaultScopes.Add("profile");
});
}).AddAccountClaimsPrincipalFactory<RemoteAuthenticationState, RemoteUserAccount, BackendAccountClaimsFactory>();

var authSettings = builder.Configuration.GetSection("Authorization").Get<AuthorizationSettings>() ?? new AuthorizationSettings();

builder.Services.AddAuthorizationCore(config =>
{
config.AddPolicy("RequireKnowledgeBaseEditor", policy =>
policy.RequireRole("KnowledgeBaseEditor")
);
}
);

await builder.Build().RunAsync();
9 changes: 9 additions & 0 deletions src/Eurofurence.App.Backoffice/Services/IUsersService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using Eurofurence.App.Domain.Model.Users;

namespace Eurofurence.App.Backoffice.Services
{
public interface IUsersService
{
public Task<UserRecord> GetUserSelf();
}
}
17 changes: 17 additions & 0 deletions src/Eurofurence.App.Backoffice/Services/UsersService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using System.Net.Http.Json;
using System.Text.Json.Serialization;
using System.Text.Json;
using Eurofurence.App.Domain.Model.Users;

namespace Eurofurence.App.Backoffice.Services
{
public class UsersService(HttpClient http) : IUsersService
{
public async Task<UserRecord> GetUserSelf()
{
var options = new JsonSerializerOptions();
options.Converters.Add(new JsonStringEnumConverter());
return await http.GetFromJsonAsync<UserRecord>("Users/:self", options) ?? new UserRecord();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@
"ClientId": "<client-id>"
},
"AllowedHosts": "*"
}
}
2 changes: 1 addition & 1 deletion src/Eurofurence.App.Backoffice/wwwroot/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Eurofurence.App.Backoffice</title>
<title>Eurofurence App Backoffice</title>
<base href="/" />
<link rel="stylesheet" href="css/app.css" />
<link rel="icon" type="image/png" href="favicon.png" />
Expand Down
12 changes: 12 additions & 0 deletions src/Eurofurence.App.Domain.Model/Users/UserRecord.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using System.Runtime.Serialization;

namespace Eurofurence.App.Domain.Model.Users
{
public class UserRecord
{
[DataMember]
public string[] Roles { get; set; }
[DataMember]
public UserRegistration[] Registrations { get; set; }
}
}
12 changes: 12 additions & 0 deletions src/Eurofurence.App.Domain.Model/Users/UserRegistration.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using System.Runtime.Serialization;

namespace Eurofurence.App.Domain.Model.Users
{
public class UserRegistration
{
[DataMember]
public string Id { get; set; }
[DataMember]
public UserRegistrationStatus Status { get; set; }
}
}
12 changes: 12 additions & 0 deletions src/Eurofurence.App.Domain.Model/Users/UserRegistrationClaims.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
namespace Eurofurence.App.Domain.Model.Users
{
public static class UserRegistrationClaims
{
public static string Id = "RegSysId";
public static string StatusPrefix = "RegSysStatus";
public static string Status(string id)
{
return $"{StatusPrefix}:{id}";
}
}
}
14 changes: 14 additions & 0 deletions src/Eurofurence.App.Domain.Model/Users/UserRegistrationStatus.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
namespace Eurofurence.App.Domain.Model.Users
{
public enum UserRegistrationStatus
{
Unknown = 0,
New = 1,
Approved = 2,
PartiallyPaid = 3,
Paid = 4,
CheckedIn = 5,
Cancelled = 6,
Deleted = 7,
}
}
Loading