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
-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",