diff --git a/.github/workflows/todo-sample.cd.yml b/.github/workflows/todo-sample.cd.yml index d4e64f3ba3..739e45be3f 100644 --- a/.github/workflows/todo-sample.cd.yml +++ b/.github/workflows/todo-sample.cd.yml @@ -59,6 +59,11 @@ jobs: - name: Install wasm run: cd src && dotnet workload install wasm-tools + - name: Configure bswup + run: | + sed -i 's/\/\/ self.noPrerenderQuery/self.noPrerenderQuery/g' TodoSample/src/Client/TodoSample.Client.Web/wwwroot/service-worker.published.js + sed -i 's/\/\/ self.disablePassiveFirstBoot/self.disablePassiveFirstBoot/g' TodoSample/src/Client/TodoSample.Client.Web/wwwroot/service-worker.published.js + - name: Generate CSS/JS files run: dotnet build TodoSample/src/Client/TodoSample.Client.Core/TodoSample.Client.Core.csproj -t:BeforeBuildTasks -p:Version="${{ vars.APPLICATION_DISPLAY_VERSION}}" --no-restore -c Release diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/AppComponentBase.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/AppComponentBase.cs index 596f43d88a..4efcf25c82 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/AppComponentBase.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/AppComponentBase.cs @@ -44,6 +44,11 @@ public partial class AppComponentBase : ComponentBase, IAsyncDisposable [AutoInject] protected ITelemetryContext TelemetryContext = default!; + /// + /// + /// + [AutoInject] protected AbsoluteServerAddressProvider AbsoluteServerAddress { get; set; } = default!; + private readonly CancellationTokenSource cts = new(); protected CancellationToken CurrentCancellationToken => cts.Token; diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/ClientAppCoordinator.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/ClientAppCoordinator.cs index 8425ae9932..3b3754b075 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/ClientAppCoordinator.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/ClientAppCoordinator.cs @@ -132,7 +132,7 @@ private async Task ConnectSignalR() hubConnection = new HubConnectionBuilder() .WithAutomaticReconnect(new SignalRInfinitiesRetryPolicy()) - .WithUrl($"{HttpClient.BaseAddress}app-hub", options => + .WithUrl(new Uri(AbsoluteServerAddress, "app-hub"), options => { options.Transports = HttpTransportType.WebSockets; options.SkipNegotiation = options.Transports is HttpTransportType.WebSockets; @@ -205,7 +205,7 @@ private async Task HubConnectionDisconnected(Exception? exception) { if (exception is HubException && exception.Message.EndsWith(nameof(AppStrings.UnauthorizedException))) { - await AuthenticationManager.RefreshToken(); + await AuthenticationManager.RefreshToken(CurrentCancellationToken); } logger.LogError(exception, "SignalR connection lost."); diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/RootLayout.razor.scss b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/RootLayout.razor.scss index c238500a1e..92e4903066 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/RootLayout.razor.scss +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/RootLayout.razor.scss @@ -22,6 +22,7 @@ main { width: 100%; padding: 2rem; padding-top: 5rem; + background-color: unset; } } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/UserMenu.razor.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/UserMenu.razor.cs index 3ff3befceb..f1007c7668 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/UserMenu.razor.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/UserMenu.razor.cs @@ -42,9 +42,8 @@ protected override async Task OnInitAsync() user = (await PrerenderStateService.GetValue(() => HttpClient.GetFromJsonAsync("api/User/GetCurrentUser", JsonSerializerOptions.GetTypeInfo(), CurrentCancellationToken)))!; - var serverAddress = Configuration.GetServerAddress(); var access_token = await PrerenderStateService.GetValue(AuthTokenProvider.GetAccessToken); - profileImageUrl = $"{serverAddress}/api/Attachment/GetProfileImage?access_token={access_token}"; + profileImageUrl = new Uri(AbsoluteServerAddress, $"/api/Attachment/GetProfileImage?access_token={access_token}").ToString(); await base.OnInitAsync(); } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/LoadingComponent.razor b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/LoadingComponent.razor index 22c2d90d6a..de28567787 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/LoadingComponent.razor +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/LoadingComponent.razor @@ -1,11 +1,24 @@ - -
+
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/LoadingComponent.razor.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/LoadingComponent.razor.cs index 0b86f459bf..cdffb31c3e 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/LoadingComponent.razor.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/LoadingComponent.razor.cs @@ -1,6 +1,38 @@ -namespace Boilerplate.Client.Core.Components; + +namespace Boilerplate.Client.Core.Components; public partial class LoadingComponent { [Parameter] public string Color { get; set; } = "#123456"; + + /// + /// Renders the Loading component in full-screen mode that renders over all other components. + /// + [Parameter] public bool FullScreen { get; set; } + + /// + /// This component is used in different parts of the application under varying conditions. + /// + /// 1. In App.razor, for non-prerendered applications, this component displays minimal content before + /// Blazor fully loads. Since it doesn’t automatically get removed after the app loads, the z-index is set to -1 + /// to ensure that once Blazor renders the main components, the loader falls behind the main content and becomes hidden. + /// Additionally, the z-index will not change because @rendermode in App.razor is null, which prevents the OnAfterRender + /// method from being invoked. + /// + /// 2. In other parts of the project, like during Authorizing and Navigating, the component is automatically + /// removed from the screen, so a negative z-index is unnecessary and could actually cause it to be invisible. + /// In these cases, it needs a higher z-index to ensure it appears above other components. The new z-index value + /// is applied in the OnAfterRender lifecycle method. + /// + private string zIndex = "-1"; + protected override void OnAfterRender(bool firstRender) + { + if (firstRender) + { + zIndex = "999999"; + StateHasChanged(); + } + + base.OnAfterRender(firstRender); + } } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Offline/OfflineEditProfilePage.razor b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Offline/OfflineEditProfilePage.razor index 0f3c391ea3..7cafa588a3 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Offline/OfflineEditProfilePage.razor +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Offline/OfflineEditProfilePage.razor @@ -7,7 +7,11 @@
- + + @Localizer[nameof(AppStrings.OfflineEditProfileTitle)] + + + @Localizer[nameof(AppStrings.OfflineEditProfileMessage)] @@ -16,7 +20,7 @@ } - + diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Offline/OfflineEditProfilePage.razor.scss b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Offline/OfflineEditProfilePage.razor.scss index 7b1d922f13..1ec5d1823d 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Offline/OfflineEditProfilePage.razor.scss +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Offline/OfflineEditProfilePage.razor.scss @@ -1,8 +1,2 @@ section { } - -::deep { - form { - width: min(25rem, 100%); - } -} diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Products/AddOrEditProductModal.razor.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Products/AddOrEditProductModal.razor.cs index df7bb6c2af..a49e5ea436 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Products/AddOrEditProductModal.razor.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Products/AddOrEditProductModal.razor.cs @@ -1,5 +1,5 @@ using Boilerplate.Shared.Controllers.Categories; -using Boilerplate.Shared.Controllers.Product; +using Boilerplate.Shared.Controllers.Products; using Boilerplate.Shared.Dtos.Products; namespace Boilerplate.Client.Core.Components.Pages.Authorized.Products; diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Products/ProductsPage.razor.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Products/ProductsPage.razor.cs index 055df432d0..4158d651e2 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Products/ProductsPage.razor.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Products/ProductsPage.razor.cs @@ -1,5 +1,5 @@ //-:cnd:noEmit -using Boilerplate.Shared.Controllers.Product; +using Boilerplate.Shared.Controllers.Products; using Boilerplate.Shared.Dtos.Products; namespace Boilerplate.Client.Core.Components.Pages.Authorized.Products; diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Settings/Accordion.razor b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Settings/Accordion.razor index 50d3ebaa07..d8c6dcbed3 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Settings/Accordion.razor +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Settings/Accordion.razor @@ -1,6 +1,8 @@ @inherits AppComponentBase - diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Settings/ChangeEmailSection.razor b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Settings/ChangeEmailSection.razor index ec7c7557e1..e70b0803d6 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Settings/ChangeEmailSection.razor +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Settings/ChangeEmailSection.razor @@ -4,7 +4,7 @@ @if (showConfirmation is false) { - + diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Settings/ChangeEmailSection.razor.scss b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Settings/ChangeEmailSection.razor.scss index ca0af593d5..7fca1eb544 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Settings/ChangeEmailSection.razor.scss +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Settings/ChangeEmailSection.razor.scss @@ -1,10 +1,3 @@ section { width: 100%; } - -::deep { - form { - width: 100%; - max-width: 27rem; - } -} diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Settings/ChangePhoneNumberSection.razor b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Settings/ChangePhoneNumberSection.razor index 7df89cd0f7..38771ad1e6 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Settings/ChangePhoneNumberSection.razor +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Settings/ChangePhoneNumberSection.razor @@ -4,7 +4,7 @@ @if (showConfirmation is false) { - + diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Settings/ChangePhoneNumberSection.razor.scss b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Settings/ChangePhoneNumberSection.razor.scss index ca0af593d5..7fca1eb544 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Settings/ChangePhoneNumberSection.razor.scss +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Settings/ChangePhoneNumberSection.razor.scss @@ -1,10 +1,3 @@ section { width: 100%; } - -::deep { - form { - width: 100%; - max-width: 27rem; - } -} diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Settings/DeleteAccountSection.razor b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Settings/DeleteAccountSection.razor index 984e417909..83eb2987f9 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Settings/DeleteAccountSection.razor +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Settings/DeleteAccountSection.razor @@ -1,15 +1,15 @@ @inherits AppComponentBase
- + @Localizer[nameof(AppStrings.DeleteAccount)] - + @Localizer[nameof(AppStrings.DeleteAccountPrompt)] - +
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Settings/DeleteAccountSection.razor.scss b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Settings/DeleteAccountSection.razor.scss index ec82ca4dcc..00b117f7f7 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Settings/DeleteAccountSection.razor.scss +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Settings/DeleteAccountSection.razor.scss @@ -3,9 +3,3 @@ section { display: flex; justify-content: center; } - -::deep { - .stack { - max-width: 27rem; - } -} diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Settings/ProfileSection.razor b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Settings/ProfileSection.razor index a2d677d5c8..a96b81ffa0 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Settings/ProfileSection.razor +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Settings/ProfileSection.razor @@ -54,7 +54,7 @@ }
- + diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Settings/ProfileSection.razor.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Settings/ProfileSection.razor.cs index 1eaea48056..da318e973f 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Settings/ProfileSection.razor.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Settings/ProfileSection.razor.cs @@ -26,9 +26,8 @@ protected override async Task OnInitAsync() removeProfileImageHttpUrl = $"api/Attachment/RemoveProfileImage?access_token={access_token}"; - var serverAddress = Configuration.GetServerAddress(); - profileImageUrl = $"{serverAddress}/api/Attachment/GetProfileImage?access_token={access_token}"; - profileImageUploadUrl = $"{serverAddress}/api/Attachment/UploadProfileImage?access_token={access_token}"; + profileImageUrl = new Uri(AbsoluteServerAddress, $"/api/Attachment/GetProfileImage?access_token={access_token}").ToString(); + profileImageUploadUrl = new Uri(AbsoluteServerAddress, $"/api/Attachment/UploadProfileImage?access_token={access_token}").ToString(); await base.OnInitAsync(); } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Settings/ProfileSection.razor.scss b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Settings/ProfileSection.razor.scss index ca0af593d5..7fca1eb544 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Settings/ProfileSection.razor.scss +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Settings/ProfileSection.razor.scss @@ -1,10 +1,3 @@ section { width: 100%; } - -::deep { - form { - width: 100%; - max-width: 27rem; - } -} diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Settings/SettingsPage.razor b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Settings/SettingsPage.razor index 317b02acfe..d4dea84563 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Settings/SettingsPage.razor +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Settings/SettingsPage.razor @@ -12,13 +12,15 @@
- - @@ -39,13 +41,15 @@ - - diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Settings/SettingsPage.razor.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Settings/SettingsPage.razor.cs index 4f9869aa37..6ac9471092 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Settings/SettingsPage.razor.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Authorized/Settings/SettingsPage.razor.cs @@ -32,9 +32,8 @@ protected override async Task OnInitAsync() { user = await userController.GetCurrentUser(CurrentCancellationToken); - var serverAddress = Configuration.GetServerAddress(); var access_token = await PrerenderStateService.GetValue(AuthTokenProvider.GetAccessToken); - profileImageUrl = $"{serverAddress}/api/Attachment/GetProfileImage?access_token={access_token}"; + profileImageUrl = new Uri(AbsoluteServerAddress, $"/api/Attachment/GetProfileImage?access_token={access_token}").ToString(); } finally { diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/HomePage.razor b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/HomePage.razor index c05631ff55..bdec845cc6 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/HomePage.razor +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/HomePage.razor @@ -23,7 +23,93 @@ Href="https://github.com/bitfoundation/bitplatform/tree/develop/src/Templates/Boilerplate"> @Localizer[nameof(AppStrings.GitHubRepo)] - + + + + + + @if (isLoading) + { + + } + else if (gitHubStats is not null) + { + + + Name: + @gitHubStats.Name + + + Description: + @gitHubStats.Description + + + Homepage: + @gitHubStats.Homepage + + + Total stars: + @gitHubStats.StargazersCount.ToString("N0") + + + Forks count: + @gitHubStats.ForksCount.ToString("N0") + + + Open issues count: + @gitHubStats.OpenIssuesCount.ToString("N0") + + + Default branch: + @gitHubStats.DefaultBranch + + + } + else + { + Stats could not be loaded. + } + + + @if (isLoading) + { + + } + else if (nugetStats is not null) + { + var data = nugetStats.Data[0]; + + + Package id: + @data.Id + + + Version: + @data.Version + + + Title: + @data.Title + + + Project url: + @data.ProjectUrl + + + Total downloads: + @data.TotalDownloads.ToString("N0") + + + } + else + { + Stats could not be loaded. + } + + + +
@@ -36,6 +122,9 @@
+ + + diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/HomePage.razor.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/HomePage.razor.cs index 6ce99ea79c..5b06f3dea2 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/HomePage.razor.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/HomePage.razor.cs @@ -1,10 +1,34 @@ -namespace Boilerplate.Client.Core.Components.Pages; +using Boilerplate.Shared.Controllers.Statistics; +using Boilerplate.Shared.Dtos.Statistics; + +namespace Boilerplate.Client.Core.Components.Pages; public partial class HomePage { protected override string? Title => Localizer[nameof(AppStrings.Home)]; protected override string? Subtitle => string.Empty; - [CascadingParameter] private BitDir? currentDir { get; set; } + + [AutoInject] private IStatisticsController statisticsController = default!; + + private bool isLoading = true; + private GitHubStats? gitHubStats; + private NugetStatsDto? nugetStats; + + protected override async Task OnInitAsync() + { + await base.OnInitAsync(); + + try + { + (nugetStats, gitHubStats) = await ( + statisticsController.GetNugetStats(packageId: "Bit.BlazorUI", CurrentCancellationToken), + statisticsController.GetGitHubStats(CurrentCancellationToken)); + } + finally + { + isLoading = false; + } + } } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/ConfirmPage.razor b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/ConfirmPage.razor index 944fa04ae3..2c1d96c3b6 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/ConfirmPage.razor +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/ConfirmPage.razor @@ -7,148 +7,150 @@
- @Localizer[nameof(AppStrings.ConfirmTitle)] - - - @if (showEmailConfirmation) - { - - - @if (isEmailConfirmed is false) - { - - @Localizer[nameof(AppStrings.ConfirmEmailSubtitle)] - @Localizer[nameof(AppStrings.ConfirmEmailMessage)] - - - - - - - - - - - - - - @Localizer[nameof(AppStrings.EmailTokenConfirmButtonText)] - - - -
- - @Localizer[nameof(AppStrings.NotReceivedEmailMessage)] - - - - @Localizer[nameof(AppStrings.CheckSpamMailMessage)] - - - - @Localizer[nameof(AppStrings.ResendEmailTokenButtonText)] - - } - else - { - - @Localizer[nameof(AppStrings.EmailConfirmationSuccessTitle), emailModel.Email!] - - - - @Localizer[nameof(AppStrings.EmailConfirmationSuccessMessage)] - - - @Localizer[nameof(AppStrings.SignIn)] - } -
-
- } - - @if (showPhoneConfirmation) - { - - - @if (isPhoneConfirmed is false) - { - - @Localizer[nameof(AppStrings.ConfirmPhoneSubtitle)] - @Localizer[nameof(AppStrings.ConfirmPhoneMessage)] - - - - - - - - - - - - - - @Localizer[nameof(AppStrings.PhoneTokenConfirmButtonText)] - - - -
- - @Localizer[nameof(AppStrings.NotReceivedPhoneMessage)] - - - - @Localizer[nameof(AppStrings.ResendPhoneTokenButtonText)] - - } - else - { - - @Localizer[nameof(AppStrings.PhoneConfirmationSuccessTitle), phoneModel.PhoneNumber!] - - - - @Localizer[nameof(AppStrings.PhoneConfirmationSuccessMessage)] - - - @Localizer[nameof(AppStrings.SignIn)] - } -
-
- } -
-
- - @Localizer[nameof(AppStrings.SignIn)] - @Localizer[nameof(AppStrings.Or)] - @Localizer[nameof(AppStrings.SignUp)] + + @Localizer[nameof(AppStrings.ConfirmTitle)] + + + @if (showEmailConfirmation) + { + + + @if (isEmailConfirmed is false) + { + + @Localizer[nameof(AppStrings.ConfirmEmailSubtitle)] + @Localizer[nameof(AppStrings.ConfirmEmailMessage)] + + + + + + + + + + + + + + @Localizer[nameof(AppStrings.EmailTokenConfirmButtonText)] + + + +
+ + @Localizer[nameof(AppStrings.NotReceivedEmailMessage)] + + + + @Localizer[nameof(AppStrings.CheckSpamMailMessage)] + + + + @Localizer[nameof(AppStrings.ResendEmailTokenButtonText)] + + } + else + { + + @Localizer[nameof(AppStrings.EmailConfirmationSuccessTitle), emailModel.Email!] + + + + @Localizer[nameof(AppStrings.EmailConfirmationSuccessMessage)] + + + @Localizer[nameof(AppStrings.SignIn)] + } +
+
+ } + + @if (showPhoneConfirmation) + { + + + @if (isPhoneConfirmed is false) + { + + @Localizer[nameof(AppStrings.ConfirmPhoneSubtitle)] + @Localizer[nameof(AppStrings.ConfirmPhoneMessage)] + + + + + + + + + + + + + + @Localizer[nameof(AppStrings.PhoneTokenConfirmButtonText)] + + + +
+ + @Localizer[nameof(AppStrings.NotReceivedPhoneMessage)] + + + + @Localizer[nameof(AppStrings.ResendPhoneTokenButtonText)] + + } + else + { + + @Localizer[nameof(AppStrings.PhoneConfirmationSuccessTitle), phoneModel.PhoneNumber!] + + + + @Localizer[nameof(AppStrings.PhoneConfirmationSuccessMessage)] + + + @Localizer[nameof(AppStrings.SignIn)] + } +
+
+ } +
+
+ + @Localizer[nameof(AppStrings.SignIn)] + @Localizer[nameof(AppStrings.Or)] + @Localizer[nameof(AppStrings.SignUp)] +
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/ConfirmPage.razor.scss b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/ConfirmPage.razor.scss index 189bd821d2..17d0d236ed 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/ConfirmPage.razor.scss +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/ConfirmPage.razor.scss @@ -4,9 +4,3 @@ section { width: 100%; height: 100%; } - -::deep { - .confirm-pivot { - width: 304px; - } -} diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/ForgotPasswordPage.razor b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/ForgotPasswordPage.razor index 502e669d22..f7ac8faac2 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/ForgotPasswordPage.razor +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/ForgotPasswordPage.razor @@ -7,55 +7,57 @@
- @Localizer[nameof(AppStrings.ForgotPasswordTitle)] + + @Localizer[nameof(AppStrings.ForgotPasswordTitle)] - - @Localizer[nameof(AppStrings.ForgotPasswordMessage)] - -
- - - - - - - - - + + @Localizer[nameof(AppStrings.ForgotPasswordMessage)] + +
+ + + + + + + + + - - - - - + + + + +
+
+ + @Localizer[nameof(AppStrings.Submit)] +
- - @Localizer[nameof(AppStrings.Submit)] - -
- -
-
- @Localizer[nameof(AppStrings.ResetPasswordMessageInForgot)] - @Localizer[nameof(AppStrings.ResetPassword)] -
+ +
+
+ @Localizer[nameof(AppStrings.ResetPasswordMessageInForgot)] + @Localizer[nameof(AppStrings.ResetPassword)] +
- - @Localizer[nameof(AppStrings.SignIn)] - @Localizer[nameof(AppStrings.Or)] - @Localizer[nameof(AppStrings.SignUp)] + + @Localizer[nameof(AppStrings.SignIn)] + @Localizer[nameof(AppStrings.Or)] + @Localizer[nameof(AppStrings.SignUp)] +
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/ForgotPasswordPage.razor.scss b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/ForgotPasswordPage.razor.scss index 0fb1d9708a..17d0d236ed 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/ForgotPasswordPage.razor.scss +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/ForgotPasswordPage.razor.scss @@ -4,9 +4,3 @@ section { width: 100%; height: 100%; } - -::deep { - form { - width: 304px; - } -} diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/ResetPasswordPage.razor b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/ResetPasswordPage.razor index 731f87cb61..2fa2de5f3c 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/ResetPasswordPage.razor +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/ResetPasswordPage.razor @@ -6,149 +6,151 @@
- @Localizer[nameof(AppStrings.ResetPasswordTitle)] + + @Localizer[nameof(AppStrings.ResetPasswordTitle)] - @if (isPasswordChanged is false) - { - - @Localizer[nameof(AppStrings.ResetPasswordSubtitle)] - @Localizer[nameof(AppStrings.ResetPasswordMessage)] - -
- - + @if (isPasswordChanged is false) + { + + @Localizer[nameof(AppStrings.ResetPasswordSubtitle)] + @Localizer[nameof(AppStrings.ResetPasswordMessage)] + +
+ + - - @if (isTokenEntered is false) - { - - @if (showEmail && showPhone) - { - - - - - + + @if (isTokenEntered is false) + { + + @if (showEmail && showPhone) + { + + + + + - - - - - - } - else if (showEmail) + + + + + + } + else if (showEmail) + { + + + } + else if (showPhone) + { + + + } + + + + + + + @Localizer[nameof(AppStrings.Continue)] + + + @if (selectedKey == EmailKey) { - - + + @Localizer[nameof(AppStrings.NotReceivedEmailMessage)] +
+ @Localizer[nameof(AppStrings.CheckSpamMailMessage)] +
} - else if (showPhone) + else { - - + + @Localizer[nameof(AppStrings.NotReceivedPhoneMessage)] + } - - -
- - - @Localizer[nameof(AppStrings.Continue)] - - - @if (selectedKey == EmailKey) - { - - @Localizer[nameof(AppStrings.NotReceivedEmailMessage)] -
- @Localizer[nameof(AppStrings.CheckSpamMailMessage)] -
+ + @Localizer[nameof(AppStrings.Resend)] + } else { - - @Localizer[nameof(AppStrings.NotReceivedPhoneMessage)] - - } + +
+ + +
+
+ + +
+
- - @Localizer[nameof(AppStrings.Resend)] - - } - else - { - -
- - -
-
- - -
-
+ + @Localizer[nameof(AppStrings.ResetPasswordButtonText)] + + } +
+ + } + else + { + + @Localizer[nameof(AppStrings.ResetPasswordSuccessTitle), model.PhoneNumber!] + - - @Localizer[nameof(AppStrings.ResetPasswordButtonText)] - - } -
- - } - else - { - - @Localizer[nameof(AppStrings.ResetPasswordSuccessTitle), model.PhoneNumber!] - + + @Localizer[nameof(AppStrings.ResetPasswordSuccessBody)] + + } +
+ + @Localizer[nameof(AppStrings.SignIn)] + @Localizer[nameof(AppStrings.Or)] + @Localizer[nameof(AppStrings.SignUp)] + - - @Localizer[nameof(AppStrings.ResetPasswordSuccessBody)] - - } -
- - @Localizer[nameof(AppStrings.SignIn)] - @Localizer[nameof(AppStrings.Or)] - @Localizer[nameof(AppStrings.SignUp)] -
diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/ResetPasswordPage.razor.scss b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/ResetPasswordPage.razor.scss index 0fb1d9708a..ac4f4f5252 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/ResetPasswordPage.razor.scss +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/ResetPasswordPage.razor.scss @@ -3,10 +3,4 @@ section { width: 100%; height: 100%; -} - -::deep { - form { - width: 304px; - } -} +} \ No newline at end of file diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/SignIn/SignInPage.razor b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/SignIn/SignInPage.razor index 0536df2943..800f6170dd 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/SignIn/SignInPage.razor +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/SignIn/SignInPage.razor @@ -7,7 +7,7 @@
- + diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/SignIn/SignInPage.razor.scss b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/SignIn/SignInPage.razor.scss index c1250519ac..265a370b27 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/SignIn/SignInPage.razor.scss +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/SignIn/SignInPage.razor.scss @@ -4,20 +4,3 @@ section { width: 100%; height: 100%; } - -.form-forgot-password { - font-size: 14px; - line-height: 24px; - margin-bottom: 20px; -} - -.tfa-otp-container { - gap: 4px; -} - - -::deep { - form { - max-width: 460px; - } -} diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/SignUp/SignUpPage.razor b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/SignUp/SignUpPage.razor index af6bc32736..d6fbcea999 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/SignUp/SignUpPage.razor +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/SignUp/SignUpPage.razor @@ -7,25 +7,26 @@
- - @Localizer[nameof(AppStrings.SignUpPanelTitle)] + + + @Localizer[nameof(AppStrings.SignUpPanelTitle)] - - @Localizer[nameof(AppStrings.SignUpPanelSubtitle)] - - + + @Localizer[nameof(AppStrings.SignUpPanelSubtitle)] + + - + - @Localizer[AppStrings.Or] - @Localizer[AppStrings.Or] + @Localizer[AppStrings.Or] + @Localizer[AppStrings.Or] - - + + - - - + + + @* *@ - - - - + + + + - - - - - + + + + + - - - + + + @*#if (captcha == "reCaptcha")*@ - + @*#endif*@ - - @Localizer[nameof(AppStrings.SignUp)] - - - -
- - @Localizer[nameof(AppStrings.SignInMessageInSignUp)] - @Localizer[nameof(AppStrings.SignIn)] - @Localizer[nameof(AppStrings.Or)] - - @Localizer[nameof(AppStrings.Confirm)] - - - - By signing up, you agree to our @Localizer[nameof(AppStrings.Terms)] - + + @Localizer[nameof(AppStrings.SignUp)] + +
+ +
+ + @Localizer[nameof(AppStrings.SignInMessageInSignUp)] + @Localizer[nameof(AppStrings.SignIn)] + @Localizer[nameof(AppStrings.Or)] + + @Localizer[nameof(AppStrings.Confirm)] + + + + By signing up, you agree to our @Localizer[nameof(AppStrings.Terms)] + +
\ No newline at end of file diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/NotAuthorizedPage.razor b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/NotAuthorizedPage.razor index 2f0047c280..4e02f40af1 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/NotAuthorizedPage.razor +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/NotAuthorizedPage.razor @@ -4,18 +4,25 @@ @Localizer[nameof(AppStrings.NotAuthorizedPageTitle)] - -
- - - - - @Localizer[nameof(AppStrings.ForbiddenException)] - +@if (isRefreshingToken) +{ + +} +else +{ + +
+ + - @Localizer[nameof(AppStrings.YouAreSignInAs)] @user.GetDisplayName() + + @Localizer[nameof(AppStrings.ForbiddenException)] + - @Localizer[nameof(AppStrings.SignInAsDifferentUser)] - -
-
\ No newline at end of file + @Localizer[nameof(AppStrings.YouAreSignInAs)] @user.GetDisplayName() + + @Localizer[nameof(AppStrings.SignInAsDifferentUser)] +
+
+
+} \ No newline at end of file diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/NotAuthorizedPage.razor.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/NotAuthorizedPage.razor.cs index 9d76ecf3ec..ba7aa38d2c 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/NotAuthorizedPage.razor.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/NotAuthorizedPage.razor.cs @@ -4,6 +4,7 @@ namespace Boilerplate.Client.Core.Components.Pages; public partial class NotAuthorizedPage { + private bool isRefreshingToken; private ClaimsPrincipal user = default!; [SupplyParameterFromQuery(Name = "return-url"), Parameter] public string? ReturnUrl { get; set; } @@ -27,26 +28,30 @@ protected override async Task OnAfterFirstRenderAsync() if (string.IsNullOrEmpty(refresh_token) is false && ReturnUrl?.Contains("try_refreshing_token=false", StringComparison.InvariantCulture) is null or false) { - await AuthenticationManager.RefreshToken(); + isRefreshingToken = true; + StateHasChanged(); + try + { + await AuthenticationManager.RefreshToken(CurrentCancellationToken); + } + catch (UnauthorizedException) + { + RedirectToSignInPage(); + } + finally + { + isRefreshingToken = false; + } logger.LogInformation("Refreshing access token."); - if ((await AuthenticationStateTask).User.IsAuthenticated()) + if (ReturnUrl is not null) { - if (ReturnUrl is not null) - { - var @char = ReturnUrl.Contains('?') ? '&' : '?'; // The RedirectUrl may already include a query string. - NavigationManager.NavigateTo($"{ReturnUrl}{@char}try_refreshing_token=false"); - } + var @char = ReturnUrl.Contains('?') ? '&' : '?'; // The RedirectUrl may already include a query string. + NavigationManager.NavigateTo($"{ReturnUrl}{@char}try_refreshing_token=false"); } } - if ((await AuthenticationStateTask).User.IsAuthenticated() is false) - { - // If neither the refresh_token nor the access_token is present, proceed to the sign-in page. - RedirectToSignInPage(); - } - await base.OnAfterFirstRenderAsync(); } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Extensions/IClientCoreServiceCollectionExtensions.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Extensions/IClientCoreServiceCollectionExtensions.cs index b0b74618df..1ff00f7094 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Extensions/IClientCoreServiceCollectionExtensions.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Extensions/IClientCoreServiceCollectionExtensions.cs @@ -10,7 +10,6 @@ using Boilerplate.Client.Core; using System.Diagnostics.CodeAnalysis; using Microsoft.AspNetCore.Components.WebAssembly.Services; -using Boilerplate.Client.Core.Components; using Boilerplate.Client.Core.Services.HttpMessageHandlers; namespace Microsoft.Extensions.DependencyInjection; @@ -30,6 +29,7 @@ public static IServiceCollection AddClientCoreProjectServices(this IServiceColle services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(sp => new() { GetAddress = () => sp.GetRequiredService().BaseAddress! /* Read AbsoluteServerAddressProvider's comments for more info. */ }); // The following services must be unique to each app session. // Defining them as singletons would result in them being shared across all users in Blazor Server and during pre-rendering. diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Extensions/ILoggingBuilderExtensions.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Extensions/ILoggingBuilderExtensions.cs index 474e6d82fa..b1c2aadab0 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Extensions/ILoggingBuilderExtensions.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Extensions/ILoggingBuilderExtensions.cs @@ -13,18 +13,14 @@ public static ILoggingBuilder AddDiagnosticLogger(this ILoggingBuilder builder) public static ILoggingBuilder ConfigureLoggers(this ILoggingBuilder loggingBuilder) { - loggingBuilder.ClearProviders(); - if (AppEnvironment.IsDev()) { loggingBuilder.AddDebug(); } - if (!AppPlatform.IsBrowser) + if (!AppPlatform.IsBrowser) // Browser has its own BrowserConsoleLogger. { - loggingBuilder.AddConsole(); - // DiagnosticLogger is already logging in browser's console. - // But Console logger is still useful in Visual Studio's Device Log (Android, iOS) or BrowserStack etc. + loggingBuilder.AddConsole(); // Device Log / logcat } loggingBuilder.AddDiagnosticLogger(); diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/AbsoluteServerAddressProvider.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/AbsoluteServerAddressProvider.cs new file mode 100644 index 0000000000..230e7daf9e --- /dev/null +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/AbsoluteServerAddressProvider.cs @@ -0,0 +1,28 @@ +namespace Boilerplate.Client.Core.Services; + +/// +/// The `ServerAddress` setting in `Client.Core/appsettings.json` can be either relative or absolute. +/// If the server address is relative, we prepend it with `builder.HostEnvironment.BaseAddress` in Blazor WebAssembly +/// or with the request URL from `IHttpContextAccessor.HttpContext.Request` in Blazor Server. +/// The resulting server address is useful in various scenarios, such as binding an image's `src` attribute to a server API, +/// like retrieving the current user's profile image. +/// +public class AbsoluteServerAddressProvider +{ + public required Func GetAddress { get; init; } + + public static implicit operator string(AbsoluteServerAddressProvider provider) + { + return provider.GetAddress().ToString(); + } + + public static implicit operator Uri(AbsoluteServerAddressProvider provider) + { + return provider.GetAddress(); + } + + public override string ToString() + { + return this; + } +} diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/AuthenticationManager.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/AuthenticationManager.cs index 897886353e..5d7a3f22cf 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/AuthenticationManager.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/AuthenticationManager.cs @@ -17,6 +17,7 @@ public partial class AuthenticationManager : AuthenticationStateProvider [AutoInject] private IAuthTokenProvider tokenProvider = default!; [AutoInject] private IPrerenderStateService prerenderStateService; [AutoInject] private IExceptionHandler exceptionHandler = default!; + [AutoInject] private IStringLocalizer localizer = default!; [AutoInject] private IIdentityController identityController = default!; /// @@ -47,7 +48,7 @@ public async Task SignOut(CancellationToken cancellationToken) { try { - if (await storageService.GetItem("refresh_token") is not null) + if (string.IsNullOrEmpty(await storageService.GetItem("access_token")) is false) { await userController.SignOut(cancellationToken); } @@ -64,60 +65,56 @@ public async Task SignOut(CancellationToken cancellationToken) } } - public async Task RefreshToken() + public async Task RefreshToken(CancellationToken cancellationToken) { - if (AppPlatform.IsBlazorHybrid is false) + try { - await cookie.Remove("access_token"); + await semaphore.WaitAsync(); + + try + { + string? refresh_token = await storageService.GetItem("refresh_token"); + if (string.IsNullOrEmpty(refresh_token)) + throw new UnauthorizedException(localizer[nameof(AppStrings.YouNeedToSignIn)]); + + var refreshTokenResponse = await identityController.Refresh(new() { RefreshToken = refresh_token }, cancellationToken); + await StoreTokens(refreshTokenResponse!); + return refreshTokenResponse.AccessToken; + } + catch (UnauthorizedException) // refresh_token is either invalid or expired. + { + if (AppPlatform.IsBlazorHybrid is false) + { + await cookie.Remove("access_token"); + } + await storageService.RemoveItem("access_token"); + await storageService.RemoveItem("refresh_token"); + throw; + } + } + finally + { + NotifyAuthenticationStateChanged(Task.FromResult(await GetAuthenticationStateAsync())); + semaphore.Release(); } - await storageService.RemoveItem("access_token"); - NotifyAuthenticationStateChanged(Task.FromResult(await GetAuthenticationStateAsync())); } + /// + /// Handles the process of determining the user's authentication state based on the availability of access and refresh tokens. + /// + /// - If no access / refresh token exists, an anonymous user object is returned to Blazor. + /// - If an access token exists, a ClaimsPrincipal is created from it regardless of its expiration status. This ensures: + /// - Users can access anonymous-allowed pages without unnecessary delays caused by token refresh attempts **during app startup**. + /// - For protected pages, it is typical for these pages to make HTTP requests to secured APIs. In such cases, the `AuthDelegatingHandler.cs` + /// validates the access token and refreshes it if necessary, keeping Blazor updated with the latest authentication state. + /// public override async Task GetAuthenticationStateAsync() { try { var access_token = await prerenderStateService.GetValue(() => tokenProvider.GetAccessToken()); - bool inPrerenderSession = AppPlatform.IsBlazorHybrid is false && jsRuntime.IsInitialized() is false; - - if (string.IsNullOrEmpty(access_token) && inPrerenderSession is false) - { - try - { - await semaphore.WaitAsync(); - access_token = await tokenProvider.GetAccessToken(); - if (string.IsNullOrEmpty(access_token)) // Check again after acquiring the lock. - { - string? refresh_token = await storageService.GetItem("refresh_token"); - - if (string.IsNullOrEmpty(refresh_token) is false) - { - // We refresh the access_token to ensure a seamless user experience, preventing unnecessary 'NotAuthorized' page redirects and improving overall UX. - // This method is triggered after 401 and 403 server responses in AuthDelegationHandler, - // as well as when accessing pages without the required permissions in NotAuthorizedPage, ensuring that any recent claims granted to the user are promptly reflected. - - try - { - var refreshTokenResponse = await identityController.Refresh(new() { RefreshToken = refresh_token }, CancellationToken.None); - await StoreTokens(refreshTokenResponse!); - access_token = refreshTokenResponse!.AccessToken; - } - catch (UnauthorizedException) // refresh_token is either invalid or expired. - { - await storageService.RemoveItem("refresh_token"); - } - } - } - } - finally - { - semaphore.Release(); - } - } - - return new AuthenticationState(tokenProvider.ParseAccessToken(access_token, validateExpiry: false /* For better UX in order to minimize Routes.razor's Authorizing loading duration. */)); + return new AuthenticationState(tokenProvider.ParseAccessToken(access_token, validateExpiry: false)); } catch (Exception exp) { diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/ExceptionHandlerBase.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/ClientExceptionHandlerBase.cs similarity index 51% rename from src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/ExceptionHandlerBase.cs rename to src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/ClientExceptionHandlerBase.cs index 6c137be287..95dc616f96 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/ExceptionHandlerBase.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/ClientExceptionHandlerBase.cs @@ -1,18 +1,16 @@ //+:cnd:noEmit -using System.Reflection; using System.Diagnostics; using Microsoft.Extensions.Logging; using System.Runtime.CompilerServices; namespace Boilerplate.Client.Core.Services; -public abstract partial class ExceptionHandlerBase : IExceptionHandler +public abstract partial class ClientExceptionHandlerBase : SharedExceptionHandler, IExceptionHandler { [AutoInject] protected Bit.Butil.Console Console = default!; [AutoInject] protected ITelemetryContext TelemetryContext = default!; - [AutoInject] protected ILogger Logger = default!; + [AutoInject] protected ILogger Logger = default!; [AutoInject] protected readonly MessageBoxService MessageBoxService = default!; - [AutoInject] protected readonly IStringLocalizer Localizer = default!; public void Handle(Exception exception, Dictionary? parameters = null, @@ -35,14 +33,7 @@ protected virtual void Handle(Exception exception, Dictionary pa using (var scope = Logger.BeginScope(parameters.ToDictionary(i => i.Key, i => i.Value ?? string.Empty))) { - var exceptionMessageToLog = exception.Message; - var innerException = exception.InnerException; - - while (innerException is not null) - { - exceptionMessageToLog += $"{Environment.NewLine}{innerException.Message}"; - innerException = innerException.InnerException; - } + var exceptionMessageToLog = GetExceptionMessageToLog(exception); if (exception is KnownException) { @@ -54,8 +45,7 @@ protected virtual void Handle(Exception exception, Dictionary pa } } - string exceptionMessageToShow = (exception as KnownException)?.Message ?? - (isDevEnv ? exception.ToString() : Localizer[nameof(AppStrings.UnknownException)]); + string exceptionMessageToShow = GetExceptionMessageToShow(exception); MessageBoxService.Show(exceptionMessageToShow, Localizer[nameof(AppStrings.Error)]); @@ -64,29 +54,4 @@ protected virtual void Handle(Exception exception, Dictionary pa Debugger.Break(); } } - - protected Exception UnWrapException(Exception exception) - { - if (exception is AggregateException aggregateException) - { - return aggregateException.Flatten().InnerException ?? aggregateException; - } - else if (exception is TargetInvocationException) - { - return exception.InnerException ?? exception; - } - - return exception; - } - - protected bool IgnoreException(Exception exception) - { - if (exception is KnownException) - return false; - - return exception is TaskCanceledException || - exception is OperationCanceledException || - exception is TimeoutException || - (exception.InnerException is not null && IgnoreException(exception.InnerException)); - } } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/HttpMessageHandlers/AuthDelegatingHandler.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/HttpMessageHandlers/AuthDelegatingHandler.cs index c796951373..8795da20f3 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/HttpMessageHandlers/AuthDelegatingHandler.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/HttpMessageHandlers/AuthDelegatingHandler.cs @@ -1,28 +1,34 @@ -using System.Net.Http.Headers; +using System.Reflection; +using System.Net.Http.Headers; +using Boilerplate.Shared.Controllers; using Boilerplate.Shared.Controllers.Identity; namespace Boilerplate.Client.Core.Services.HttpMessageHandlers; -public partial class AuthDelegatingHandler(IAuthTokenProvider tokenProvider, - IJSRuntime jsRuntime, - IServiceProvider serviceProvider, - IStorageService storageService, - HttpMessageHandler handler) - : DelegatingHandler(handler) +public partial class AuthDelegatingHandler(IJSRuntime jsRuntime, + IStorageService storageService, + IServiceProvider serviceProvider, + IAuthTokenProvider tokenProvider, + IStringLocalizer localizer, + AbsoluteServerAddressProvider absoluteServerAddress, + HttpMessageHandler handler) : DelegatingHandler(handler) { + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { - var isRefreshTokenRequest = request.RequestUri?.LocalPath?.Contains(IIdentityController.RefreshUri, StringComparison.InvariantCultureIgnoreCase) is true; + var isInternalRequest = request.RequestUri!.ToString().StartsWith(absoluteServerAddress, StringComparison.InvariantCultureIgnoreCase); try { - if (request.Headers.Authorization is null && isRefreshTokenRequest is false) + if (isInternalRequest && /* We will restrict sending the access token to our own server only. */ + HasAnonymousApiAttribute(request) is false && + request.Headers.Authorization is null) { var access_token = await tokenProvider.GetAccessToken(); - if (access_token is not null) + if (string.IsNullOrEmpty(access_token) is false) { if (tokenProvider.ParseAccessToken(access_token, validateExpiry: true).IsAuthenticated() is false) - throw new UnauthorizedException(nameof(AppStrings.YouNeedToSignIn)); + throw new UnauthorizedException(localizer[nameof(AppStrings.YouNeedToSignIn)]); request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", access_token); } @@ -38,24 +44,36 @@ protected override async Task SendAsync(HttpRequestMessage if (AppPlatform.IsBlazorHybrid is false && jsRuntime.IsInitialized() is false) throw; // We don't have access to refresh_token during pre-rendering. + var isRefreshTokenRequest = request.RequestUri?.LocalPath?.Contains(IIdentityController.RefreshUri, StringComparison.InvariantCultureIgnoreCase) is true; + if (isRefreshTokenRequest) throw; // To prevent refresh token loop var refresh_token = await storageService.GetItem("refresh_token"); - if (refresh_token is null) throw; + if (string.IsNullOrEmpty(refresh_token)) throw; var authManager = serviceProvider.GetRequiredService(); // In the AuthenticationStateProvider, the access_token is refreshed using the refresh_token (if available). - await authManager.RefreshToken(); - - var access_token = await tokenProvider.GetAccessToken(); - - if (string.IsNullOrEmpty(access_token)) throw; + var access_token = await authManager.RefreshToken(cancellationToken); request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", access_token); return await base.SendAsync(request, cancellationToken); } } + + /// + /// + /// + private static bool HasAnonymousApiAttribute(HttpRequestMessage request) + { + if (request.Options.TryGetValue(new(RequestOptionNames.IControllerType), out Type? controllerType) is false) + return false; + + var parameterTypes = ((Dictionary)request.Options.GetValueOrDefault(RequestOptionNames.ActionParametersInfo)!).Select(p => p.Value).ToArray(); + var method = controllerType!.GetMethod((string)request.Options.GetValueOrDefault(RequestOptionNames.ActionName)!, parameterTypes)!; + return controllerType.GetCustomAttribute(inherit: true) is not null || + method.GetCustomAttribute() is not null; + } } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/HttpMessageHandlers/ExceptionDelegatingHandler.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/HttpMessageHandlers/ExceptionDelegatingHandler.cs index 2ee7fec9cf..32d06d17d4 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/HttpMessageHandlers/ExceptionDelegatingHandler.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/HttpMessageHandlers/ExceptionDelegatingHandler.cs @@ -4,16 +4,17 @@ namespace Boilerplate.Client.Core.Services.HttpMessageHandlers; public partial class ExceptionDelegatingHandler(IStringLocalizer localizer, - //#if (signalR != true) - PubSubService pubSubService, - //#endif - JsonSerializerOptions jsonSerializerOptions, - HttpMessageHandler handler) - : DelegatingHandler(handler) + //#if (signalR != true) + PubSubService pubSubService, + //#endif + JsonSerializerOptions jsonSerializerOptions, + AbsoluteServerAddressProvider absoluteServerAddress, + HttpMessageHandler handler) : DelegatingHandler(handler) { protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { bool serverCommunicationSuccess = false; + var isInternalRequest = request.RequestUri!.ToString().StartsWith(absoluteServerAddress, StringComparison.InvariantCultureIgnoreCase); try { @@ -21,25 +22,24 @@ protected override async Task SendAsync(HttpRequestMessage serverCommunicationSuccess = true; - if (response.IsSuccessStatusCode is false && response.Content.Headers.ContentType?.MediaType?.Contains("application/json", StringComparison.InvariantCultureIgnoreCase) is true) + if (isInternalRequest && /* The following exception handling mechanism applies exclusively to responses from our own server. */ + response.IsSuccessStatusCode is false && + response.Content.Headers.ContentType?.MediaType?.Contains("application/json", StringComparison.InvariantCultureIgnoreCase) is true) { - if (response.Headers.TryGetValues("Request-Id", out IEnumerable? values) && values is not null && values.Any()) - { - RestErrorInfo restError = (await response!.Content.ReadFromJsonAsync(jsonSerializerOptions.GetTypeInfo(), cancellationToken))!; + RestErrorInfo restError = (await response!.Content.ReadFromJsonAsync(jsonSerializerOptions.GetTypeInfo(), cancellationToken))!; - Type exceptionType = typeof(RestErrorInfo).Assembly.GetType(restError.ExceptionType!) ?? typeof(UnknownException); + Type exceptionType = typeof(RestErrorInfo).Assembly.GetType(restError.ExceptionType!) ?? typeof(UnknownException); - var args = new List { typeof(KnownException).IsAssignableFrom(exceptionType) ? new LocalizedString(restError.Key!, restError.Message!) : restError.Message! }; + var args = new List { typeof(KnownException).IsAssignableFrom(exceptionType) ? new LocalizedString(restError.Key!, restError.Message!) : restError.Message! }; - if (exceptionType == typeof(ResourceValidationException)) - { - args.Add(restError.Payload); - } + if (exceptionType == typeof(ResourceValidationException)) + { + args.Add(restError.Payload); + } - Exception exp = (Exception)Activator.CreateInstance(exceptionType, args.ToArray())!; + Exception exp = (Exception)Activator.CreateInstance(exceptionType, args.ToArray())!; - throw exp; - } + throw exp; } if (response.StatusCode is HttpStatusCode.Unauthorized) diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/HttpMessageHandlers/RetryDelegatingHandler.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/HttpMessageHandlers/RetryDelegatingHandler.cs index 08d38b9ae8..8e50cf5eb2 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/HttpMessageHandlers/RetryDelegatingHandler.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/HttpMessageHandlers/RetryDelegatingHandler.cs @@ -21,7 +21,7 @@ protected override async Task SendAsync(HttpRequestMessage } catch (Exception exp) when (exp is not KnownException || exp is ServerConnectionException) // If the exception is either unknown or a server connection issue, let's retry once more. { - if (HasNoRetryPolicy(request)) + if (HasNoRetryPolicyAttribute(request)) throw; lastExp = exp; @@ -35,14 +35,16 @@ protected override async Task SendAsync(HttpRequestMessage /// /// /// - private static bool HasNoRetryPolicy(HttpRequestMessage request) + private static bool HasNoRetryPolicyAttribute(HttpRequestMessage request) { if (request.Options.TryGetValue(new(RequestOptionNames.IControllerType), out Type? controllerType) is false) return false; var parameterTypes = ((Dictionary)request.Options.GetValueOrDefault(RequestOptionNames.ActionParametersInfo)!).Select(p => p.Value).ToArray(); var method = controllerType!.GetMethod((string)request.Options.GetValueOrDefault(RequestOptionNames.ActionName)!, parameterTypes)!; - return method.GetCustomAttribute() is not null; + + return controllerType.GetCustomAttribute(inherit: true) is not null || + method.GetCustomAttribute() is not null; } private static IEnumerable GetDelaySequence(TimeSpan scaleFirstTry) diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Styles/app.scss b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Styles/app.scss index b850fa2043..8d709324f8 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Styles/app.scss +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Styles/app.scss @@ -67,6 +67,10 @@ p { } } +.max-width { + width: min(25rem, 100%); +} + div[dir=rtl] { .bitdatagrid-paginator { .go-next, .go-last { diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/appsettings.json b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/appsettings.json index c78c02f339..7691767137 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/appsettings.json +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/appsettings.json @@ -1,4 +1,4 @@ -{ +{ //#if (api == "Integrated") "ServerAddress": "http://localhost:5030/", //#endif diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Services/MauiExceptionHandler.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Services/MauiExceptionHandler.cs index aba5b8504f..d5ffa6e90f 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Services/MauiExceptionHandler.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Services/MauiExceptionHandler.cs @@ -7,7 +7,7 @@ namespace Boilerplate.Client.Maui.Services; /// Employing Microsoft.Extensions.Logging implementations (like Sentry.Extensions.Logging) will result in /// automatic exception logging due to the logger.LogError method call within the ExceptionHandlerBase class. /// -public partial class MauiExceptionHandler : ExceptionHandlerBase +public partial class MauiExceptionHandler : ClientExceptionHandlerBase { protected override void Handle(Exception exception, Dictionary parameters) { diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Services/MauiLocalHttpServer.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Services/MauiLocalHttpServer.cs index 79885729bc..a6e5e900f0 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Services/MauiLocalHttpServer.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Maui/Services/MauiLocalHttpServer.cs @@ -1,16 +1,15 @@ -using System.Net; +using EmbedIO; +using System.Net; using System.Net.Sockets; -using EmbedIO; using EmbedIO.Actions; using Boilerplate.Client.Core.Components; -using Microsoft.Extensions.Logging; namespace Boilerplate.Client.Maui.Services; public partial class MauiLocalHttpServer : ILocalHttpServer { - [AutoInject] private IConfiguration configuration; [AutoInject] private IExceptionHandler exceptionHandler; + [AutoInject] private AbsoluteServerAddressProvider absoluteServerAddress; private WebServer? localHttpServer; @@ -25,7 +24,7 @@ public int Start(CancellationToken cancellationToken) { try { - var url = $"{configuration.GetServerAddress()}/api/Identity/SocialSignedIn?culture={CultureInfo.CurrentUICulture.Name}"; + var url = new Uri(absoluteServerAddress, $"/api/Identity/SocialSignedIn?culture={CultureInfo.CurrentUICulture.Name}").ToString(); ctx.Redirect(url); diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Web/Services/WebExceptionHandler.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Web/Services/WebExceptionHandler.cs index 2b6f05695e..8e7dd2c635 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Web/Services/WebExceptionHandler.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Web/Services/WebExceptionHandler.cs @@ -1,6 +1,6 @@ namespace Boilerplate.Client.Web.Services; -public partial class WebExceptionHandler : ExceptionHandlerBase +public partial class WebExceptionHandler : ClientExceptionHandlerBase { protected override void Handle(Exception exception, Dictionary parameters) { diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Web/appsettings.json b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Web/appsettings.json index 5fe62e7f93..d2235cccdd 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Web/appsettings.json +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Web/appsettings.json @@ -1,6 +1,7 @@ { "WebAppRender": { "PrerenderEnabled": false, + "PrerenderEnabled_Comment": "for apps with Prerender enabled, follow the instructions in the service-worker.published.js file", "BlazorMode": "BlazorServer", "BlazorMode_Comment": "BlazorServer, BlazorWebAssembly and BlazorAuto. Default value of Client.Core/appsettings.Production.json is BlazorAuto" }, diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Web/wwwroot/service-worker.published.js b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Web/wwwroot/service-worker.published.js index 224a871b66..d31f7d76c9 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Web/wwwroot/service-worker.published.js +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Web/wwwroot/service-worker.published.js @@ -62,9 +62,15 @@ self.serverHandledUrls = [ ]; self.defaultUrl = "/"; -self.caseInsensitiveUrl = true; -self.noPrerenderQuery = 'no-prerender=true'; -self.isPassive = self.disablePassiveFirstBoot = true; +self.isPassive = true; self.errorTolerance = 'lax'; +self.caseInsensitiveUrl = true; + + +// on apps with Prerendering enabled, to have the best experience for the end user un-comment the following two lines. +// more info: https://bitplatform.dev/bswup/service-worker +// self.noPrerenderQuery = 'no-prerender=true'; +// self.disablePassiveFirstBoot = true; + self.importScripts('_content/Bit.Bswup/bit-bswup.sw.js'); \ No newline at end of file diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/Program.Services.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/Program.Services.cs index 0a9b133850..445b900fa5 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/Program.Services.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/Program.Services.cs @@ -42,10 +42,7 @@ public static void AddClientWindowsProjectServices(this IServiceCollection servi loggingBuilder.AddConfiguration(configuration.GetSection("Logging")); loggingBuilder.AddEventSourceLogger(); - if (AppPlatform.IsWindows) - { - loggingBuilder.AddEventLog(); - } + loggingBuilder.AddEventLog(); //#if (appCenter == true) if (Microsoft.AppCenter.AppCenter.Configured) { diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/Services/WindowsExceptionHandler.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/Services/WindowsExceptionHandler.cs index 03c98a0444..29f3aef7f1 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/Services/WindowsExceptionHandler.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/Services/WindowsExceptionHandler.cs @@ -6,7 +6,7 @@ /// Employing Microsoft.Extensions.Logging implementations (like Sentry.Extensions.Logging) will result in /// automatic exception logging due to the logger.LogError method call within the ExceptionHandlerBase class. /// -public partial class WindowsExceptionHandler : ExceptionHandlerBase +public partial class WindowsExceptionHandler : ClientExceptionHandlerBase { protected override void Handle(Exception exception, Dictionary parameters) { diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/Services/WindowsLocalHttpServer.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/Services/WindowsLocalHttpServer.cs index 5b5c46bb18..d82bca6e5d 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/Services/WindowsLocalHttpServer.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Windows/Services/WindowsLocalHttpServer.cs @@ -1,18 +1,16 @@ -using System.Net; +using EmbedIO; +using System.Net; using System.Net.Http; using System.Net.Sockets; -using EmbedIO; using EmbedIO.Actions; using Boilerplate.Client.Core.Components; -using Microsoft.Extensions.Logging; namespace Boilerplate.Client.Windows.Services; public partial class WindowsLocalHttpServer : ILocalHttpServer { - [AutoInject] private IConfiguration configuration; [AutoInject] private IExceptionHandler exceptionHandler; - [AutoInject] private ILogger logger = default!; + [AutoInject] private AbsoluteServerAddressProvider absoluteServerAddress; private WebServer? localHttpServer; @@ -27,7 +25,7 @@ public int Start(CancellationToken cancellationToken) { try { - var url = $"{configuration.GetServerAddress()}/api/Identity/SocialSignedIn?culture={CultureInfo.CurrentUICulture.Name}"; + var url = new Uri(absoluteServerAddress, $"/api/Identity/SocialSignedIn?culture={CultureInfo.CurrentUICulture.Name}").ToString(); ctx.Redirect(url); diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Directory.Packages8.props b/src/Templates/Boilerplate/Bit.Boilerplate/src/Directory.Packages8.props index 2830e932e9..99c904bbc2 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Directory.Packages8.props +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Directory.Packages8.props @@ -69,7 +69,7 @@ - + diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Categories/CategoryController.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Categories/CategoryController.cs index e2f39ea182..536fe678c6 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Categories/CategoryController.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Categories/CategoryController.cs @@ -6,7 +6,7 @@ using Boilerplate.Shared.Dtos.Categories; using Boilerplate.Shared.Controllers.Categories; -namespace Boilerplate.Server.Api.Controllers; +namespace Boilerplate.Server.Api.Controllers.Categories; [ApiController, Route("api/[controller]/[action]")] public partial class CategoryController : AppControllerBase, ICategoryController diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Dashboard/DashboardController.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Dashboard/DashboardController.cs index 7a2ac7701f..7895f782bc 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Dashboard/DashboardController.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Dashboard/DashboardController.cs @@ -1,7 +1,7 @@ using Boilerplate.Shared.Dtos.Dashboard; using Boilerplate.Shared.Controllers.Dashboard; -namespace Boilerplate.Server.Api.Controllers; +namespace Boilerplate.Server.Api.Controllers.Dashboard; [ApiController, Route("api/[controller]/[action]")] public partial class DashboardController : AppControllerBase, IDashboardController diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Products/ProductController.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Products/ProductController.cs index be48987b41..b455070c9b 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Products/ProductController.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Products/ProductController.cs @@ -4,9 +4,9 @@ using Microsoft.AspNetCore.SignalR; //#endif using Boilerplate.Shared.Dtos.Products; -using Boilerplate.Shared.Controllers.Product; +using Boilerplate.Shared.Controllers.Products; -namespace Boilerplate.Server.Api.Controllers; +namespace Boilerplate.Server.Api.Controllers.Products; [ApiController, Route("api/[controller]/[action]")] public partial class ProductController : AppControllerBase, IProductController diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Statistics/StatisticsController.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Statistics/StatisticsController.cs new file mode 100644 index 0000000000..3cb3323f97 --- /dev/null +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Controllers/Statistics/StatisticsController.cs @@ -0,0 +1,19 @@ +using Boilerplate.Server.Api.Services; +using Boilerplate.Shared.Controllers.Statistics; +using Boilerplate.Shared.Dtos.Statistics; + +namespace Boilerplate.Server.Api.Controllers.Statistics; + +[ApiController, Route("api/[controller]/[action]")] +public partial class StatisticsController : AppControllerBase, IStatisticsController +{ + [AutoInject] private NugetStatisticsHttpClient nugetHttpClient = default!; + + [AllowAnonymous] + [HttpGet("{packageId}")] + public async Task GetNugetStats(string packageId, CancellationToken cancellationToken) + { + var stats = await nugetHttpClient.GetPackageStats(packageId, cancellationToken); + return stats; + } +} diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Program.Services.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Program.Services.cs index 408d9471ce..c2165f0c70 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Program.Services.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Program.Services.cs @@ -131,11 +131,11 @@ public static void AddServerApiProjectServices(this WebApplicationBuilder builde services.AddAntiforgery(); - services.ConfigureHttpJsonOptions(options => options.SerializerOptions.TypeInfoResolverChain.AddRange([AppJsonContext.Default, IdentityJsonContext.Default])); + services.ConfigureHttpJsonOptions(options => options.SerializerOptions.TypeInfoResolverChain.AddRange([AppJsonContext.Default, IdentityJsonContext.Default, ServerJsonContext.Default])); services .AddControllers() - .AddJsonOptions(options => options.JsonSerializerOptions.TypeInfoResolverChain.AddRange([AppJsonContext.Default, IdentityJsonContext.Default])) + .AddJsonOptions(options => options.JsonSerializerOptions.TypeInfoResolverChain.AddRange([AppJsonContext.Default, IdentityJsonContext.Default, ServerJsonContext.Default])) //#if (api == "Integrated") .AddApplicationPart(typeof(AppControllerBase).Assembly) //#endif @@ -260,6 +260,11 @@ void AddDbContext(DbContextOptionsBuilder options) c.BaseAddress = new Uri("https://www.google.com/recaptcha/"); }); //#endif + + services.AddHttpClient(c => + { + c.BaseAddress = new Uri("https://azuresearch-usnc.nuget.org"); + }); } private static void AddIdentity(WebApplicationBuilder builder) diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Services/NugetStatisticsHttpClient.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Services/NugetStatisticsHttpClient.cs new file mode 100644 index 0000000000..22f870b34c --- /dev/null +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Services/NugetStatisticsHttpClient.cs @@ -0,0 +1,18 @@ +using Boilerplate.Shared.Dtos.Statistics; + +namespace Boilerplate.Server.Api.Services; + +public partial class NugetStatisticsHttpClient +{ + [AutoInject] protected HttpClient httpClient = default!; + + public virtual async ValueTask GetPackageStats(string packageId, CancellationToken cancellationToken) + { + var url = $"/query?q=packageid:{packageId}"; + + var response = await httpClient.GetFromJsonAsync(url, ServerJsonContext.Default.Options.GetTypeInfo(), cancellationToken) + ?? throw new ResourceNotFoundException(packageId); + + return response; + } +} diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Services/ServerExceptionHandler.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Services/ServerExceptionHandler.cs index 4cbea7ccfd..f0d31bcd1c 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Services/ServerExceptionHandler.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Services/ServerExceptionHandler.cs @@ -1,15 +1,13 @@ using System.Net; -using System.Reflection; using System.Diagnostics; using Microsoft.Net.Http.Headers; using Microsoft.AspNetCore.Diagnostics; namespace Boilerplate.Server.Api.Services; -public partial class ServerExceptionHandler : IExceptionHandler +public partial class ServerExceptionHandler : SharedExceptionHandler, IExceptionHandler { [AutoInject] private IWebHostEnvironment webHostEnvironment = default!; - [AutoInject] private IStringLocalizer localizer = default!; [AutoInject] private JsonSerializerOptions jsonSerializerOptions = default!; public async ValueTask TryHandleAsync(HttpContext httpContext, Exception e, CancellationToken cancellationToken) @@ -22,13 +20,13 @@ public async ValueTask TryHandleAsync(HttpContext httpContext, Exception e // The details of all of the exceptions are returned only in dev mode. in any other modes like production, only the details of the known exceptions are returned. var key = knownException?.Key ?? nameof(UnknownException); - var message = knownException?.Message ?? (webHostEnvironment.IsDevelopment() ? exception.Message : localizer[nameof(UnknownException)]); + var message = GetExceptionMessageToShow(exception); var statusCode = (int)(exception is RestException restExp ? restExp.StatusCode : HttpStatusCode.InternalServerError); if (exception is KnownException && message == key) { - message = localizer[message]; + message = Localizer[message]; } var restExceptionPayload = new RestErrorInfo @@ -49,18 +47,4 @@ public async ValueTask TryHandleAsync(HttpContext httpContext, Exception e return true; } - - private Exception UnWrapException(Exception exception) - { - if (exception is AggregateException aggregateException) - { - return aggregateException.Flatten().InnerException ?? aggregateException; - } - else if (exception is TargetInvocationException) - { - return exception.InnerException ?? exception; - } - - return exception; - } } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Services/ServerJsonContext.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Services/ServerJsonContext.cs index 801a336c29..6b02767d20 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Services/ServerJsonContext.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Api/Services/ServerJsonContext.cs @@ -1,11 +1,13 @@ //+:cnd:noEmit +using Boilerplate.Shared.Dtos.Statistics; + namespace Boilerplate.Server.Api.Services; /// /// https://devblogs.microsoft.com/dotnet/try-the-new-system-text-json-source-generator/ /// [JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)] -[JsonSerializable(typeof(Dictionary))] +[JsonSerializable(typeof(NugetStatsDto))] //#if (captcha == "reCaptcha") [JsonSerializable(typeof(GoogleRecaptchaVerificationResponse))] //#endif diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Web/Components/App.razor b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Web/Components/App.razor index 932a6ff164..923124f5e7 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Web/Components/App.razor +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Web/Components/App.razor @@ -62,7 +62,7 @@ @if (renderMode != null && (serverWebSettings.WebAppRender.PrerenderEnabled is false || noPrerender)) { - + } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Controllers/Attributes.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Controllers/Attributes.cs index 55a18795a7..1df3a6d0cf 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Controllers/Attributes.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Controllers/Attributes.cs @@ -38,10 +38,18 @@ internal partial class HttpPatchAttribute(string? template = null) : Attribute /// /// Avoid retrying the request upon failure. -/// /// -[AttributeUsage(AttributeTargets.Method)] +[AttributeUsage(AttributeTargets.Method | AttributeTargets.Interface)] public partial class NoRetryPolicyAttribute : Attribute { } + +/// +/// Avoid validating / using access token +/// +[AttributeUsage(AttributeTargets.Method | AttributeTargets.Interface)] +public partial class AnonymousApiAttribute : Attribute +{ + +} diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Controllers/IMinimalApiController.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Controllers/IMinimalApiController.cs index 3ce8c5b362..2db7f604c8 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Controllers/IMinimalApiController.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Controllers/IMinimalApiController.cs @@ -1,5 +1,6 @@ namespace Boilerplate.Shared.Controllers; +[AnonymousApi] public interface IMinimalApiController : IAppController { [HttpGet("api/minimal-api-sample/{routeParameter}{?queryStringParameter}")] diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Controllers/Identity/IIdentityController.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Controllers/Identity/IIdentityController.cs index 653e263da7..2baf087e88 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Controllers/Identity/IIdentityController.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Controllers/Identity/IIdentityController.cs @@ -3,7 +3,7 @@ namespace Boilerplate.Shared.Controllers.Identity; -[Route("api/[controller]/[action]/")] +[Route("api/[controller]/[action]/"), AnonymousApi] public interface IIdentityController : IAppController { [HttpPost] diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Controllers/Identity/IUserController.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Controllers/Identity/IUserController.cs index 0d915c8e2b..62774e30ed 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Controllers/Identity/IUserController.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Controllers/Identity/IUserController.cs @@ -11,7 +11,7 @@ public interface IUserController : IAppController [HttpGet] Task> GetUserSessions(CancellationToken cancellationToken); - [HttpPost] + [HttpPost, NoRetryPolicy] Task SignOut(CancellationToken cancellationToken); [HttpPost("{id}")] diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Controllers/Products/IProductController.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Controllers/Products/IProductController.cs index 669f71e376..037894064f 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Controllers/Products/IProductController.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Controllers/Products/IProductController.cs @@ -1,6 +1,6 @@ using Boilerplate.Shared.Dtos.Products; -namespace Boilerplate.Shared.Controllers.Product; +namespace Boilerplate.Shared.Controllers.Products; [Route("api/[controller]/[action]/")] public interface IProductController : IAppController diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Controllers/PushNotification/IPushNotificationController.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Controllers/PushNotification/IPushNotificationController.cs index f61ea747fe..4d4a58a1c3 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Controllers/PushNotification/IPushNotificationController.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Controllers/PushNotification/IPushNotificationController.cs @@ -2,7 +2,7 @@ namespace Boilerplate.Shared.Controllers.PushNotification; -[Route("api/[controller]/[action]/")] +[Route("api/[controller]/[action]/"), AnonymousApi] public interface IPushNotificationController : IAppController { [HttpPost] diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Controllers/Statistics/IStatisticsController.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Controllers/Statistics/IStatisticsController.cs new file mode 100644 index 0000000000..026fc1af4d --- /dev/null +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Controllers/Statistics/IStatisticsController.cs @@ -0,0 +1,13 @@ +using Boilerplate.Shared.Dtos.Statistics; + +namespace Boilerplate.Shared.Controllers.Statistics; + +[Route("api/[controller]/[action]/"), AnonymousApi] +public interface IStatisticsController : IAppController +{ + [HttpGet("{packageId}")] + Task GetNugetStats(string packageId, CancellationToken cancellationToken); + + [HttpGet, Route("https://api.github.com/repos/bitfoundation/bitplatform")] + Task GetGitHubStats(CancellationToken cancellationToken) => default!; +} diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Dtos/AppJsonContext.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Dtos/AppJsonContext.cs index 6ab5f7d91c..39d4b3a7ad 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Dtos/AppJsonContext.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Dtos/AppJsonContext.cs @@ -9,6 +9,7 @@ //#if (notification == true) using Boilerplate.Shared.Dtos.PushNotification; //#endif +using Boilerplate.Shared.Dtos.Statistics; namespace Boilerplate.Shared.Dtos; @@ -19,6 +20,8 @@ namespace Boilerplate.Shared.Dtos; [JsonSerializable(typeof(Dictionary))] [JsonSerializable(typeof(string[]))] [JsonSerializable(typeof(RestErrorInfo))] +[JsonSerializable(typeof(GitHubStats))] +[JsonSerializable(typeof(NugetStatsDto))] //#if (notification == true) [JsonSerializable(typeof(DeviceInstallationDto))] //#endif diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Dtos/Statistics/GitHubStats.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Dtos/Statistics/GitHubStats.cs new file mode 100644 index 0000000000..27903db9f0 --- /dev/null +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Dtos/Statistics/GitHubStats.cs @@ -0,0 +1,18 @@ +namespace Boilerplate.Shared.Dtos.Statistics; + +public record GitHubStats( + [property: JsonPropertyName("id")] int Id, + [property: JsonPropertyName("name")] string Name, + [property: JsonPropertyName("full_name")] string FullName, + [property: JsonPropertyName("description")] string Description, + [property: JsonPropertyName("homepage")] string Homepage, + [property: JsonPropertyName("size")] int Size, + [property: JsonPropertyName("stargazers_count")] int StargazersCount, + [property: JsonPropertyName("watchers_count")] int WatchersCount, + [property: JsonPropertyName("language")] string Language, + [property: JsonPropertyName("forks_count")] int ForksCount, + [property: JsonPropertyName("open_issues_count")] int OpenIssuesCount, + [property: JsonPropertyName("default_branch")] string DefaultBranch, + [property: JsonPropertyName("network_count")] int NetworkCount, + [property: JsonPropertyName("subscribers_count")] int SubscribersCount +); diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Dtos/Statistics/NugetStatsDto.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Dtos/Statistics/NugetStatsDto.cs new file mode 100644 index 0000000000..04d74ac268 --- /dev/null +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Dtos/Statistics/NugetStatsDto.cs @@ -0,0 +1,15 @@ +namespace Boilerplate.Shared.Dtos.Statistics; + +public record NugetStatsDto( + [property: JsonPropertyName("data")] IReadOnlyList Data +); + +public record Datum( + [property: JsonPropertyName("id")] string Id, + [property: JsonPropertyName("version")] string Version, + [property: JsonPropertyName("description")] string Description, + [property: JsonPropertyName("title")] string Title, + [property: JsonPropertyName("projectUrl")] string ProjectUrl, + [property: JsonPropertyName("totalDownloads")] int TotalDownloads +); + diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Services/SharedExceptionHandler.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Services/SharedExceptionHandler.cs new file mode 100644 index 0000000000..6a54ab1abb --- /dev/null +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/Services/SharedExceptionHandler.cs @@ -0,0 +1,58 @@ +using System.Reflection; + +namespace Boilerplate.Shared.Services; + +public partial class SharedExceptionHandler +{ + [AutoInject] protected IStringLocalizer Localizer { get; set; } = default!; + + protected string GetExceptionMessageToShow(Exception exception) + { + if (exception is KnownException) + return exception.Message; + + if (AppEnvironment.IsDev()) + return exception.ToString(); + + return Localizer[nameof(AppStrings.UnknownException)]; + } + + protected string GetExceptionMessageToLog(Exception exception) + { + var exceptionMessageToLog = exception.Message; + var innerException = exception.InnerException; + + while (innerException is not null) + { + exceptionMessageToLog += $"{Environment.NewLine}{innerException.Message}"; + innerException = innerException.InnerException; + } + + return exceptionMessageToLog; + } + + protected Exception UnWrapException(Exception exception) + { + if (exception is AggregateException aggregateException) + { + return aggregateException.Flatten().InnerException ?? aggregateException; + } + else if (exception is TargetInvocationException) + { + return exception.InnerException ?? exception; + } + + return exception; + } + + protected bool IgnoreException(Exception exception) + { + if (exception is KnownException) + return false; + + return exception is TaskCanceledException || + exception is OperationCanceledException || + exception is TimeoutException || + (exception.InnerException is not null && IgnoreException(exception.InnerException)); + } +} diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/appsettings.Development.json b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/appsettings.Development.json index d2a86e4696..f57da75893 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/appsettings.Development.json +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/appsettings.Development.json @@ -1,65 +1,72 @@ -{ +{ "Logging": { - "Console": { + //#if (appInsights == true) + "ApplicationInsights": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning" }, "IncludeScopes": true }, - "EventLog": { + "ApplicationInsightsLoggerProvider": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning" }, "IncludeScopes": true }, - "EventSource": { + //#endif + //#if (appCenter == true) + "AppCenterLoggerProvider": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning" }, "IncludeScopes": true }, - "DiagnosticLogger": { + //#endif + "Console": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning" }, "IncludeScopes": true }, - "Debug": { + "WebAssemblyConsoleLoggerProvider": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning" }, "IncludeScopes": true }, - //#if (appInsights == true) - "ApplicationInsights": { + "EventLog": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning" }, - "IncludeScopes": false + "IncludeScopes": true }, - "ApplicationInsightsLoggerProvider": { + "EventSource": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning" }, - "IncludeScopes": false + "IncludeScopes": true }, - //#endif - //#if (appCenter == true) - "AppCenterLoggerProvider": { + "DiagnosticLogger": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + }, + "IncludeScopes": true + }, + "Debug": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning" }, - "IncludeScopes": false + "IncludeScopes": true } - //#endif }, "$schema": "https://json.schemastore.org/appsettings.json" } \ No newline at end of file diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/appsettings.json b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/appsettings.json index f7313af1be..2ecf726374 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/appsettings.json +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Shared/appsettings.json @@ -1,4 +1,4 @@ -{ +{ "WebClientUrl": null, "WebClientUrl_Comment": "If you are hosting the API and web client on different URLs (e.g., api.company.com and app.company.com), you must set `WebClientUrl` to your web client's address. This ensures that the API server redirects to the correct URL after social sign-ins and other similar actions.", //#if (appInsights == true) @@ -42,6 +42,13 @@ }, "IncludeScopes": true }, + "WebAssemblyConsoleLoggerProvider": { + "LogLevel": { + "Default": "Warning", + "Microsoft.EntityFrameworkCore.Database.Command": "Information" + }, + "IncludeScopes": true + }, "EventLog": { "LogLevel": { "Default": "Warning",