Skip to content

Commit e29971d

Browse files
Merge pull request #58 from CodebreakerApp/57-authentication
57 authentication
2 parents 454894f + 75798e1 commit e29971d

24 files changed

Lines changed: 359 additions & 172 deletions

src/CodeBreaker.Blazor.Client/CodeBreaker.Blazor.Client.csproj

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,12 @@
1515
<ItemGroup>
1616
<PackageReference Include="BlazorApplicationInsights" Version="3.0.5" />
1717
<PackageReference Include="CNinnovation.Codebreaker.GamesClient" Version="3.6.0-beta.24" />
18-
<PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="8.0.4" />
19-
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="8.0.4" />
20-
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="8.0.4" PrivateAssets="all" />
21-
<PackageReference Include="Microsoft.Authentication.WebAssembly.Msal" Version="8.0.4" />
18+
<PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="8.0.5" />
19+
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="8.0.5" />
20+
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="8.0.5" PrivateAssets="all" />
21+
<PackageReference Include="Microsoft.Authentication.WebAssembly.Msal" Version="8.0.5" />
2222
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" />
23-
<PackageReference Include="Microsoft.Extensions.Localization" Version="8.0.4" />
23+
<PackageReference Include="Microsoft.Extensions.Localization" Version="8.0.5" />
2424
<PackageReference Include="Microsoft.FluentUI.AspNetCore.Components" Version="4.7.2" />
2525
<PackageReference Include="Microsoft.FluentUI.AspNetCore.Components.Icons" Version="4.7.2" />
2626
</ItemGroup>
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
using CodeBreaker.Blazor.Client.Services;
2+
3+
namespace CodeBreaker.Blazor.Client.Contracts.Services;
4+
5+
public interface IGamerNameSuggestionClient
6+
{
7+
Task<GamerNameSuggestionsResult> GetGamerNameSuggestionsAsync(int count = 10, CancellationToken cancellationToken = default);
8+
}
9+
10+
public record GamerNameSuggestionsResult(string[] Suggestions);
Lines changed: 41 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,45 @@
11
@page "/authentication/{action}"
22
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
3+
@inject IStringLocalizer<Resource> Loc
34

4-
@*
5-
<RemoteAuthenticatorView Action="@Action" OnLogOutSucceeded="OnLogout">
6-
<LoggingIn />
7-
<CompletingLoggingIn />
8-
<CompletingLogOut />
9-
<LoggingIn />
10-
<LogInFailed />
11-
<LogOut />
12-
<LogOutFailed />
13-
<LogOutSucceeded />
5+
<RemoteAuthenticatorView Action="@Action">
6+
<LoggingIn>
7+
<FluentMessageBar Intent="@MessageIntent.Info">
8+
@Loc["AuthPage_LoggingIn"]
9+
</FluentMessageBar>
10+
</LoggingIn>
11+
<CompletingLoggingIn>
12+
<FluentMessageBar Intent="@MessageIntent.Info">
13+
@Loc["AuthPage_CompletingLoggingIn"]
14+
</FluentMessageBar>
15+
</CompletingLoggingIn>
16+
<CompletingLogOut>
17+
<FluentMessageBar Intent="@MessageIntent.Info">
18+
@Loc["AuthPage_CompletingLogOut"]
19+
</FluentMessageBar>
20+
</CompletingLogOut>
21+
<LogInFailed>
22+
<FluentMessageBar Intent="@MessageIntent.Error">
23+
@Loc["AuthPage_LogInFailed"]
24+
</FluentMessageBar>
25+
</LogInFailed>
26+
<LogOut>
27+
<FluentMessageBar Intent="@MessageIntent.Info">
28+
@Loc["AuthPage_LogOut"]
29+
</FluentMessageBar>
30+
</LogOut>
31+
<LogOutFailed>
32+
<FluentMessageBar Intent="@MessageIntent.Error">
33+
@Loc["AuthPage_LogOutFailed"]
34+
</FluentMessageBar>
35+
</LogOutFailed>
36+
<LogOutSucceeded>
37+
<FluentMessageBar Intent="@MessageIntent.Success">
38+
@Loc["AuthPage_LogOutSucceeded"]
39+
</FluentMessageBar>
40+
</LogOutSucceeded>
1441
</RemoteAuthenticatorView>
15-
*@
42+
43+
@code {
44+
[Parameter] public string? Action { get; set; }
45+
}

src/CodeBreaker.Blazor.Client/Pages/Authentication.razor.cs

Lines changed: 0 additions & 17 deletions
This file was deleted.

src/CodeBreaker.Blazor.Client/Pages/GamePage.razor

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,26 @@
99
{
1010
<FluentGrid Class="align-items-end">
1111
<FluentGridItem xs="12" md="6">
12-
<FluentTextField
13-
Class="full-width"
14-
Label="@Loc["GamePage_NameInput"]"
15-
Placeholder="@Loc["GamePage_NameInputPlaceholder"]"
16-
@bind-Value="_name"
17-
Immediate
18-
Required
19-
Maxlength="100" />
12+
@if (_isNamePrefilled)
13+
{
14+
<FluentTextField
15+
Class="full-width"
16+
Label="@Loc["GamePage_NameInput"]"
17+
Required
18+
Value="@_gamerName"
19+
ReadOnly />
20+
}
21+
else
22+
{
23+
<FluentSelect
24+
Class="full-width"
25+
Label="@Loc["GamePage_NameInput"]"
26+
Items="_gamerNameSuggestions"
27+
@bind-Value="_gamerName"
28+
Required>
29+
<FluentProgressRing />
30+
</FluentSelect>
31+
}
2032
</FluentGridItem>
2133
<FluentGridItem xs="12" md="3">
2234
<FluentSelect

src/CodeBreaker.Blazor.Client/Pages/GamePage.razor.cs

Lines changed: 50 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,47 +8,74 @@
88
using Microsoft.Extensions.Localization;
99
using System.Collections.Frozen;
1010
using Microsoft.FluentUI.AspNetCore.Components;
11+
using Microsoft.AspNetCore.Components.Authorization;
12+
using CodeBreaker.Blazor.Client.Contracts.Services;
1113

1214
namespace CodeBreaker.Blazor.Client.Pages;
1315

1416
public partial class GamePage : IDisposable
1517
{
1618
//TODO: Get Data from API
1719
private readonly FrozenDictionary<string, GameType> _gameTypes = new Dictionary<string, GameType>() {
18-
{ "8x5", GameType.Game8x5 },
19-
//{ "6x4 Mini", GameType.Game6x4Mini },
2020
{ "6x4", GameType.Game6x4 },
21+
//{ "6x4 Mini", GameType.Game6x4Mini },
22+
{ "8x5", GameType.Game8x5 },
2123
{ "5x5x4", GameType.Game5x5x4 },
2224
}.ToFrozenDictionary();
2325

2426
private IDisposable? _beforeNavigationInterceptor;
2527
private readonly System.Timers.Timer _timer = new(TimeSpan.FromHours(1));
2628
private GameMode _gameStatus = GameMode.NotRunning;
27-
private string _name = string.Empty;
29+
private string? _gamerName;
30+
private string[]? _gamerNameSuggestions;
2831
private bool _loadingGame = false;
32+
private bool _loadingGamerNameSuggestions = false;
2933
private bool _isGameCancelling = false;
3034
private GameInfo? _game;
3135
private GameType _gameType; // A workaround, because the GameType in GameInfo is a string.
36+
private bool _isNamePrefilled;
37+
private PersistingComponentStateSubscription _persistingSubscription;
38+
39+
[Inject] private ILogger<GamePage> Logger { get; init; } = default!;
3240

3341
[Inject] private IGamesClient Client { get; init; } = default!;
42+
43+
[Inject] private IGamerNameSuggestionClient GamerNameSuggestionClient { get; init; } = default!;
3444

3545
[Inject] private NavigationManager NavigationManager { get; init; } = default!;
3646

37-
[Inject] private Microsoft.FluentUI.AspNetCore.Components.IDialogService DialogService { get; init; } = default!;
47+
[Inject] private IDialogService DialogService { get; init; } = default!;
3848

3949
[Inject] private IStringLocalizer<Resource> Loc { get; init; } = default!;
4050

51+
[CascadingParameter] private Task<AuthenticationState> AuthenticationStateTask { get; set; } = default!;
52+
53+
[Inject] private PersistentComponentState ApplicationState { get; init; } = default!;
54+
4155
private string? SelectedGameTypeKey { get; set; }
4256

4357
private GameType? SelectedGameType => SelectedGameTypeKey is null ? null : _gameTypes[SelectedGameTypeKey];
4458

45-
private bool CanStartGame => !string.IsNullOrWhiteSpace(_name) && _name.Length > 3 && !_loadingGame;
59+
private bool CanStartGame => !string.IsNullOrWhiteSpace(_gamerName) && !_loadingGame;
4660

4761
protected override async Task OnInitializedAsync()
4862
{
4963
_timer.Elapsed += OnTimedEvent;
5064
_timer.AutoReset = true;
51-
_name = string.Empty;
65+
_persistingSubscription = ApplicationState.RegisterOnPersisting(PersistGamerNameAsync);
66+
67+
// Get gamer name from user claims (if available)
68+
_gamerName = (await AuthenticationStateTask).User.FindFirst("extension_gamerName")?.Value;
69+
_isNamePrefilled = !string.IsNullOrEmpty(_gamerName);
70+
71+
// Get gamer name suggestions, if not prefilled and therefore the user is not authenticated
72+
if (!_isNamePrefilled && !ApplicationState.TryTakeFromJson(nameof(_gamerNameSuggestions), out _gamerNameSuggestions))
73+
_gamerNameSuggestions = (await GamerNameSuggestionClient.GetGamerNameSuggestionsAsync()).Suggestions;
74+
75+
// Set the first suggestion as default, if there are any and therefore the user is not authenticated
76+
if (_gamerNameSuggestions is not null)
77+
_gamerName = _gamerNameSuggestions.FirstOrDefault();
78+
5279
await base.OnInitializedAsync();
5380
}
5481

@@ -60,6 +87,13 @@ protected override void OnAfterRender(bool firstRender)
6087
base.OnAfterRender(firstRender);
6188
}
6289

90+
private Task PersistGamerNameAsync()
91+
{
92+
ApplicationState.PersistAsJson(nameof(_gamerName), _gamerName);
93+
ApplicationState.PersistAsJson(nameof(_gamerNameSuggestions), _gamerNameSuggestions);
94+
return Task.CompletedTask;
95+
}
96+
6397
private async Task StartGameAsync()
6498
{
6599
if (SelectedGameType is null)
@@ -69,19 +103,24 @@ private async Task StartGameAsync()
69103
{
70104
_loadingGame = true;
71105
_gameStatus = GameMode.NotRunning;
72-
(Guid gameId, int numberCodes, int maxMoves, IDictionary<string, string[]> fieldValues) = await Client.StartGameAsync(SelectedGameType.Value, _name);
73-
_game = new(gameId, SelectedGameType.Value.ToString(), _name, DateTime.Now, numberCodes, maxMoves)
106+
(Guid gameId, int numberCodes, int maxMoves, IDictionary<string, string[]> fieldValues) = await Client.StartGameAsync(SelectedGameType.Value, _gamerName);
107+
_game = new(gameId, SelectedGameType.Value.ToString(), _gamerName, DateTime.Now, numberCodes, maxMoves)
74108
{
75109
FieldValues = fieldValues.ToDictionary(x => x.Key, x => x.Value.AsEnumerable()),
76110
Codes = []
77111
};
78112
_gameType = SelectedGameType.Value;
79113
_gameStatus = GameMode.Started;
80114
}
115+
catch (HttpRequestException ex)
116+
{
117+
Logger.LogError(ex, ex.Message);
118+
DialogService.ShowError(ex.Message);
119+
}
81120
catch (Exception ex)
82121
{
83-
//TODO: Handle Exception
84-
Console.WriteLine(ex.Message);
122+
Logger.LogError(ex, ex.Message);
123+
throw;
85124
}
86125
finally
87126
{
@@ -179,5 +218,6 @@ public void Dispose()
179218
{
180219
_timer?.Dispose();
181220
_beforeNavigationInterceptor?.Dispose();
221+
_persistingSubscription.Dispose();
182222
}
183223
}

src/CodeBreaker.Blazor.Client/Program.cs

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,20 @@
1313
builder.Services.AddLocalization();
1414
builder.Services.AddBlazorApplicationInsights();
1515

16-
builder.Services.AddHttpClient("GameApi", (HttpClient client) =>
17-
client.BaseAddress = new Uri(builder.Configuration.GetRequired("ApiBase")));
16+
builder.Services.AddMsalAuthentication(options =>
17+
{
18+
builder.Configuration.Bind("AzureAdB2C", options.ProviderOptions.Authentication);
19+
//options.ProviderOptions.DefaultAccessTokenScopes.Add(""); // TODO
20+
options.UserOptions.NameClaim = "extension_GamerName";
21+
});
22+
23+
builder.Services.AddHttpClient<IGamerNameSuggestionClient, GamerNameSuggestionClient>(configure =>
24+
configure.BaseAddress = new Uri(builder.Configuration.GetRequired("UserApiBase")));
25+
26+
builder.Services.AddHttpClient<IGamesClient, GamesClient>(configure =>
27+
configure.BaseAddress = new Uri(builder.Configuration.GetRequired("GameApiBase")));
28+
//.AddHttpMessageHandler<>(); // TODO
1829

19-
builder.Services.AddHttpClient<IGamesClient, GamesClient>("GameApi");
2030
builder.Services.AddScoped<IMobileDetectorService, MobileDetectorService>();
2131

2232
var host = builder.Build();

0 commit comments

Comments
 (0)