├── .dockerignore ├── .editorconfig ├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ └── dotnet.yml ├── .gitignore ├── .template.config ├── icon.png ├── ide.host.json └── template.json ├── .vscode ├── launch.json ├── settings.json └── tasks.json ├── Directory.Build.props ├── Directory.Build.targets ├── Dockerfile ├── FSH.BlazorWebAssembly.nuspec ├── FSH.BlazorWebAssembly.sln ├── LICENSE ├── README.md ├── dotnet.ruleset ├── global.json ├── icon.png ├── scripts ├── nswag-regen.ps1 └── pull-shared-from-webapi.ps1 ├── src ├── Client.Infrastructure │ ├── ApiClient │ │ ├── FSHApi.cs │ │ ├── IApiService.cs │ │ └── nswag.json │ ├── Auth │ │ ├── AuthProvider.cs │ │ ├── AuthorizationServiceExtensions.cs │ │ ├── AzureAd │ │ │ ├── AzureAdAuthenticationService.cs │ │ │ ├── AzureAdAuthorizationMessageHandler.cs │ │ │ └── AzureAdClaimsPrincipalFactory.cs │ │ ├── IAuthenticationService.cs │ │ ├── Jwt │ │ │ ├── AccessTokenProviderAccessor.cs │ │ │ ├── JwtAuthenticationHeaderHandler.cs │ │ │ └── JwtAuthenticationService.cs │ │ ├── MustHavePermissionAttribute.cs │ │ └── Startup.cs │ ├── Client.Infrastructure.csproj │ ├── Common │ │ ├── AccessTokenProviderExtensions.cs │ │ ├── ApplicationConstants.cs │ │ ├── ConfigNames.cs │ │ ├── IAppService.cs │ │ ├── LocalizationConstants.cs │ │ └── StorageConstants.cs │ ├── GlobalUsings.cs │ ├── Notifications │ │ ├── ConnectionState.cs │ │ ├── ConnectionStateChanged.cs │ │ ├── INotificationPublisher.cs │ │ ├── NotificationPublisher.cs │ │ ├── NotificationWrapper.cs │ │ └── Startup.cs │ ├── Preferences │ │ ├── ClientPreference.cs │ │ ├── ClientPreferenceManager.cs │ │ ├── FshTablePreference.cs │ │ ├── IClientPreferenceManager.cs │ │ ├── IPreference.cs │ │ └── IPreferenceManager.cs │ ├── Startup.cs │ └── Theme │ │ ├── CustomColors.cs │ │ ├── CustomTypography.cs │ │ ├── DarkTheme.cs │ │ └── LightTheme.cs ├── Client │ ├── App.razor │ ├── Client.csproj │ ├── Components │ │ ├── Common │ │ │ ├── CustomValidation.cs │ │ │ ├── ErrorHandler.razor │ │ │ ├── ErrorHandler.razor.cs │ │ │ ├── FshCustomError.razor │ │ │ ├── FshTable.cs │ │ │ ├── FshTitle.razor │ │ │ ├── PersonCard.razor │ │ │ ├── PersonCard.razor.cs │ │ │ └── TablePager.razor │ │ ├── Dialogs │ │ │ ├── DeleteConfirmation.razor │ │ │ └── Logout.razor │ │ ├── EntityTable │ │ │ ├── AddEditModal.razor │ │ │ ├── AddEditModal.razor.cs │ │ │ ├── EntityClientTableContext.cs │ │ │ ├── EntityField.cs │ │ │ ├── EntityServerTableContext.cs │ │ │ ├── EntityTable.razor │ │ │ ├── EntityTable.razor.cs │ │ │ ├── EntityTableContext.cs │ │ │ ├── IAddEditModal.cs │ │ │ └── PaginationResponse.cs │ │ ├── Localization │ │ │ └── LanguageSelector.razor │ │ ├── Notifications │ │ │ ├── NotificationConnection.razor │ │ │ ├── NotificationConnection.razor.cs │ │ │ └── NotificationConnectionStatus.razor │ │ └── ThemeManager │ │ │ ├── ColorPanel.razor │ │ │ ├── ColorPanel.razor.cs │ │ │ ├── DarkModePanel.razor │ │ │ ├── DarkModePanel.razor.cs │ │ │ ├── RadiusPanel.razor │ │ │ ├── RadiusPanel.razor.cs │ │ │ ├── TableCustomizationPanel.razor │ │ │ ├── TableCustomizationPanel.razor.cs │ │ │ ├── ThemeButton.razor │ │ │ ├── ThemeButton.razor.cs │ │ │ ├── ThemeDrawer.razor │ │ │ └── ThemeDrawer.razor.cs │ ├── Pages │ │ ├── Authentication │ │ │ ├── Authentication.razor │ │ │ ├── ForgotPassword.razor │ │ │ ├── ForgotPassword.razor.cs │ │ │ ├── Login.razor │ │ │ ├── Login.razor.cs │ │ │ ├── SelfRegister.razor │ │ │ └── SelfRegister.razor.cs │ │ ├── Catalog │ │ │ ├── BrandAutocomplete.cs │ │ │ ├── Brands.razor │ │ │ ├── Products.razor │ │ │ └── Products.razor.cs │ │ ├── Identity │ │ │ ├── Account │ │ │ │ ├── Account.razor │ │ │ │ ├── Profile.razor │ │ │ │ ├── Profile.razor.cs │ │ │ │ ├── Security.razor │ │ │ │ └── Security.razor.cs │ │ │ ├── Roles │ │ │ │ ├── RolePermissions.razor │ │ │ │ ├── RolePermissions.razor.cs │ │ │ │ ├── Roles.razor │ │ │ │ └── Roles.razor.cs │ │ │ └── Users │ │ │ │ ├── UserProfile.razor │ │ │ │ ├── UserProfile.razor.cs │ │ │ │ ├── UserRoles.razor │ │ │ │ ├── UserRoles.razor.cs │ │ │ │ ├── Users.razor │ │ │ │ └── Users.razor.cs │ │ ├── Index.razor │ │ ├── Multitenancy │ │ │ ├── Tenants.razor │ │ │ ├── Tenants.razor.cs │ │ │ └── UpgradeSubscriptionModal.razor │ │ └── Personal │ │ │ ├── AuditLogs.razor │ │ │ ├── AuditLogs.razor.cs │ │ │ ├── Dashboard.razor │ │ │ └── Dashboard.razor.cs │ ├── Program.cs │ ├── Properties │ │ └── launchSettings.json │ ├── Resources │ │ ├── Components │ │ │ └── Common │ │ │ │ ├── TablePager.ar.resx │ │ │ │ ├── TablePager.de.resx │ │ │ │ ├── TablePager.en.resx │ │ │ │ ├── TablePager.es.resx │ │ │ │ ├── TablePager.fr.resx │ │ │ │ ├── TablePager.id.resx │ │ │ │ ├── TablePager.it.resx │ │ │ │ ├── TablePager.km.resx │ │ │ │ ├── TablePager.nl.resx │ │ │ │ ├── TablePager.pt.resx │ │ │ │ ├── TablePager.ru.resx │ │ │ │ └── TablePager.sv.resx │ │ ├── Pages │ │ │ ├── Account │ │ │ │ ├── Register.ar.resx │ │ │ │ ├── Register.de.resx │ │ │ │ ├── Register.en.resx │ │ │ │ ├── Register.es.resx │ │ │ │ ├── Register.fr.resx │ │ │ │ ├── Register.id.resx │ │ │ │ ├── Register.it.resx │ │ │ │ ├── Register.km.resx │ │ │ │ ├── Register.nl.resx │ │ │ │ ├── Register.pt.resx │ │ │ │ ├── Register.ru.resx │ │ │ │ ├── Register.sv.resx │ │ │ │ ├── Reset.ar.resx │ │ │ │ ├── Reset.de.resx │ │ │ │ ├── Reset.en.resx │ │ │ │ ├── Reset.es.resx │ │ │ │ ├── Reset.fr.resx │ │ │ │ ├── Reset.id.resx │ │ │ │ ├── Reset.it.resx │ │ │ │ ├── Reset.km.resx │ │ │ │ ├── Reset.nl.resx │ │ │ │ ├── Reset.pt.resx │ │ │ │ ├── Reset.ru.resx │ │ │ │ └── Reset.sv.resx │ │ │ ├── Authentication │ │ │ │ ├── Authentication.ar.resx │ │ │ │ ├── Authentication.de.resx │ │ │ │ ├── Authentication.en.resx │ │ │ │ ├── Authentication.es.resx │ │ │ │ ├── Authentication.fr.resx │ │ │ │ ├── Authentication.id.resx │ │ │ │ ├── Authentication.it.resx │ │ │ │ ├── Authentication.km.resx │ │ │ │ ├── Authentication.nl.resx │ │ │ │ ├── Authentication.pt.resx │ │ │ │ ├── Authentication.ru.resx │ │ │ │ ├── Authentication.sv.resx │ │ │ │ ├── ForgotPassword.ar.resx │ │ │ │ ├── ForgotPassword.de.resx │ │ │ │ ├── ForgotPassword.en.resx │ │ │ │ ├── ForgotPassword.es.resx │ │ │ │ ├── ForgotPassword.fr.resx │ │ │ │ ├── ForgotPassword.id.resx │ │ │ │ ├── ForgotPassword.it.resx │ │ │ │ ├── ForgotPassword.km.resx │ │ │ │ ├── ForgotPassword.nl.resx │ │ │ │ ├── ForgotPassword.pt.resx │ │ │ │ ├── ForgotPassword.ru.resx │ │ │ │ ├── ForgotPassword.sv.resx │ │ │ │ ├── Login.ar.resx │ │ │ │ ├── Login.de.resx │ │ │ │ ├── Login.en.resx │ │ │ │ ├── Login.es.resx │ │ │ │ ├── Login.fr.resx │ │ │ │ ├── Login.id.resx │ │ │ │ ├── Login.it.resx │ │ │ │ ├── Login.km.resx │ │ │ │ ├── Login.nl.resx │ │ │ │ ├── Login.pt.resx │ │ │ │ ├── Login.ru.resx │ │ │ │ ├── Login.sv.resx │ │ │ │ ├── Register.ar.resx │ │ │ │ ├── Register.de.resx │ │ │ │ ├── Register.en.resx │ │ │ │ ├── Register.es.resx │ │ │ │ ├── Register.fr.resx │ │ │ │ ├── Register.id.resx │ │ │ │ ├── Register.it.resx │ │ │ │ ├── Register.km.resx │ │ │ │ ├── Register.nl.resx │ │ │ │ ├── Register.pt.resx │ │ │ │ ├── Register.ru.resx │ │ │ │ └── Register.sv.resx │ │ │ ├── Catalog │ │ │ │ ├── AddEditBrandModal.ar.resx │ │ │ │ ├── AddEditBrandModal.de.resx │ │ │ │ ├── AddEditBrandModal.en.resx │ │ │ │ ├── AddEditBrandModal.es.resx │ │ │ │ ├── AddEditBrandModal.fr.resx │ │ │ │ ├── AddEditBrandModal.id.resx │ │ │ │ ├── AddEditBrandModal.it.resx │ │ │ │ ├── AddEditBrandModal.km.resx │ │ │ │ ├── AddEditBrandModal.nl.resx │ │ │ │ ├── AddEditBrandModal.pt.resx │ │ │ │ ├── AddEditBrandModal.ru.resx │ │ │ │ ├── AddEditProductModal.ar.resx │ │ │ │ ├── AddEditProductModal.de.resx │ │ │ │ ├── AddEditProductModal.en.resx │ │ │ │ ├── AddEditProductModal.es.resx │ │ │ │ ├── AddEditProductModal.fr.resx │ │ │ │ ├── AddEditProductModal.id.resx │ │ │ │ ├── AddEditProductModal.it.resx │ │ │ │ ├── AddEditProductModal.km.resx │ │ │ │ ├── AddEditProductModal.nl.resx │ │ │ │ ├── AddEditProductModal.pt.resx │ │ │ │ ├── AddEditProductModal.ru.resx │ │ │ │ ├── AddEditProductModal.sv.resx │ │ │ │ ├── Brands.ar.resx │ │ │ │ ├── Brands.de.resx │ │ │ │ ├── Brands.en.resx │ │ │ │ ├── Brands.es.resx │ │ │ │ ├── Brands.fr.resx │ │ │ │ ├── Brands.id.resx │ │ │ │ ├── Brands.it.resx │ │ │ │ ├── Brands.km.resx │ │ │ │ ├── Brands.nl.resx │ │ │ │ ├── Brands.pt.resx │ │ │ │ ├── Brands.ru.resx │ │ │ │ ├── Brands.sv.resx │ │ │ │ ├── Products.ar.resx │ │ │ │ ├── Products.de.resx │ │ │ │ ├── Products.en.resx │ │ │ │ ├── Products.es.resx │ │ │ │ ├── Products.fr.resx │ │ │ │ ├── Products.id.resx │ │ │ │ ├── Products.it.resx │ │ │ │ ├── Products.km.resx │ │ │ │ ├── Products.nl.resx │ │ │ │ ├── Products.pt.resx │ │ │ │ ├── Products.ru.resx │ │ │ │ └── Products.sv.resx │ │ │ ├── Communication │ │ │ │ ├── Chat.ar.resx │ │ │ │ ├── Chat.de.resx │ │ │ │ ├── Chat.en.resx │ │ │ │ ├── Chat.es.resx │ │ │ │ ├── Chat.fr.resx │ │ │ │ ├── Chat.id.resx │ │ │ │ ├── Chat.it.resx │ │ │ │ ├── Chat.km.resx │ │ │ │ ├── Chat.nl.resx │ │ │ │ ├── Chat.pt.resx │ │ │ │ ├── Chat.ru.resx │ │ │ │ └── Chat.sv.resx │ │ │ ├── Content │ │ │ │ ├── Home.ar.resx │ │ │ │ ├── Home.de.resx │ │ │ │ ├── Home.en.resx │ │ │ │ ├── Home.es.resx │ │ │ │ ├── Home.fr.resx │ │ │ │ ├── Home.id.resx │ │ │ │ ├── Home.it.resx │ │ │ │ ├── Home.km.resx │ │ │ │ ├── Home.nl.resx │ │ │ │ ├── Home.pt.resx │ │ │ │ ├── Home.ru.resx │ │ │ │ ├── Home.sv.resx │ │ │ │ ├── Resources.ar.resx │ │ │ │ ├── Resources.de.resx │ │ │ │ ├── Resources.en.resx │ │ │ │ ├── Resources.es.resx │ │ │ │ ├── Resources.fr.resx │ │ │ │ ├── Resources.id.resx │ │ │ │ ├── Resources.it.resx │ │ │ │ ├── Resources.km.resx │ │ │ │ ├── Resources.nl.resx │ │ │ │ ├── Resources.pt.resx │ │ │ │ ├── Resources.ru.resx │ │ │ │ └── Resources.sv.resx │ │ │ ├── Identity │ │ │ │ ├── Account │ │ │ │ │ ├── Account.ar.resx │ │ │ │ │ ├── Account.de.resx │ │ │ │ │ ├── Account.en.resx │ │ │ │ │ ├── Account.es.resx │ │ │ │ │ ├── Account.fr.resx │ │ │ │ │ ├── Account.id.resx │ │ │ │ │ ├── Account.it.resx │ │ │ │ │ ├── Account.km.resx │ │ │ │ │ ├── Account.nl.resx │ │ │ │ │ ├── Account.pt.resx │ │ │ │ │ ├── Account.ru.resx │ │ │ │ │ ├── Account.sv.resx │ │ │ │ │ ├── Profile.ar.resx │ │ │ │ │ ├── Profile.de.resx │ │ │ │ │ ├── Profile.en.resx │ │ │ │ │ ├── Profile.es.resx │ │ │ │ │ ├── Profile.fr.resx │ │ │ │ │ ├── Profile.id.resx │ │ │ │ │ ├── Profile.it.resx │ │ │ │ │ ├── Profile.km.resx │ │ │ │ │ ├── Profile.nl.resx │ │ │ │ │ ├── Profile.pt.resx │ │ │ │ │ ├── Profile.ru.resx │ │ │ │ │ ├── Profile.sv.resx │ │ │ │ │ ├── Security.ar.resx │ │ │ │ │ ├── Security.de.resx │ │ │ │ │ ├── Security.en.resx │ │ │ │ │ ├── Security.es.resx │ │ │ │ │ ├── Security.fr.resx │ │ │ │ │ ├── Security.id.resx │ │ │ │ │ ├── Security.it.resx │ │ │ │ │ ├── Security.km.resx │ │ │ │ │ ├── Security.nl.resx │ │ │ │ │ ├── Security.pt.resx │ │ │ │ │ ├── Security.ru.resx │ │ │ │ │ └── Security.sv.resx │ │ │ │ ├── RegisterUserModal.ar.resx │ │ │ │ ├── RegisterUserModal.de.resx │ │ │ │ ├── RegisterUserModal.en.resx │ │ │ │ ├── RegisterUserModal.es.resx │ │ │ │ ├── RegisterUserModal.fr.resx │ │ │ │ ├── RegisterUserModal.id.resx │ │ │ │ ├── RegisterUserModal.it.resx │ │ │ │ ├── RegisterUserModal.km.resx │ │ │ │ ├── RegisterUserModal.nl.resx │ │ │ │ ├── RegisterUserModal.pt.resx │ │ │ │ ├── RegisterUserModal.ru.resx │ │ │ │ ├── RegisterUserModal.sv.resx │ │ │ │ ├── Reset.ar.resx │ │ │ │ ├── Reset.de.resx │ │ │ │ ├── Reset.en.resx │ │ │ │ ├── Reset.es.resx │ │ │ │ ├── Reset.fr.resx │ │ │ │ ├── Reset.id.resx │ │ │ │ ├── Reset.it.resx │ │ │ │ ├── Reset.km.resx │ │ │ │ ├── Reset.nl.resx │ │ │ │ ├── Reset.pt.resx │ │ │ │ ├── Reset.ru.resx │ │ │ │ ├── Reset.sv.resx │ │ │ │ ├── RoleModal.ar.resx │ │ │ │ ├── RoleModal.de.resx │ │ │ │ ├── RoleModal.en.resx │ │ │ │ ├── RoleModal.es.resx │ │ │ │ ├── RoleModal.fr.resx │ │ │ │ ├── RoleModal.id.resx │ │ │ │ ├── RoleModal.it.resx │ │ │ │ ├── RoleModal.km.resx │ │ │ │ ├── RoleModal.nl.resx │ │ │ │ ├── RoleModal.pt.resx │ │ │ │ ├── RoleModal.ru.resx │ │ │ │ ├── RoleModal.sv.resx │ │ │ │ ├── Roles │ │ │ │ │ ├── RolePermissions.ar.resx │ │ │ │ │ ├── RolePermissions.de.resx │ │ │ │ │ ├── RolePermissions.en.resx │ │ │ │ │ ├── RolePermissions.es.resx │ │ │ │ │ ├── RolePermissions.fr.resx │ │ │ │ │ ├── RolePermissions.id.resx │ │ │ │ │ ├── RolePermissions.it.resx │ │ │ │ │ ├── RolePermissions.km.resx │ │ │ │ │ ├── RolePermissions.nl.resx │ │ │ │ │ ├── RolePermissions.pt.resx │ │ │ │ │ ├── RolePermissions.ru.resx │ │ │ │ │ ├── RolePermissions.sv.resx │ │ │ │ │ ├── Roles.ar.resx │ │ │ │ │ ├── Roles.de.resx │ │ │ │ │ ├── Roles.en.resx │ │ │ │ │ ├── Roles.es.resx │ │ │ │ │ ├── Roles.fr.resx │ │ │ │ │ ├── Roles.id.resx │ │ │ │ │ ├── Roles.it.resx │ │ │ │ │ ├── Roles.km.resx │ │ │ │ │ ├── Roles.nl.resx │ │ │ │ │ ├── Roles.pt.resx │ │ │ │ │ ├── Roles.ru.resx │ │ │ │ │ └── Roles.sv.resx │ │ │ │ └── Users │ │ │ │ │ ├── UserProfile.ar.resx │ │ │ │ │ ├── UserProfile.de.resx │ │ │ │ │ ├── UserProfile.en.resx │ │ │ │ │ ├── UserProfile.es.resx │ │ │ │ │ ├── UserProfile.fr.resx │ │ │ │ │ ├── UserProfile.id.resx │ │ │ │ │ ├── UserProfile.it.resx │ │ │ │ │ ├── UserProfile.km.resx │ │ │ │ │ ├── UserProfile.nl.resx │ │ │ │ │ ├── UserProfile.pt.resx │ │ │ │ │ ├── UserProfile.ru.resx │ │ │ │ │ ├── UserProfile.sv.resx │ │ │ │ │ ├── UserRoles.ar.resx │ │ │ │ │ ├── UserRoles.de.resx │ │ │ │ │ ├── UserRoles.en.resx │ │ │ │ │ ├── UserRoles.es.resx │ │ │ │ │ ├── UserRoles.fr.resx │ │ │ │ │ ├── UserRoles.id.resx │ │ │ │ │ ├── UserRoles.it.resx │ │ │ │ │ ├── UserRoles.km.resx │ │ │ │ │ ├── UserRoles.nl.resx │ │ │ │ │ ├── UserRoles.pt.resx │ │ │ │ │ ├── UserRoles.ru.resx │ │ │ │ │ ├── UserRoles.sv.resx │ │ │ │ │ ├── Users.ar.resx │ │ │ │ │ ├── Users.de.resx │ │ │ │ │ ├── Users.en.resx │ │ │ │ │ ├── Users.es.resx │ │ │ │ │ ├── Users.fr.resx │ │ │ │ │ ├── Users.id.resx │ │ │ │ │ ├── Users.it.resx │ │ │ │ │ ├── Users.km.resx │ │ │ │ │ ├── Users.nl.resx │ │ │ │ │ ├── Users.pt.resx │ │ │ │ │ ├── Users.ru.resx │ │ │ │ │ └── Users.sv.resx │ │ │ ├── Index.de.resx │ │ │ ├── Index.en.resx │ │ │ ├── Index.it.resx │ │ │ ├── Index.pt.resx │ │ │ ├── Misc │ │ │ │ ├── AddEditDocumentModal.ar.resx │ │ │ │ ├── AddEditDocumentModal.de.resx │ │ │ │ ├── AddEditDocumentModal.en.resx │ │ │ │ ├── AddEditDocumentModal.es.resx │ │ │ │ ├── AddEditDocumentModal.fr.resx │ │ │ │ ├── AddEditDocumentModal.id.resx │ │ │ │ ├── AddEditDocumentModal.it.resx │ │ │ │ ├── AddEditDocumentModal.km.resx │ │ │ │ ├── AddEditDocumentModal.nl.resx │ │ │ │ ├── AddEditDocumentModal.pt.resx │ │ │ │ ├── AddEditDocumentModal.ru.resx │ │ │ │ ├── AddEditDocumentModal.sv.resx │ │ │ │ ├── AddEditDocumentTypeModal.ar.resx │ │ │ │ ├── AddEditDocumentTypeModal.de.resx │ │ │ │ ├── AddEditDocumentTypeModal.en.resx │ │ │ │ ├── AddEditDocumentTypeModal.es.resx │ │ │ │ ├── AddEditDocumentTypeModal.fr.resx │ │ │ │ ├── AddEditDocumentTypeModal.km.resx │ │ │ │ ├── AddEditDocumentTypeModal.nl.resx │ │ │ │ ├── AddEditDocumentTypeModal.pt.resx │ │ │ │ ├── AddEditDocumentTypeModal.ru.resx │ │ │ │ ├── DocumentStore.ar.resx │ │ │ │ ├── DocumentStore.de.resx │ │ │ │ ├── DocumentStore.en.resx │ │ │ │ ├── DocumentStore.es.resx │ │ │ │ ├── DocumentStore.fr.resx │ │ │ │ ├── DocumentStore.id.resx │ │ │ │ ├── DocumentStore.it.resx │ │ │ │ ├── DocumentStore.km.resx │ │ │ │ ├── DocumentStore.nl.resx │ │ │ │ ├── DocumentStore.pt.resx │ │ │ │ ├── DocumentStore.ru.resx │ │ │ │ ├── DocumentStore.sv.resx │ │ │ │ ├── DocumentTypes.ar.resx │ │ │ │ ├── DocumentTypes.de.resx │ │ │ │ ├── DocumentTypes.en.resx │ │ │ │ ├── DocumentTypes.es.resx │ │ │ │ ├── DocumentTypes.fr.resx │ │ │ │ ├── DocumentTypes.km.resx │ │ │ │ ├── DocumentTypes.nl.resx │ │ │ │ ├── DocumentTypes.pt.resx │ │ │ │ └── DocumentTypes.ru.resx │ │ │ ├── Multitenancy │ │ │ │ ├── Tenants.de.resx │ │ │ │ ├── Tenants.en.resx │ │ │ │ ├── Tenants.it.resx │ │ │ │ ├── Tenants.pt.resx │ │ │ │ ├── UpgradeSubscriptionModal.de.resx │ │ │ │ ├── UpgradeSubscriptionModal.en.resx │ │ │ │ ├── UpgradeSubscriptionModal.it.resx │ │ │ │ └── UpgradeSubscriptionModal.pt.resx │ │ │ └── Personal │ │ │ │ ├── AuditLogs.ar.resx │ │ │ │ ├── AuditLogs.de.resx │ │ │ │ ├── AuditLogs.en.resx │ │ │ │ ├── AuditLogs.es.resx │ │ │ │ ├── AuditLogs.fr.resx │ │ │ │ ├── AuditLogs.it.resx │ │ │ │ ├── AuditLogs.km.resx │ │ │ │ ├── AuditLogs.nl.resx │ │ │ │ ├── AuditLogs.pt.resx │ │ │ │ ├── AuditLogs.ru.resx │ │ │ │ ├── AuditLogs.sv.resx │ │ │ │ ├── Dashboard.ar.resx │ │ │ │ ├── Dashboard.de.resx │ │ │ │ ├── Dashboard.en.resx │ │ │ │ ├── Dashboard.es.resx │ │ │ │ ├── Dashboard.fr.resx │ │ │ │ ├── Dashboard.id.resx │ │ │ │ ├── Dashboard.it.resx │ │ │ │ ├── Dashboard.km.resx │ │ │ │ ├── Dashboard.nl.resx │ │ │ │ ├── Dashboard.pt.resx │ │ │ │ ├── Dashboard.ru.resx │ │ │ │ └── Dashboard.sv.resx │ │ └── Shared │ │ │ ├── Components │ │ │ ├── AddEditExtendedAttributeModalLocalization.ar.resx │ │ │ ├── AddEditExtendedAttributeModalLocalization.de.resx │ │ │ ├── AddEditExtendedAttributeModalLocalization.en.resx │ │ │ ├── AddEditExtendedAttributeModalLocalization.es.resx │ │ │ ├── AddEditExtendedAttributeModalLocalization.fr.resx │ │ │ ├── AddEditExtendedAttributeModalLocalization.id.resx │ │ │ ├── AddEditExtendedAttributeModalLocalization.it.resx │ │ │ ├── AddEditExtendedAttributeModalLocalization.km.resx │ │ │ ├── AddEditExtendedAttributeModalLocalization.nl.resx │ │ │ ├── AddEditExtendedAttributeModalLocalization.pt.resx │ │ │ ├── AddEditExtendedAttributeModalLocalization.ru.resx │ │ │ ├── AddEditExtendedAttributeModalLocalization.sv.resx │ │ │ ├── ExtendedAttributesLocalization.ar.resx │ │ │ ├── ExtendedAttributesLocalization.de.resx │ │ │ ├── ExtendedAttributesLocalization.en.resx │ │ │ ├── ExtendedAttributesLocalization.es.resx │ │ │ ├── ExtendedAttributesLocalization.fr.resx │ │ │ ├── ExtendedAttributesLocalization.id.resx │ │ │ ├── ExtendedAttributesLocalization.it.resx │ │ │ ├── ExtendedAttributesLocalization.km.resx │ │ │ ├── ExtendedAttributesLocalization.nl.resx │ │ │ ├── ExtendedAttributesLocalization.pt.resx │ │ │ ├── ExtendedAttributesLocalization.ru.resx │ │ │ ├── ExtendedAttributesLocalization.sv.resx │ │ │ ├── ImportExcelModal.ar.resx │ │ │ ├── ImportExcelModal.de.resx │ │ │ ├── ImportExcelModal.en.resx │ │ │ ├── ImportExcelModal.es.resx │ │ │ ├── ImportExcelModal.fr.resx │ │ │ ├── ImportExcelModal.id.resx │ │ │ ├── ImportExcelModal.it.resx │ │ │ ├── ImportExcelModal.km.resx │ │ │ ├── ImportExcelModal.nl.resx │ │ │ ├── ImportExcelModal.pt.resx │ │ │ ├── ImportExcelModal.ru.resx │ │ │ ├── ImportExcelModal.sv.resx │ │ │ ├── LanguageSelector.ar.resx │ │ │ ├── LanguageSelector.de.resx │ │ │ ├── LanguageSelector.en.resx │ │ │ ├── LanguageSelector.es.resx │ │ │ ├── LanguageSelector.fr.resx │ │ │ ├── LanguageSelector.id.resx │ │ │ ├── LanguageSelector.it.resx │ │ │ ├── LanguageSelector.nl.resx │ │ │ ├── LanguageSelector.pt.resx │ │ │ └── LanguageSelector.ru.resx │ │ │ ├── Dialogs │ │ │ ├── DeleteConfirmation.en.resx │ │ │ ├── DeleteConfirmation.es.resx │ │ │ ├── DeleteConfirmation.fr.resx │ │ │ ├── DeleteConfirmation.id.resx │ │ │ ├── DeleteConfirmation.it.resx │ │ │ ├── DeleteConfirmation.km.resx │ │ │ ├── DeleteConfirmation.nl.resx │ │ │ ├── DeleteConfirmation.pt.resx │ │ │ ├── DeleteConfirmation.ru.resx │ │ │ ├── DeleteConfirmation.sv.resx │ │ │ ├── Logout.en.resx │ │ │ ├── Logout.es.resx │ │ │ ├── Logout.fr.resx │ │ │ ├── Logout.id.resx │ │ │ ├── Logout.it.resx │ │ │ ├── Logout.km.resx │ │ │ ├── Logout.nl.resx │ │ │ ├── Logout.pt.resx │ │ │ ├── Logout.ru.resx │ │ │ ├── Logout.sv.resx │ │ │ ├── ‏‏DeleteConfirmation.ar.resx │ │ │ └── ‏‏Logout.ar.resx │ │ │ ├── MainLayout.de.resx │ │ │ ├── MainLayout.en.resx │ │ │ ├── MainLayout.es.resx │ │ │ ├── MainLayout.fr.resx │ │ │ ├── MainLayout.id.resx │ │ │ ├── MainLayout.it.resx │ │ │ ├── MainLayout.km.resx │ │ │ ├── MainLayout.nl.resx │ │ │ ├── MainLayout.pt.resx │ │ │ ├── MainLayout.ru.resx │ │ │ ├── MainLayout.sv.resx │ │ │ ├── NavMenu.de.resx │ │ │ ├── NavMenu.en.resx │ │ │ ├── NavMenu.es.resx │ │ │ ├── NavMenu.fr.resx │ │ │ ├── NavMenu.id.resx │ │ │ ├── NavMenu.it.resx │ │ │ ├── NavMenu.km.resx │ │ │ ├── NavMenu.nl.resx │ │ │ ├── NavMenu.pt.resx │ │ │ ├── NavMenu.ru.resx │ │ │ ├── NavMenu.sv.resx │ │ │ ├── NotFoundLayout.de.resx │ │ │ ├── NotFoundLayout.en.resx │ │ │ ├── NotFoundLayout.es.resx │ │ │ ├── NotFoundLayout.fr.resx │ │ │ ├── NotFoundLayout.id.resx │ │ │ ├── NotFoundLayout.it.resx │ │ │ ├── NotFoundLayout.km.resx │ │ │ ├── NotFoundLayout.nl.resx │ │ │ ├── NotFoundLayout.pt.resx │ │ │ ├── NotFoundLayout.ru.resx │ │ │ ├── NotFoundLayout.sv.resx │ │ │ ├── SharedResource.de.resx │ │ │ ├── SharedResource.en.resx │ │ │ ├── SharedResource.it.resx │ │ │ ├── ‏‏MainLayout.ar.resx │ │ │ ├── ‏‏NavMenu.ar.resx │ │ │ └── ‏‏NotFoundLayout.ar.resx │ ├── Shared │ │ ├── ApiHelper.cs │ │ ├── BaseLayout.razor │ │ ├── BaseLayout.razor.cs │ │ ├── DialogServiceExtensions.cs │ │ ├── MainLayout.razor │ │ ├── MainLayout.razor.cs │ │ ├── NavMenu.razor │ │ ├── NavMenu.razor.cs │ │ ├── NotFound.razor │ │ └── SharedResource.cs │ ├── _Imports.razor │ └── wwwroot │ │ ├── appsettings.Development.json │ │ ├── appsettings.json │ │ ├── css │ │ └── fsh.css │ │ ├── favicon.ico │ │ ├── full-stack-hero-logo.png │ │ ├── index.html │ │ ├── manifest.json │ │ ├── service-worker.js │ │ └── service-worker.published.js ├── Host │ ├── Host.csproj │ ├── Pages │ │ ├── Error.cshtml │ │ └── Error.cshtml.cs │ ├── Program.cs │ ├── Properties │ │ └── launchSettings.json │ ├── appsettings.Development.json │ └── appsettings.json └── Shared │ ├── Authorization │ ├── ClaimsPrincipalExtensions.cs │ ├── FSHClaims.cs │ ├── FSHPermissions.cs │ └── FSHRoles.cs │ ├── Events │ └── IEvent.cs │ ├── MultiTenancy │ └── MultitenancyConstants.cs │ ├── Notifications │ ├── BasicNotification.cs │ ├── INotificationMessage.cs │ ├── JobNotification.cs │ ├── NotificationConstants.cs │ └── StatsChangedNotification.cs │ └── Shared.csproj └── stylecop.json /.dockerignore: -------------------------------------------------------------------------------- 1 | **/.classpath 2 | **/.dockerignore 3 | **/.env 4 | **/.git 5 | **/.gitignore 6 | **/.project 7 | **/.settings 8 | **/.toolstarget 9 | **/.vs 10 | **/.vscode 11 | **/*.*proj.user 12 | **/*.dbmdl 13 | **/*.jfm 14 | **/azds.yaml 15 | **/bin 16 | **/charts 17 | **/docker-compose* 18 | **/Dockerfile* 19 | **/node_modules 20 | **/npm-debug.log 21 | **/obj 22 | **/secrets.dev.yaml 23 | **/values.dev.yaml 24 | LICENSE 25 | README.md -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | open_collective: fullstackhero 3 | custom: ['https://www.buymeacoffee.com/codewithmukesh'] 4 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "nuget" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "daily" 12 | -------------------------------------------------------------------------------- /.github/workflows/dotnet.yml: -------------------------------------------------------------------------------- 1 | name: .NET 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Setup .NET 17 | uses: actions/setup-dotnet@v1 18 | with: 19 | dotnet-version: 6.0.x 20 | - name: Restore dependencies 21 | run: dotnet restore 22 | - name: Build 23 | run: dotnet build --no-restore 24 | - name: Test 25 | run: dotnet test --no-build --verbosity normal 26 | -------------------------------------------------------------------------------- /.template.config/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fullstackhero/blazor-wasm-boilerplate/a3e3eeb70897e9bc9e6ce5d0d2b7a50800081620/.template.config/icon.png -------------------------------------------------------------------------------- /.template.config/ide.host.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/vs-2017.3.host", 3 | "order": 0, 4 | "icon": "icon.png" 5 | } -------------------------------------------------------------------------------- /.template.config/template.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/template", 3 | "author": "Mukesh Murugan", 4 | "classifications": [ 5 | "Blazor", 6 | "WASM", 7 | "WebAssembly", 8 | "Clean Architecture", 9 | "Boilerplate", 10 | "macOS", 11 | "Windows", 12 | "Cloud", 13 | "Web" 14 | ], 15 | "tags": { 16 | "language": "C#", 17 | "type": "project" 18 | }, 19 | "identity": "FullStackHero.BlazorWebAssembly.Boilerplate", 20 | "name": "Blazor WebAssembly Boilerplate - FullStackHero", 21 | "description": "Clean Architecture Boilerplate Template for .NET 6.0 Blazor WebAssembly built for FSH WebAPI with the goodness of MudBlazor Components.", 22 | "shortName": "fsh-blazor", 23 | "sourceName": "FSH.BlazorWebAssembly", 24 | "preferNameDirectory": true, 25 | "sources": [ 26 | { 27 | "source": "./", 28 | "target": "./", 29 | "exclude": [ 30 | ".template.config/**" 31 | ] 32 | } 33 | ], 34 | "primaryOutputs": [ 35 | { 36 | "path": "./" 37 | } 38 | ], 39 | "postActions": [ 40 | { 41 | "description": "Restore NuGet packages required by this project.", 42 | "manualInstructions": [ 43 | { 44 | "text": "Run 'dotnet restore'" 45 | } 46 | ], 47 | "actionId": "210D431B-A78B-4D2F-B762-4ED3E3EA9025", 48 | "continueOnError": true 49 | } 50 | ] 51 | } -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Launch and Debug Standalone Blazor WebAssembly App", 6 | "type": "blazorwasm", 7 | "request": "launch", 8 | "cwd": "${workspaceFolder}/src/Host", 9 | "url": "https://localhost:5002" 10 | } 11 | ] 12 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "razor.disableBlazorDebugPrompt": true, 3 | "cSpell.words": [ 4 | "Blazored", 5 | "borderradius", 6 | "Upto" 7 | ] 8 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "build", 6 | "command": "dotnet", 7 | "type": "process", 8 | "args": [ 9 | "build", 10 | "${workspaceFolder}/src/Host/Host.csproj", 11 | "/property:GenerateFullPaths=true", 12 | "/consoleloggerparameters:NoSummary" 13 | ], 14 | "problemMatcher": "$msCompile" 15 | }, 16 | { 17 | "label": "publish", 18 | "command": "dotnet", 19 | "type": "process", 20 | "args": [ 21 | "publish", 22 | "${workspaceFolder}/src/Host/Host.csproj", 23 | "/property:GenerateFullPaths=true", 24 | "/consoleloggerparameters:NoSummary" 25 | ], 26 | "problemMatcher": "$msCompile" 27 | }, 28 | { 29 | "label": "watch", 30 | "command": "dotnet", 31 | "type": "process", 32 | "args": [ 33 | "watch", 34 | "run", 35 | "${workspaceFolder}/src/Host/Host.csproj", 36 | "/property:GenerateFullPaths=true", 37 | "/consoleloggerparameters:NoSummary" 38 | ], 39 | "problemMatcher": "$msCompile" 40 | } 41 | ] 42 | } -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | $(MSBuildThisFileDirectory)dotnet.ruleset 5 | false 6 | false 7 | true 8 | $(OutputPath)$(AssemblyName).xml 9 | true 10 | enable 11 | enable 12 | 13 | 14 | 15 | 16 | all 17 | runtime; build; native; contentfiles; analyzers; buildtransitive 18 | 19 | 20 | all 21 | runtime; build; native; contentfiles; analyzers; buildtransitive 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Directory.Build.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | $(OutputPath)$(AssemblyName).xml 4 | 5 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base 2 | WORKDIR /app 3 | EXPOSE 80 4 | EXPOSE 443 5 | 6 | FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build 7 | WORKDIR /src 8 | 9 | COPY ["src/Host/Host.csproj", "src/Host/"] 10 | COPY ["src/Shared/Shared.csproj", "src/Shared/"] 11 | COPY ["src/Client/Client.csproj", "src/Client/"] 12 | COPY ["src/Client.Infrastructure/Client.Infrastructure.csproj", "src/Client.Infrastructure/"] 13 | 14 | RUN dotnet restore "src/Host/Host.csproj" 15 | 16 | COPY . . 17 | WORKDIR "/src/src/Host" 18 | 19 | RUN dotnet publish "Host.csproj" -c Release --no-restore -o /app/publish 20 | 21 | FROM base AS final 22 | WORKDIR /app 23 | 24 | COPY --from=build /app/publish . 25 | 26 | ENTRYPOINT ["dotnet", "FSH.BlazorWebAssembly.Host.dll"] -------------------------------------------------------------------------------- /FSH.BlazorWebAssembly.nuspec: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | FullStackHero.BlazorWebAssembly.Boilerplate 5 | Blazor WebAssembly Boilerplate - FullStackHero 6 | 0.0.1-rc 7 | Mukesh Murugan 8 | 9 | Clean Architecture Boilerplate Template for .NET 6.0 Blazor WebAssembly built for FSH WebAPI with the goodness of MudBlazor Components. 10 | 11 | en-US 12 | MIT 13 | 2021 14 | https://fullstackhero.net/blazor-webassembly-boilerplate/general/getting-started/ 15 | 16 | 17 | 18 | 19 | cleanarchitecture clean architecture blazor mukesh codewithmukesh fullstackhero solution csharp wasm webassembly mudblazor 20 | ./content/icon.png 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /FSH.BlazorWebAssembly.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.0.31808.319 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Host", "src\Host\Host.csproj", "{CB2F88F0-F072-420C-9B43-162C2C0773F5}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Client", "src\Client\Client.csproj", "{D5DB6E8A-0161-4EF1-B4D9-467E9A7EF9A7}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Shared", "src\Shared\Shared.csproj", "{0D7A7E2D-CCF4-496E-9F2B-141712F4C00F}" 11 | EndProject 12 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Client.Infrastructure", "src\Client.Infrastructure\Client.Infrastructure.csproj", "{28E5DFC7-A05B-40DB-9E35-AC1E84C008A2}" 13 | EndProject 14 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{F6BE3160-F6ED-4E11-A33F-063DD9186D84}" 15 | ProjectSection(SolutionItems) = preProject 16 | .dockerignore = .dockerignore 17 | .editorconfig = .editorconfig 18 | .gitignore = .gitignore 19 | Directory.Build.props = Directory.Build.props 20 | Directory.Build.targets = Directory.Build.targets 21 | Dockerfile = Dockerfile 22 | dotnet.ruleset = dotnet.ruleset 23 | global.json = global.json 24 | stylecop.json = stylecop.json 25 | EndProjectSection 26 | EndProject 27 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "scripts", "scripts", "{F529F1FF-B2FD-46D5-80A1-D4C726230E40}" 28 | ProjectSection(SolutionItems) = preProject 29 | scripts\nswag-regen.ps1 = scripts\nswag-regen.ps1 30 | scripts\pull-shared-from-webapi.ps1 = scripts\pull-shared-from-webapi.ps1 31 | EndProjectSection 32 | EndProject 33 | Global 34 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 35 | Debug|Any CPU = Debug|Any CPU 36 | Release|Any CPU = Release|Any CPU 37 | EndGlobalSection 38 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 39 | {CB2F88F0-F072-420C-9B43-162C2C0773F5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 40 | {CB2F88F0-F072-420C-9B43-162C2C0773F5}.Debug|Any CPU.Build.0 = Debug|Any CPU 41 | {CB2F88F0-F072-420C-9B43-162C2C0773F5}.Release|Any CPU.ActiveCfg = Release|Any CPU 42 | {CB2F88F0-F072-420C-9B43-162C2C0773F5}.Release|Any CPU.Build.0 = Release|Any CPU 43 | {D5DB6E8A-0161-4EF1-B4D9-467E9A7EF9A7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 44 | {D5DB6E8A-0161-4EF1-B4D9-467E9A7EF9A7}.Debug|Any CPU.Build.0 = Debug|Any CPU 45 | {D5DB6E8A-0161-4EF1-B4D9-467E9A7EF9A7}.Release|Any CPU.ActiveCfg = Release|Any CPU 46 | {D5DB6E8A-0161-4EF1-B4D9-467E9A7EF9A7}.Release|Any CPU.Build.0 = Release|Any CPU 47 | {0D7A7E2D-CCF4-496E-9F2B-141712F4C00F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 48 | {0D7A7E2D-CCF4-496E-9F2B-141712F4C00F}.Debug|Any CPU.Build.0 = Debug|Any CPU 49 | {0D7A7E2D-CCF4-496E-9F2B-141712F4C00F}.Release|Any CPU.ActiveCfg = Release|Any CPU 50 | {0D7A7E2D-CCF4-496E-9F2B-141712F4C00F}.Release|Any CPU.Build.0 = Release|Any CPU 51 | {28E5DFC7-A05B-40DB-9E35-AC1E84C008A2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 52 | {28E5DFC7-A05B-40DB-9E35-AC1E84C008A2}.Debug|Any CPU.Build.0 = Debug|Any CPU 53 | {28E5DFC7-A05B-40DB-9E35-AC1E84C008A2}.Release|Any CPU.ActiveCfg = Release|Any CPU 54 | {28E5DFC7-A05B-40DB-9E35-AC1E84C008A2}.Release|Any CPU.Build.0 = Release|Any CPU 55 | EndGlobalSection 56 | GlobalSection(SolutionProperties) = preSolution 57 | HideSolutionNode = FALSE 58 | EndGlobalSection 59 | GlobalSection(ExtensibilityGlobals) = postSolution 60 | SolutionGuid = {175E8AA9-2BB6-41F9-ACE9-71733ED6EDFD} 61 | EndGlobalSection 62 | EndGlobal 63 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 fullstackhero 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /global.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdk": { 3 | "version": "6.0", 4 | "rollForward": "latestMajor", 5 | "allowPrerelease": true 6 | } 7 | } -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fullstackhero/blazor-wasm-boilerplate/a3e3eeb70897e9bc9e6ce5d0d2b7a50800081620/icon.png -------------------------------------------------------------------------------- /scripts/nswag-regen.ps1: -------------------------------------------------------------------------------- 1 | # This script is cross-platform, supporting all OSes that PowerShell Core/7 runs on. 2 | 3 | $currentDirectory = Get-Location 4 | $rootDirectory = git rev-parse --show-toplevel 5 | $hostDirectory = Join-Path -Path $rootDirectory -ChildPath 'src/Host' 6 | $infrastructurePrj = Join-Path -Path $rootDirectory -ChildPath 'src/Client.Infrastructure/Client.Infrastructure.csproj' 7 | 8 | Write-Host "Make sure you have run the FSH.WebApi project. `n" 9 | Write-Host "Press any key to continue... `n" 10 | $null = $Host.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown'); 11 | 12 | Set-Location -Path $hostDirectory 13 | Write-Host "Host Directory is $hostDirectory `n" 14 | 15 | <# Run command #> 16 | dotnet build -t:NSwag $infrastructurePrj 17 | 18 | Set-Location -Path $currentDirectory 19 | Write-Host -NoNewLine 'NSwag Regenerated. Press any key to continue...'; 20 | $null = $Host.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown'); 21 | -------------------------------------------------------------------------------- /scripts/pull-shared-from-webapi.ps1: -------------------------------------------------------------------------------- 1 | # This script is cross-platform, supporting all OSes that PowerShell Core/7 runs on. 2 | 3 | $rootDirectory = git rev-parse --show-toplevel 4 | $sourcePath = Join-Path -Path $rootDirectory -ChildPath '..\dotnet-webapi-boilerplate\src\Core\Shared' 5 | $destinationPath = Join-Path -Path $rootDirectory -ChildPath 'src\Shared' 6 | 7 | $excludes = @('bin', 'obj') 8 | 9 | Write-Host "Pull changes from the Fullstackhero WebApi Shared Project" 10 | write-Host "---------------------------------------------------------" 11 | Write-Host 12 | 13 | If ($null -eq $sourcePath) { 14 | Write-Error "Error! The expected path of WebApi Shared Project does not exist: $sourcePath" 15 | Exit 1 16 | } 17 | 18 | if ($null -eq (Resolve-Path $destinationPath)) { 19 | # Ensure the destination exists 20 | try 21 | { 22 | New-Item -Path $destinationPath -ItemType Directory -ErrorAction Stop | Out-Null 23 | } 24 | catch 25 | { 26 | Write-Error "Error! Unable to create output path \"$destinationPath\"" 27 | Exit 1 28 | } 29 | } 30 | 31 | Write-Host "WARNING! This will delete everything in the shared project ($($destinationPath | Resolve-Path))" 32 | Write-Host "and then copy over the whole project from the webapi repository ($($sourcePath | Resolve-Path))" 33 | Write-Host 34 | Read-Host -Prompt "Press ENTER to continue" 35 | 36 | Remove-Item -Path "$destinationPath" -Recurse -Force 37 | Copy-Item -Path (Get-Item -Path "$sourcePath" -Exclude $excludes).FullName -Destination "$destinationPath" -Recurse -Force 38 | 39 | Write-Host "Changes have been pulled." 40 | Write-Host -------------------------------------------------------------------------------- /src/Client.Infrastructure/ApiClient/IApiService.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.BlazorWebAssembly.Client.Infrastructure.ApiClient; 2 | 3 | public interface IApiService 4 | { 5 | } -------------------------------------------------------------------------------- /src/Client.Infrastructure/ApiClient/nswag.json: -------------------------------------------------------------------------------- 1 | { 2 | "runtime": "Net60", 3 | "defaultVariables": null, 4 | "documentGenerator": { 5 | "fromDocument": { 6 | "json": "", 7 | "url": "https://localhost:5001/swagger/v1/swagger.json", 8 | "output": null, 9 | "newLineBehavior": "Auto" 10 | } 11 | }, 12 | "codeGenerators": { 13 | "openApiToCSharpClient": { 14 | "clientBaseClass": null, 15 | "configurationClass": null, 16 | "generateClientClasses": true, 17 | "generateClientInterfaces": true, 18 | "clientBaseInterface": "IApiService", 19 | "injectHttpClient": true, 20 | "disposeHttpClient": false, 21 | "protectedMethods": [], 22 | "generateExceptionClasses": true, 23 | "exceptionClass": "ApiException", 24 | "wrapDtoExceptions": true, 25 | "useHttpClientCreationMethod": false, 26 | "httpClientType": "System.Net.Http.HttpClient", 27 | "useHttpRequestMessageCreationMethod": false, 28 | "useBaseUrl": false, 29 | "generateBaseUrlProperty": true, 30 | "generateSyncMethods": false, 31 | "generatePrepareRequestAndProcessResponseAsAsyncMethods": false, 32 | "exposeJsonSerializerSettings": false, 33 | "clientClassAccessModifier": "public", 34 | "typeAccessModifier": "public", 35 | "generateContractsOutput": false, 36 | "contractsNamespace": null, 37 | "contractsOutputFilePath": null, 38 | "parameterDateTimeFormat": "s", 39 | "parameterDateFormat": "yyyy-MM-dd", 40 | "generateUpdateJsonSerializerSettingsMethod": true, 41 | "useRequestAndResponseSerializationSettings": false, 42 | "serializeTypeInformation": false, 43 | "queryNullValue": "", 44 | "className": "{controller}Client", 45 | "operationGenerationMode": "MultipleClientsFromOperationId", 46 | "additionalNamespaceUsages": [], 47 | "additionalContractNamespaceUsages": [], 48 | "generateOptionalParameters": false, 49 | "generateJsonMethods": false, 50 | "enforceFlagEnums": false, 51 | "parameterArrayType": "System.Collections.Generic.IEnumerable", 52 | "parameterDictionaryType": "System.Collections.Generic.IDictionary", 53 | "responseArrayType": "System.Collections.Generic.ICollection", 54 | "responseDictionaryType": "System.Collections.Generic.IDictionary", 55 | "wrapResponses": false, 56 | "wrapResponseMethods": [], 57 | "generateResponseClasses": true, 58 | "responseClass": "SwaggerResponse", 59 | "namespace": "FSH.BlazorWebAssembly.Client.Infrastructure.ApiClient", 60 | "requiredPropertiesMustBeDefined": true, 61 | "dateType": "System.DateTimeOffset", 62 | "jsonConverters": null, 63 | "anyType": "object", 64 | "dateTimeType": "System.DateTime", 65 | "timeType": "System.TimeSpan", 66 | "timeSpanType": "System.TimeSpan", 67 | "arrayType": "System.Collections.Generic.ICollection", 68 | "arrayInstanceType": "System.Collections.ObjectModel.Collection", 69 | "dictionaryType": "System.Collections.Generic.IDictionary", 70 | "dictionaryInstanceType": "System.Collections.Generic.Dictionary", 71 | "arrayBaseType": "System.Collections.ObjectModel.Collection", 72 | "dictionaryBaseType": "System.Collections.Generic.Dictionary", 73 | "classStyle": "Poco", 74 | "jsonLibrary": "NewtonsoftJson", 75 | "generateDefaultValues": true, 76 | "generateDataAnnotations": true, 77 | "excludedTypeNames": [], 78 | "excludedParameterNames": [], 79 | "handleReferences": false, 80 | "generateImmutableArrayProperties": false, 81 | "generateImmutableDictionaryProperties": false, 82 | "jsonSerializerSettingsTransformationMethod": null, 83 | "inlineNamedArrays": false, 84 | "inlineNamedDictionaries": false, 85 | "inlineNamedTuples": true, 86 | "inlineNamedAny": false, 87 | "generateDtoTypes": true, 88 | "generateOptionalPropertiesAsNullable": false, 89 | "generateNullableReferenceTypes": true, 90 | "templateDirectory": null, 91 | "typeNameGeneratorType": null, 92 | "propertyNameGeneratorType": null, 93 | "enumNameGeneratorType": null, 94 | "serviceHost": null, 95 | "serviceSchemes": null, 96 | "output": "FSHApi.cs", 97 | "newLineBehavior": "Auto" 98 | } 99 | } 100 | } -------------------------------------------------------------------------------- /src/Client.Infrastructure/Auth/AuthProvider.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.BlazorWebAssembly.Client.Infrastructure.Auth; 2 | 3 | public enum AuthProvider 4 | { 5 | Jwt, 6 | AzureAd 7 | } -------------------------------------------------------------------------------- /src/Client.Infrastructure/Auth/AuthorizationServiceExtensions.cs: -------------------------------------------------------------------------------- 1 | using FSH.WebApi.Shared.Authorization; 2 | using Microsoft.AspNetCore.Authorization; 3 | 4 | namespace FSH.BlazorWebAssembly.Client.Infrastructure.Auth; 5 | 6 | public static class AuthorizationServiceExtensions 7 | { 8 | public static async Task HasPermissionAsync(this IAuthorizationService service, ClaimsPrincipal user, string action, string resource) => 9 | (await service.AuthorizeAsync(user, null, FSHPermission.NameFor(action, resource))).Succeeded; 10 | } -------------------------------------------------------------------------------- /src/Client.Infrastructure/Auth/AzureAd/AzureAdAuthenticationService.cs: -------------------------------------------------------------------------------- 1 | using FSH.BlazorWebAssembly.Client.Infrastructure.ApiClient; 2 | using Microsoft.AspNetCore.Components; 3 | using Microsoft.AspNetCore.Components.WebAssembly.Authentication; 4 | 5 | namespace FSH.BlazorWebAssembly.Client.Infrastructure.Auth.AzureAd; 6 | 7 | internal class AzureAdAuthenticationService : IAuthenticationService 8 | { 9 | private readonly SignOutSessionStateManager _signOut; 10 | private readonly NavigationManager _navigation; 11 | 12 | public AzureAdAuthenticationService(SignOutSessionStateManager signOut, NavigationManager navigation) => 13 | (_signOut, _navigation) = (signOut, navigation); 14 | 15 | public AuthProvider ProviderType => AuthProvider.AzureAd; 16 | 17 | public void NavigateToExternalLogin(string returnUrl) => 18 | _navigation.NavigateTo($"authentication/login?returnUrl={Uri.EscapeDataString(returnUrl)}"); 19 | 20 | public Task LoginAsync(string tenantId, TokenRequest request) => 21 | throw new NotImplementedException(); 22 | 23 | public async Task LogoutAsync() 24 | { 25 | await _signOut.SetSignOutState(); 26 | _navigation.NavigateTo("authentication/logout"); 27 | } 28 | 29 | public Task ReLoginAsync(string returnUrl) 30 | { 31 | NavigateToExternalLogin(returnUrl); 32 | return Task.CompletedTask; 33 | } 34 | } -------------------------------------------------------------------------------- /src/Client.Infrastructure/Auth/AzureAd/AzureAdAuthorizationMessageHandler.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Components; 2 | using Microsoft.AspNetCore.Components.WebAssembly.Authentication; 3 | 4 | namespace FSH.BlazorWebAssembly.Client.Infrastructure.Auth.AzureAd; 5 | 6 | public class AzureAdAuthorizationMessageHandler : AuthorizationMessageHandler 7 | { 8 | public AzureAdAuthorizationMessageHandler(IAccessTokenProvider provider, NavigationManager navigation, IConfiguration config) 9 | : base(provider, navigation) => ConfigureHandler( 10 | new[] { config[ConfigNames.ApiBaseUrl] }, 11 | new[] { config[$"{nameof(AuthProvider.AzureAd)}:{ConfigNames.ApiScope}"] }); 12 | } -------------------------------------------------------------------------------- /src/Client.Infrastructure/Auth/AzureAd/AzureAdClaimsPrincipalFactory.cs: -------------------------------------------------------------------------------- 1 | using FSH.BlazorWebAssembly.Client.Infrastructure.ApiClient; 2 | using FSH.WebApi.Shared.Authorization; 3 | using Microsoft.AspNetCore.Components.WebAssembly.Authentication; 4 | using Microsoft.AspNetCore.Components.WebAssembly.Authentication.Internal; 5 | using Microsoft.Extensions.DependencyInjection; 6 | 7 | namespace FSH.BlazorWebAssembly.Client.Infrastructure.Auth.AzureAd; 8 | 9 | internal class AzureAdClaimsPrincipalFactory : AccountClaimsPrincipalFactory 10 | { 11 | // Can't work with actual services in the constructor here, have to 12 | // use IServiceProvider, otherwise the app hangs at startup. 13 | // The culprit is probably HttpClient, as this class is instantiated 14 | // at startup while the HttpClient is being (or not even) created. 15 | private readonly IServiceProvider _services; 16 | 17 | public AzureAdClaimsPrincipalFactory(IAccessTokenProviderAccessor accessor, IServiceProvider services) 18 | : base(accessor) => 19 | _services = services; 20 | 21 | public override async ValueTask CreateUserAsync(RemoteUserAccount account, RemoteAuthenticationUserOptions options) 22 | { 23 | var principal = await base.CreateUserAsync(account, options); 24 | 25 | if (principal.Identity?.IsAuthenticated is true) 26 | { 27 | var userDetails = await _services.GetRequiredService().GetProfileAsync(); 28 | 29 | var userIdentity = (ClaimsIdentity)principal.Identity; 30 | 31 | if (!string.IsNullOrWhiteSpace(userDetails.Email) && !userIdentity.HasClaim(c => c.Type == ClaimTypes.Email)) 32 | { 33 | userIdentity.AddClaim(new Claim(ClaimTypes.Email, userDetails.Email)); 34 | } 35 | 36 | if (!string.IsNullOrWhiteSpace(userDetails.PhoneNumber) && !userIdentity.HasClaim(c => c.Type == ClaimTypes.MobilePhone)) 37 | { 38 | userIdentity.AddClaim(new Claim(ClaimTypes.MobilePhone, userDetails.PhoneNumber)); 39 | } 40 | 41 | if (!string.IsNullOrWhiteSpace(userDetails.FirstName) && !userIdentity.HasClaim(c => c.Type == ClaimTypes.Name)) 42 | { 43 | userIdentity.AddClaim(new Claim(ClaimTypes.Name, userDetails.FirstName)); 44 | } 45 | 46 | if (!string.IsNullOrWhiteSpace(userDetails.LastName) && !userIdentity.HasClaim(c => c.Type == ClaimTypes.Surname)) 47 | { 48 | userIdentity.AddClaim(new Claim(ClaimTypes.Surname, userDetails.LastName)); 49 | } 50 | 51 | if (!userIdentity.HasClaim(c => c.Type == FSHClaims.Fullname)) 52 | { 53 | userIdentity.AddClaim(new Claim(FSHClaims.Fullname, $"{userDetails.FirstName} {userDetails.LastName}")); 54 | } 55 | 56 | if (!userIdentity.HasClaim(c => c.Type == ClaimTypes.NameIdentifier)) 57 | { 58 | userIdentity.AddClaim(new Claim(ClaimTypes.NameIdentifier, userDetails.Id.ToString())); 59 | } 60 | 61 | if (!string.IsNullOrWhiteSpace(userDetails.ImageUrl) && !userIdentity.HasClaim(c => c.Type == FSHClaims.ImageUrl) && userDetails.ImageUrl is not null) 62 | { 63 | userIdentity.AddClaim(new Claim(FSHClaims.ImageUrl, userDetails.ImageUrl)); 64 | } 65 | 66 | var permissions = await _services.GetRequiredService().GetPermissionsAsync(); 67 | 68 | userIdentity.AddClaims(permissions.Select(permission => new Claim(FSHClaims.Permission, permission))); 69 | } 70 | 71 | return principal; 72 | } 73 | } -------------------------------------------------------------------------------- /src/Client.Infrastructure/Auth/IAuthenticationService.cs: -------------------------------------------------------------------------------- 1 | using FSH.BlazorWebAssembly.Client.Infrastructure.ApiClient; 2 | 3 | namespace FSH.BlazorWebAssembly.Client.Infrastructure.Auth; 4 | 5 | public interface IAuthenticationService 6 | { 7 | AuthProvider ProviderType { get; } 8 | 9 | void NavigateToExternalLogin(string returnUrl); 10 | 11 | Task LoginAsync(string tenantId, TokenRequest request); 12 | 13 | Task LogoutAsync(); 14 | 15 | Task ReLoginAsync(string returnUrl); 16 | } -------------------------------------------------------------------------------- /src/Client.Infrastructure/Auth/Jwt/AccessTokenProviderAccessor.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Components.WebAssembly.Authentication; 2 | using Microsoft.AspNetCore.Components.WebAssembly.Authentication.Internal; 3 | using Microsoft.Extensions.DependencyInjection; 4 | 5 | namespace FSH.BlazorWebAssembly.Client.Infrastructure.Auth.Jwt; 6 | 7 | internal class AccessTokenProviderAccessor : IAccessTokenProviderAccessor 8 | { 9 | private readonly IServiceProvider _provider; 10 | private IAccessTokenProvider? _tokenProvider; 11 | 12 | public AccessTokenProviderAccessor(IServiceProvider provider) => 13 | _provider = provider; 14 | 15 | public IAccessTokenProvider TokenProvider => 16 | _tokenProvider ??= _provider.GetRequiredService(); 17 | } -------------------------------------------------------------------------------- /src/Client.Infrastructure/Auth/Jwt/JwtAuthenticationHeaderHandler.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Components; 2 | using Microsoft.AspNetCore.Components.WebAssembly.Authentication.Internal; 3 | 4 | namespace FSH.BlazorWebAssembly.Client.Infrastructure.Auth.Jwt; 5 | 6 | public class JwtAuthenticationHeaderHandler : DelegatingHandler 7 | { 8 | private readonly IAccessTokenProviderAccessor _tokenProviderAccessor; 9 | private readonly NavigationManager _navigation; 10 | 11 | public JwtAuthenticationHeaderHandler(IAccessTokenProviderAccessor tokenProviderAccessor, NavigationManager navigation) 12 | { 13 | _tokenProviderAccessor = tokenProviderAccessor; 14 | _navigation = navigation; 15 | } 16 | 17 | protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) 18 | { 19 | // skip token endpoints 20 | if (request.RequestUri?.AbsolutePath.Contains("/tokens") is not true) 21 | { 22 | if (await _tokenProviderAccessor.TokenProvider.GetAccessTokenAsync() is string token) 23 | { 24 | request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token); 25 | } 26 | else 27 | { 28 | _navigation.NavigateTo("/login"); 29 | } 30 | } 31 | 32 | return await base.SendAsync(request, cancellationToken); 33 | } 34 | } -------------------------------------------------------------------------------- /src/Client.Infrastructure/Auth/MustHavePermissionAttribute.cs: -------------------------------------------------------------------------------- 1 | using FSH.WebApi.Shared.Authorization; 2 | using Microsoft.AspNetCore.Authorization; 3 | 4 | namespace FSH.BlazorWebAssembly.Client.Infrastructure.Auth; 5 | 6 | public class MustHavePermissionAttribute : AuthorizeAttribute 7 | { 8 | public MustHavePermissionAttribute(string action, string resource) => 9 | Policy = FSHPermission.NameFor(action, resource); 10 | } -------------------------------------------------------------------------------- /src/Client.Infrastructure/Auth/Startup.cs: -------------------------------------------------------------------------------- 1 | using FSH.BlazorWebAssembly.Client.Infrastructure.Auth.AzureAd; 2 | using FSH.BlazorWebAssembly.Client.Infrastructure.Auth.Jwt; 3 | using Microsoft.AspNetCore.Components.WebAssembly.Authentication; 4 | using Microsoft.AspNetCore.Components.WebAssembly.Authentication.Internal; 5 | using Microsoft.Extensions.DependencyInjection; 6 | 7 | namespace FSH.BlazorWebAssembly.Client.Infrastructure.Auth; 8 | 9 | internal static class Startup 10 | { 11 | public static IServiceCollection AddAuthentication(this IServiceCollection services, IConfiguration config) => 12 | config[nameof(AuthProvider)] switch 13 | { 14 | // AzureAd 15 | nameof(AuthProvider.AzureAd) => services 16 | .AddScoped() 17 | .AddScoped() 18 | .AddMsalAuthentication(options => 19 | { 20 | config.Bind(nameof(AuthProvider.AzureAd), options.ProviderOptions.Authentication); 21 | options.ProviderOptions.DefaultAccessTokenScopes.Add( 22 | config[$"{nameof(AuthProvider.AzureAd)}:{ConfigNames.ApiScope}"]); 23 | options.ProviderOptions.LoginMode = "redirect"; 24 | }) 25 | .AddAccountClaimsPrincipalFactory() 26 | .Services, 27 | 28 | // Jwt 29 | _ => services 30 | .AddScoped() 31 | .AddScoped(sp => (IAuthenticationService)sp.GetRequiredService()) 32 | .AddScoped(sp => (IAccessTokenProvider)sp.GetRequiredService()) 33 | .AddScoped() 34 | .AddScoped() 35 | }; 36 | 37 | public static IHttpClientBuilder AddAuthenticationHandler(this IHttpClientBuilder builder, IConfiguration config) => 38 | config[nameof(AuthProvider)] switch 39 | { 40 | // AzureAd 41 | nameof(AuthProvider.AzureAd) => 42 | builder.AddHttpMessageHandler(), 43 | 44 | // Jwt 45 | _ => builder.AddHttpMessageHandler() 46 | }; 47 | } -------------------------------------------------------------------------------- /src/Client.Infrastructure/Client.Infrastructure.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net6.0 5 | FSH.BlazorWebAssembly.Client.Infrastructure 6 | FSH.BlazorWebAssembly.Client.Infrastructure 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | all 25 | runtime; build; native; contentfiles; analyzers; buildtransitive 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /src/Client.Infrastructure/Common/AccessTokenProviderExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Components.WebAssembly.Authentication; 2 | 3 | namespace FSH.BlazorWebAssembly.Client.Infrastructure.Common; 4 | 5 | public static class AccessTokenProviderExtensions 6 | { 7 | public static async Task GetAccessTokenAsync(this IAccessTokenProvider tokenProvider) => 8 | (await tokenProvider.RequestAccessToken()) 9 | .TryGetToken(out var token) 10 | ? token.Value 11 | : null; 12 | } -------------------------------------------------------------------------------- /src/Client.Infrastructure/Common/ApplicationConstants.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.BlazorWebAssembly.Client.Infrastructure.Common; 2 | 3 | public static class ApplicationConstants 4 | { 5 | public static readonly List SupportedImageFormats = new() 6 | { 7 | ".jpeg", 8 | ".jpg", 9 | ".png" 10 | }; 11 | public static readonly string StandardImageFormat = "image/jpeg"; 12 | public static readonly int MaxImageWidth = 1500; 13 | public static readonly int MaxImageHeight = 1500; 14 | public static readonly long MaxAllowedSize = 1000000; // Allows Max File Size of 1 Mb. 15 | } -------------------------------------------------------------------------------- /src/Client.Infrastructure/Common/ConfigNames.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.BlazorWebAssembly.Client.Infrastructure.Common; 2 | 3 | public static class ConfigNames 4 | { 5 | public const string ApiBaseUrl = "ApiBaseUrl"; 6 | public const string ApiScope = "ApiScope"; 7 | } -------------------------------------------------------------------------------- /src/Client.Infrastructure/Common/IAppService.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.BlazorWebAssembly.Client.Infrastructure.Common; 2 | 3 | public interface IAppService 4 | { 5 | } -------------------------------------------------------------------------------- /src/Client.Infrastructure/Common/LocalizationConstants.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.BlazorWebAssembly.Client.Infrastructure.Common; 2 | 3 | public record LanguageCode(string Code, string DisplayName, bool IsRTL = false); 4 | 5 | public static class LocalizationConstants 6 | { 7 | public static readonly LanguageCode[] SupportedLanguages = 8 | { 9 | new("en-US", "English"), 10 | new("fr-FR", "French"), 11 | new("km_KH", "Khmer"), 12 | new("de-DE", "German"), 13 | new("nl-NL", "Dutch - Netherlands"), 14 | new("es-ES", "Spanish"), 15 | new("ru-RU", "Russian"), 16 | new("sv-SE", "Swedish"), 17 | new("id-ID", "Indonesia"), 18 | new("it-IT", "Italian"), 19 | new("ar", "عربي", true), 20 | new("pt-BR", "Portugues") 21 | }; 22 | } -------------------------------------------------------------------------------- /src/Client.Infrastructure/Common/StorageConstants.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.BlazorWebAssembly.Client.Infrastructure.Common; 2 | 3 | public static class StorageConstants 4 | { 5 | public static class Local 6 | { 7 | public static string Preference = "clientPreference"; 8 | 9 | public static string AuthToken = "authToken"; 10 | public static string RefreshToken = "refreshToken"; 11 | public static string ImageUri = "userImageURL"; 12 | public static string Permissions = "permissions"; 13 | } 14 | } -------------------------------------------------------------------------------- /src/Client.Infrastructure/GlobalUsings.cs: -------------------------------------------------------------------------------- 1 | global using System.Net.Http.Headers; 2 | global using System.Security.Claims; 3 | global using System.Text.Json; 4 | global using Blazored.LocalStorage; 5 | global using FSH.BlazorWebAssembly.Client.Infrastructure.Common; 6 | global using Microsoft.AspNetCore.Components.Authorization; 7 | global using Microsoft.Extensions.Configuration; 8 | -------------------------------------------------------------------------------- /src/Client.Infrastructure/Notifications/ConnectionState.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.BlazorWebAssembly.Client.Infrastructure.Notifications; 2 | 3 | public enum ConnectionState 4 | { 5 | Connected, 6 | Connecting, 7 | Disconnected 8 | } -------------------------------------------------------------------------------- /src/Client.Infrastructure/Notifications/ConnectionStateChanged.cs: -------------------------------------------------------------------------------- 1 | using FSH.WebApi.Shared.Notifications; 2 | 3 | namespace FSH.BlazorWebAssembly.Client.Infrastructure.Notifications; 4 | 5 | public record ConnectionStateChanged(ConnectionState State, string? Message) : INotificationMessage; -------------------------------------------------------------------------------- /src/Client.Infrastructure/Notifications/INotificationPublisher.cs: -------------------------------------------------------------------------------- 1 | using FSH.WebApi.Shared.Notifications; 2 | 3 | namespace FSH.BlazorWebAssembly.Client.Infrastructure.Notifications; 4 | 5 | public interface INotificationPublisher 6 | { 7 | Task PublishAsync(INotificationMessage notification); 8 | } -------------------------------------------------------------------------------- /src/Client.Infrastructure/Notifications/NotificationPublisher.cs: -------------------------------------------------------------------------------- 1 | using FSH.WebApi.Shared.Notifications; 2 | using MediatR; 3 | using Microsoft.Extensions.Logging; 4 | 5 | namespace FSH.BlazorWebAssembly.Client.Infrastructure.Notifications; 6 | 7 | public class NotificationPublisher : INotificationPublisher 8 | { 9 | private readonly ILogger _logger; 10 | private readonly IPublisher _mediator; 11 | 12 | public NotificationPublisher(ILogger logger, IPublisher mediator) => 13 | (_logger, _mediator) = (logger, mediator); 14 | 15 | public Task PublishAsync(INotificationMessage notification) 16 | { 17 | _logger.LogInformation("Publishing Notification : {notification}", notification.GetType().Name); 18 | return _mediator.Publish(CreateNotificationWrapper(notification)); 19 | } 20 | 21 | private INotification CreateNotificationWrapper(INotificationMessage notification) => 22 | (INotification)Activator.CreateInstance( 23 | typeof(NotificationWrapper<>).MakeGenericType(notification.GetType()), notification)!; 24 | } -------------------------------------------------------------------------------- /src/Client.Infrastructure/Notifications/NotificationWrapper.cs: -------------------------------------------------------------------------------- 1 | using FSH.WebApi.Shared.Notifications; 2 | using MediatR; 3 | 4 | namespace FSH.BlazorWebAssembly.Client.Infrastructure.Notifications; 5 | 6 | public class NotificationWrapper : INotification 7 | where TNotificationMessage : INotificationMessage 8 | { 9 | public NotificationWrapper(TNotificationMessage notification) => Notification = notification; 10 | 11 | public TNotificationMessage Notification { get; } 12 | } -------------------------------------------------------------------------------- /src/Client.Infrastructure/Notifications/Startup.cs: -------------------------------------------------------------------------------- 1 | using FSH.WebApi.Shared.Notifications; 2 | using MediatR; 3 | using MediatR.Courier; 4 | using MediatR.Courier.DependencyInjection; 5 | using Microsoft.Extensions.DependencyInjection; 6 | 7 | namespace FSH.BlazorWebAssembly.Client.Infrastructure.Notifications; 8 | 9 | internal static class Startup 10 | { 11 | public static IServiceCollection AddNotifications(this IServiceCollection services) 12 | { 13 | // Add mediator processing of notifications 14 | var assemblies = AppDomain.CurrentDomain.GetAssemblies(); 15 | 16 | services 17 | .AddMediatR(assemblies) 18 | .AddCourier(assemblies) 19 | .AddTransient(); 20 | 21 | // Register handlers for all INotificationMessages 22 | foreach (var eventType in assemblies 23 | .SelectMany(a => a.GetTypes()) 24 | .Where(t => t.GetInterfaces().Any(i => i == typeof(INotificationMessage)))) 25 | { 26 | services.AddSingleton( 27 | typeof(INotificationHandler<>).MakeGenericType( 28 | typeof(NotificationWrapper<>).MakeGenericType(eventType)), 29 | serviceProvider => serviceProvider.GetRequiredService(typeof(MediatRCourier))); 30 | } 31 | 32 | return services; 33 | } 34 | } -------------------------------------------------------------------------------- /src/Client.Infrastructure/Preferences/ClientPreference.cs: -------------------------------------------------------------------------------- 1 | using FSH.BlazorWebAssembly.Client.Infrastructure.Theme; 2 | 3 | namespace FSH.BlazorWebAssembly.Client.Infrastructure.Preferences; 4 | 5 | public class ClientPreference : IPreference 6 | { 7 | public bool IsDarkMode { get; set; } 8 | public bool IsRTL { get; set; } 9 | public bool IsDrawerOpen { get; set; } 10 | public string PrimaryColor { get; set; } = CustomColors.Light.Primary; 11 | public string SecondaryColor { get; set; } = CustomColors.Light.Secondary; 12 | public double BorderRadius { get; set; } = 5; 13 | public string LanguageCode { get; set; } = LocalizationConstants.SupportedLanguages.FirstOrDefault()?.Code ?? "en-US"; 14 | public FshTablePreference TablePreference { get; set; } = new FshTablePreference(); 15 | } 16 | -------------------------------------------------------------------------------- /src/Client.Infrastructure/Preferences/ClientPreferenceManager.cs: -------------------------------------------------------------------------------- 1 | using System.Text.RegularExpressions; 2 | using FSH.BlazorWebAssembly.Client.Infrastructure.Theme; 3 | using MudBlazor; 4 | 5 | namespace FSH.BlazorWebAssembly.Client.Infrastructure.Preferences; 6 | 7 | public class ClientPreferenceManager : IClientPreferenceManager 8 | { 9 | private readonly ILocalStorageService _localStorageService; 10 | 11 | public ClientPreferenceManager( 12 | ILocalStorageService localStorageService) 13 | { 14 | _localStorageService = localStorageService; 15 | } 16 | 17 | public async Task ToggleDarkModeAsync() 18 | { 19 | if (await GetPreference() is ClientPreference preference) 20 | { 21 | preference.IsDarkMode = !preference.IsDarkMode; 22 | await SetPreference(preference); 23 | return !preference.IsDarkMode; 24 | } 25 | 26 | return false; 27 | } 28 | 29 | public async Task ToggleDrawerAsync() 30 | { 31 | if (await GetPreference() is ClientPreference preference) 32 | { 33 | preference.IsDrawerOpen = !preference.IsDrawerOpen; 34 | await SetPreference(preference); 35 | return preference.IsDrawerOpen; 36 | } 37 | 38 | return false; 39 | } 40 | 41 | public async Task ToggleLayoutDirectionAsync() 42 | { 43 | if (await GetPreference() is ClientPreference preference) 44 | { 45 | preference.IsRTL = !preference.IsRTL; 46 | await SetPreference(preference); 47 | return preference.IsRTL; 48 | } 49 | 50 | return false; 51 | } 52 | 53 | public async Task ChangeLanguageAsync(string languageCode) 54 | { 55 | if (await GetPreference() is ClientPreference preference) 56 | { 57 | var language = Array.Find(LocalizationConstants.SupportedLanguages, a => a.Code == languageCode); 58 | if (language?.Code is not null) 59 | { 60 | preference.LanguageCode = language.Code; 61 | preference.IsRTL = language.IsRTL; 62 | } 63 | else 64 | { 65 | preference.LanguageCode = "en-EN"; 66 | preference.IsRTL = false; 67 | } 68 | 69 | await SetPreference(preference); 70 | return true; 71 | } 72 | 73 | return false; 74 | } 75 | 76 | public async Task GetCurrentThemeAsync() 77 | { 78 | if (await GetPreference() is ClientPreference preference) 79 | { 80 | if (preference.IsDarkMode) return new DarkTheme(); 81 | } 82 | 83 | return new LightTheme(); 84 | } 85 | 86 | public async Task GetPrimaryColorAsync() 87 | { 88 | if (await GetPreference() is ClientPreference preference) 89 | { 90 | string colorCode = preference.PrimaryColor; 91 | if (Regex.Match(colorCode, "^#(?:[0-9a-fA-F]{3,4}){1,2}$").Success) 92 | { 93 | return colorCode; 94 | } 95 | else 96 | { 97 | preference.PrimaryColor = CustomColors.Light.Primary; 98 | await SetPreference(preference); 99 | return preference.PrimaryColor; 100 | } 101 | } 102 | 103 | return CustomColors.Light.Primary; 104 | } 105 | 106 | public async Task IsRTL() 107 | { 108 | if (await GetPreference() is ClientPreference preference) 109 | { 110 | return preference.IsRTL; 111 | } 112 | 113 | return false; 114 | } 115 | 116 | public async Task IsDrawerOpen() 117 | { 118 | if (await GetPreference() is ClientPreference preference) 119 | { 120 | return preference.IsDrawerOpen; 121 | } 122 | 123 | return false; 124 | } 125 | 126 | public static string Preference = "clientPreference"; 127 | 128 | public async Task GetPreference() 129 | { 130 | return await _localStorageService.GetItemAsync(Preference) ?? new ClientPreference(); 131 | } 132 | 133 | public async Task SetPreference(IPreference preference) 134 | { 135 | await _localStorageService.SetItemAsync(Preference, preference as ClientPreference); 136 | } 137 | } -------------------------------------------------------------------------------- /src/Client.Infrastructure/Preferences/FshTablePreference.cs: -------------------------------------------------------------------------------- 1 | using FSH.WebApi.Shared.Notifications; 2 | 3 | namespace FSH.BlazorWebAssembly.Client.Infrastructure.Preferences; 4 | 5 | public class FshTablePreference : INotificationMessage 6 | { 7 | public bool IsDense { get; set; } 8 | public bool IsStriped { get; set; } 9 | public bool HasBorder { get; set; } 10 | public bool IsHoverable { get; set; } 11 | } -------------------------------------------------------------------------------- /src/Client.Infrastructure/Preferences/IClientPreferenceManager.cs: -------------------------------------------------------------------------------- 1 | using MudBlazor; 2 | 3 | namespace FSH.BlazorWebAssembly.Client.Infrastructure.Preferences; 4 | 5 | public interface IClientPreferenceManager : IPreferenceManager 6 | { 7 | Task GetCurrentThemeAsync(); 8 | 9 | Task ToggleDarkModeAsync(); 10 | 11 | Task ToggleDrawerAsync(); 12 | 13 | Task ToggleLayoutDirectionAsync(); 14 | } -------------------------------------------------------------------------------- /src/Client.Infrastructure/Preferences/IPreference.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.BlazorWebAssembly.Client.Infrastructure.Preferences; 2 | 3 | public interface IPreference 4 | { 5 | // public string LanguageCode { get; set; } 6 | } -------------------------------------------------------------------------------- /src/Client.Infrastructure/Preferences/IPreferenceManager.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.BlazorWebAssembly.Client.Infrastructure.Preferences; 2 | 3 | public interface IPreferenceManager : IAppService 4 | { 5 | Task SetPreference(IPreference preference); 6 | 7 | Task GetPreference(); 8 | 9 | Task ChangeLanguageAsync(string languageCode); 10 | } -------------------------------------------------------------------------------- /src/Client.Infrastructure/Startup.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | using FSH.BlazorWebAssembly.Client.Infrastructure.ApiClient; 3 | using FSH.BlazorWebAssembly.Client.Infrastructure.Auth; 4 | using FSH.BlazorWebAssembly.Client.Infrastructure.Notifications; 5 | using FSH.BlazorWebAssembly.Client.Infrastructure.Preferences; 6 | using FSH.WebApi.Shared.Authorization; 7 | using Microsoft.AspNetCore.Authorization; 8 | using Microsoft.Extensions.DependencyInjection; 9 | using MudBlazor; 10 | using MudBlazor.Services; 11 | 12 | namespace FSH.BlazorWebAssembly.Client.Infrastructure; 13 | 14 | public static class Startup 15 | { 16 | private const string ClientName = "FullStackHero.API"; 17 | 18 | public static IServiceCollection AddClientServices(this IServiceCollection services, IConfiguration config) => 19 | services 20 | .AddLocalization(options => options.ResourcesPath = "Resources") 21 | .AddBlazoredLocalStorage() 22 | .AddMudServices(configuration => 23 | { 24 | configuration.SnackbarConfiguration.PositionClass = Defaults.Classes.Position.BottomRight; 25 | configuration.SnackbarConfiguration.HideTransitionDuration = 100; 26 | configuration.SnackbarConfiguration.ShowTransitionDuration = 100; 27 | configuration.SnackbarConfiguration.VisibleStateDuration = 3000; 28 | configuration.SnackbarConfiguration.ShowCloseIcon = false; 29 | }) 30 | .AddScoped() 31 | .AutoRegisterInterfaces() 32 | .AutoRegisterInterfaces() 33 | .AddNotifications() 34 | .AddAuthentication(config) 35 | .AddAuthorizationCore(RegisterPermissionClaims) 36 | 37 | // Add Api Http Client. 38 | .AddHttpClient(ClientName, client => 39 | { 40 | client.DefaultRequestHeaders.AcceptLanguage.Clear(); 41 | client.DefaultRequestHeaders.AcceptLanguage.ParseAdd(CultureInfo.DefaultThreadCurrentCulture?.TwoLetterISOLanguageName); 42 | client.BaseAddress = new Uri(config[ConfigNames.ApiBaseUrl]); 43 | }) 44 | .AddAuthenticationHandler(config) 45 | .Services 46 | .AddScoped(sp => sp.GetRequiredService().CreateClient(ClientName)); 47 | 48 | private static void RegisterPermissionClaims(AuthorizationOptions options) 49 | { 50 | foreach (var permission in FSHPermissions.All) 51 | { 52 | options.AddPolicy(permission.Name, policy => policy.RequireClaim(FSHClaims.Permission, permission.Name)); 53 | } 54 | } 55 | 56 | public static IServiceCollection AutoRegisterInterfaces(this IServiceCollection services) 57 | { 58 | var @interface = typeof(T); 59 | 60 | var types = @interface 61 | .Assembly 62 | .GetExportedTypes() 63 | .Where(t => t.IsClass && !t.IsAbstract) 64 | .Select(t => new 65 | { 66 | Service = t.GetInterface($"I{t.Name}"), 67 | Implementation = t 68 | }) 69 | .Where(t => t.Service != null); 70 | 71 | foreach (var type in types) 72 | { 73 | if (@interface.IsAssignableFrom(type.Service)) 74 | { 75 | services.AddTransient(type.Service, type.Implementation); 76 | } 77 | } 78 | 79 | return services; 80 | } 81 | } -------------------------------------------------------------------------------- /src/Client.Infrastructure/Theme/CustomColors.cs: -------------------------------------------------------------------------------- 1 | using MudBlazor; 2 | 3 | namespace FSH.BlazorWebAssembly.Client.Infrastructure.Theme; 4 | 5 | public static class CustomColors 6 | { 7 | public static readonly List ThemeColors = new() 8 | { 9 | Light.Primary, 10 | Colors.Blue.Default, 11 | Colors.BlueGrey.Default, 12 | Colors.Purple.Default, 13 | Colors.Orange.Default, 14 | Colors.Red.Default, 15 | Colors.Amber.Default, 16 | Colors.DeepPurple.Default, 17 | Colors.Pink.Default, 18 | Colors.Indigo.Default, 19 | Colors.LightBlue.Default, 20 | Colors.Cyan.Default, 21 | }; 22 | 23 | public static class Light 24 | { 25 | public const string Primary = "#3eaf7c"; 26 | public const string Secondary = "#2196f3"; 27 | public const string Background = "#FFF"; 28 | public const string AppbarBackground = "#FFF"; 29 | public const string AppbarText = "#6e6e6e"; 30 | } 31 | 32 | public static class Dark 33 | { 34 | public const string Primary = "#3eaf7c"; 35 | public const string Secondary = "#2196f3"; 36 | public const string Background = "#1b1f22"; 37 | public const string AppbarBackground = "#1b1f22"; 38 | public const string DrawerBackground = "#121212"; 39 | public const string Surface = "#202528"; 40 | public const string Disabled = "#545454"; 41 | } 42 | } -------------------------------------------------------------------------------- /src/Client.Infrastructure/Theme/CustomTypography.cs: -------------------------------------------------------------------------------- 1 | using MudBlazor; 2 | 3 | namespace FSH.BlazorWebAssembly.Client.Infrastructure.Theme; 4 | 5 | public static class CustomTypography 6 | { 7 | public static Typography FSHTypography => new Typography() 8 | { 9 | Default = new Default() 10 | { 11 | FontFamily = new[] { "Montserrat", "Helvetica", "Arial", "sans-serif" }, 12 | FontSize = ".875rem", 13 | FontWeight = 200, 14 | LineHeight = 1.43, 15 | LetterSpacing = ".01071em" 16 | }, 17 | H1 = new H1() 18 | { 19 | FontFamily = new[] { "Montserrat", "Helvetica", "Arial", "sans-serif" }, 20 | FontSize = "6rem", 21 | FontWeight = 300, 22 | LineHeight = 1.167, 23 | LetterSpacing = "-.01562em" 24 | }, 25 | H2 = new H2() 26 | { 27 | FontFamily = new[] { "Montserrat", "Helvetica", "Arial", "sans-serif" }, 28 | FontSize = "3.75rem", 29 | FontWeight = 300, 30 | LineHeight = 1.2, 31 | LetterSpacing = "-.00833em" 32 | }, 33 | H3 = new H3() 34 | { 35 | FontFamily = new[] { "Montserrat", "Helvetica", "Arial", "sans-serif" }, 36 | FontSize = "3rem", 37 | FontWeight = 400, 38 | LineHeight = 1.167, 39 | LetterSpacing = "0" 40 | }, 41 | H4 = new H4() 42 | { 43 | FontFamily = new[] { "Montserrat", "Helvetica", "Arial", "sans-serif" }, 44 | FontSize = "2.125rem", 45 | FontWeight = 400, 46 | LineHeight = 1.235, 47 | LetterSpacing = ".00735em" 48 | }, 49 | H5 = new H5() 50 | { 51 | FontFamily = new[] { "Montserrat", "Helvetica", "Arial", "sans-serif" }, 52 | FontSize = "1.5rem", 53 | FontWeight = 400, 54 | LineHeight = 1.334, 55 | LetterSpacing = "0" 56 | }, 57 | H6 = new H6() 58 | { 59 | FontFamily = new[] { "Montserrat", "Helvetica", "Arial", "sans-serif" }, 60 | FontSize = "1.25rem", 61 | FontWeight = 400, 62 | LineHeight = 1.6, 63 | LetterSpacing = ".0075em" 64 | }, 65 | Button = new Button() 66 | { 67 | FontFamily = new[] { "Montserrat", "Helvetica", "Arial", "sans-serif" }, 68 | FontSize = ".875rem", 69 | FontWeight = 400, 70 | LineHeight = 1.75, 71 | LetterSpacing = ".02857em" 72 | }, 73 | Body1 = new Body1() 74 | { 75 | FontFamily = new[] { "Montserrat", "Helvetica", "Arial", "sans-serif" }, 76 | FontSize = "1rem", 77 | FontWeight = 400, 78 | LineHeight = 1.5, 79 | LetterSpacing = ".00938em" 80 | }, 81 | Body2 = new Body2() 82 | { 83 | FontFamily = new[] { "Montserrat", "Helvetica", "Arial", "sans-serif" }, 84 | FontSize = ".875rem", 85 | FontWeight = 400, 86 | LineHeight = 1.43, 87 | LetterSpacing = ".01071em" 88 | }, 89 | Caption = new Caption() 90 | { 91 | FontFamily = new[] { "Montserrat", "Helvetica", "Arial", "sans-serif" }, 92 | FontSize = ".75rem", 93 | FontWeight = 200, 94 | LineHeight = 1.66, 95 | LetterSpacing = ".03333em" 96 | }, 97 | Subtitle1 = new Subtitle1() 98 | { 99 | FontFamily = new[] { "Montserrat", "Helvetica", "Arial", "sans-serif" }, 100 | FontSize = "1rem", 101 | FontWeight = 400, 102 | LineHeight = 1.57, 103 | LetterSpacing = ".00714em" 104 | }, 105 | Subtitle2 = new Subtitle2() 106 | { 107 | FontFamily = new[] { "Montserrat", "Helvetica", "Arial", "sans-serif" }, 108 | FontSize = ".875rem", 109 | FontWeight = 400, 110 | LineHeight = 1.57, 111 | LetterSpacing = ".00714em" 112 | } 113 | }; 114 | } -------------------------------------------------------------------------------- /src/Client.Infrastructure/Theme/DarkTheme.cs: -------------------------------------------------------------------------------- 1 | using MudBlazor; 2 | 3 | namespace FSH.BlazorWebAssembly.Client.Infrastructure.Theme; 4 | 5 | public class DarkTheme : MudTheme 6 | { 7 | public DarkTheme() 8 | { 9 | Palette = new Palette() 10 | { 11 | Primary = CustomColors.Dark.Primary, 12 | Secondary = CustomColors.Dark.Secondary, 13 | Success = CustomColors.Dark.Primary, 14 | Black = "#27272f", 15 | Background = CustomColors.Dark.Background, 16 | BackgroundGrey = "#27272f", 17 | Surface = CustomColors.Dark.Surface, 18 | DrawerBackground = CustomColors.Dark.DrawerBackground, 19 | DrawerText = "rgba(255,255,255, 0.50)", 20 | AppbarBackground = CustomColors.Dark.AppbarBackground, 21 | AppbarText = "rgba(255,255,255, 0.70)", 22 | TextPrimary = "rgba(255,255,255, 0.70)", 23 | TextSecondary = "rgba(255,255,255, 0.50)", 24 | ActionDefault = "#adadb1", 25 | ActionDisabled = "rgba(255,255,255, 0.26)", 26 | ActionDisabledBackground = "rgba(255,255,255, 0.12)", 27 | DrawerIcon = "rgba(255,255,255, 0.50)", 28 | TableLines = "#e0e0e036", 29 | Dark = CustomColors.Dark.DrawerBackground, 30 | Divider = "#e0e0e036", 31 | OverlayDark = "hsl(0deg 0% 0% / 75%)", 32 | TextDisabled = CustomColors.Dark.Disabled 33 | }; 34 | 35 | LayoutProperties = new LayoutProperties() 36 | { 37 | DefaultBorderRadius = "5px", 38 | }; 39 | 40 | Typography = CustomTypography.FSHTypography; 41 | Shadows = new Shadow(); 42 | ZIndex = new ZIndex() { Drawer = 1300 }; 43 | } 44 | } -------------------------------------------------------------------------------- /src/Client.Infrastructure/Theme/LightTheme.cs: -------------------------------------------------------------------------------- 1 | using MudBlazor; 2 | 3 | namespace FSH.BlazorWebAssembly.Client.Infrastructure.Theme; 4 | 5 | public class LightTheme : MudTheme 6 | { 7 | public LightTheme() 8 | { 9 | Palette = new Palette() 10 | { 11 | Primary = CustomColors.Light.Primary, 12 | Secondary = CustomColors.Light.Secondary, 13 | Background = CustomColors.Light.Background, 14 | AppbarBackground = CustomColors.Light.AppbarBackground, 15 | AppbarText = CustomColors.Light.AppbarText, 16 | DrawerBackground = CustomColors.Light.Background, 17 | DrawerText = "rgba(0,0,0, 0.7)", 18 | Success = CustomColors.Light.Primary, 19 | TableLines = "#e0e0e029", 20 | OverlayDark = "hsl(0deg 0% 0% / 75%)" 21 | }; 22 | LayoutProperties = new LayoutProperties() 23 | { 24 | DefaultBorderRadius = "5px" 25 | }; 26 | 27 | Typography = CustomTypography.FSHTypography; 28 | Shadows = new Shadow(); 29 | ZIndex = new ZIndex() { Drawer = 1300 }; 30 | } 31 | } -------------------------------------------------------------------------------- /src/Client/App.razor: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | @if (@context.User.Identity?.IsAuthenticated is true) 7 | { 8 |

You are not authorized to be here.

9 | } 10 | else 11 | { 12 | 13 | } 14 |
15 |
16 |
17 | 18 | 19 |

Sorry, there's nothing at this address.

20 |
21 |
22 |
23 |
-------------------------------------------------------------------------------- /src/Client/Client.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net6.0 5 | FSH.BlazorWebAssembly.Client 6 | FSH.BlazorWebAssembly.Client 7 | service-worker-assets.js 8 | true 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /src/Client/Components/Common/CustomValidation.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Components; 2 | using Microsoft.AspNetCore.Components.Forms; 3 | 4 | namespace FSH.BlazorWebAssembly.Client.Components.Common; 5 | 6 | // See https://docs.microsoft.com/en-us/aspnet/core/blazor/forms-validation?view=aspnetcore-6.0#server-validation-with-a-validator-component 7 | public class CustomValidation : ComponentBase 8 | { 9 | private ValidationMessageStore? _messageStore; 10 | 11 | [CascadingParameter] 12 | private EditContext? CurrentEditContext { get; set; } 13 | 14 | protected override void OnInitialized() 15 | { 16 | if (CurrentEditContext is null) 17 | { 18 | throw new InvalidOperationException( 19 | $"{nameof(CustomValidation)} requires a cascading " + 20 | $"parameter of type {nameof(EditContext)}. " + 21 | $"For example, you can use {nameof(CustomValidation)} " + 22 | $"inside an {nameof(EditForm)}."); 23 | } 24 | 25 | _messageStore = new(CurrentEditContext); 26 | 27 | CurrentEditContext.OnValidationRequested += (s, e) => 28 | _messageStore?.Clear(); 29 | CurrentEditContext.OnFieldChanged += (s, e) => 30 | _messageStore?.Clear(e.FieldIdentifier); 31 | } 32 | 33 | public void DisplayErrors(IDictionary> errors) 34 | { 35 | if (CurrentEditContext is not null && errors is not null) 36 | { 37 | foreach (var err in errors) 38 | { 39 | _messageStore?.Add(CurrentEditContext.Field(err.Key), err.Value); 40 | } 41 | 42 | CurrentEditContext.NotifyValidationStateChanged(); 43 | } 44 | } 45 | 46 | public void ClearErrors() 47 | { 48 | _messageStore?.Clear(); 49 | CurrentEditContext?.NotifyValidationStateChanged(); 50 | } 51 | } -------------------------------------------------------------------------------- /src/Client/Components/Common/ErrorHandler.razor: -------------------------------------------------------------------------------- 1 | @inherits ErrorBoundary 2 | @ChildContent -------------------------------------------------------------------------------- /src/Client/Components/Common/ErrorHandler.razor.cs: -------------------------------------------------------------------------------- 1 | using FSH.BlazorWebAssembly.Client.Infrastructure.Auth; 2 | using Microsoft.AspNetCore.Components; 3 | using MudBlazor; 4 | 5 | namespace FSH.BlazorWebAssembly.Client.Components.Common; 6 | 7 | public partial class ErrorHandler 8 | { 9 | [Inject] 10 | public IAuthenticationService AuthService { get; set; } = default!; 11 | 12 | public List _receivedExceptions = new(); 13 | 14 | protected override async Task OnErrorAsync(Exception exception) 15 | { 16 | _receivedExceptions.Add(exception); 17 | switch (exception) 18 | { 19 | case UnauthorizedAccessException: 20 | await AuthService.LogoutAsync(); 21 | Snackbar.Add("Authentication Failed", Severity.Error); 22 | break; 23 | } 24 | } 25 | 26 | public new void Recover() 27 | { 28 | _receivedExceptions.Clear(); 29 | base.Recover(); 30 | } 31 | } -------------------------------------------------------------------------------- /src/Client/Components/Common/FshCustomError.razor: -------------------------------------------------------------------------------- 1 | Oopsie !! 😔 -------------------------------------------------------------------------------- /src/Client/Components/Common/FshTable.cs: -------------------------------------------------------------------------------- 1 | using FSH.BlazorWebAssembly.Client.Infrastructure.Notifications; 2 | using FSH.BlazorWebAssembly.Client.Infrastructure.Preferences; 3 | using MediatR.Courier; 4 | using Microsoft.AspNetCore.Components; 5 | using MudBlazor; 6 | 7 | namespace FSH.BlazorWebAssembly.Client.Components.Common; 8 | 9 | public class FshTable : MudTable 10 | { 11 | [Inject] 12 | private IClientPreferenceManager ClientPreferences { get; set; } = default!; 13 | [Inject] 14 | protected ICourier Courier { get; set; } = default!; 15 | 16 | protected override async Task OnInitializedAsync() 17 | { 18 | if (await ClientPreferences.GetPreference() is ClientPreference clientPreference) 19 | { 20 | SetTablePreference(clientPreference.TablePreference); 21 | } 22 | 23 | Courier.SubscribeWeak>(wrapper => 24 | { 25 | SetTablePreference(wrapper.Notification); 26 | StateHasChanged(); 27 | }); 28 | 29 | await base.OnInitializedAsync(); 30 | } 31 | 32 | private void SetTablePreference(FshTablePreference tablePreference) 33 | { 34 | Dense = tablePreference.IsDense; 35 | Striped = tablePreference.IsStriped; 36 | Bordered = tablePreference.HasBorder; 37 | Hover = tablePreference.IsHoverable; 38 | } 39 | } -------------------------------------------------------------------------------- /src/Client/Components/Common/FshTitle.razor: -------------------------------------------------------------------------------- 1 | @Title 2 | @Description 3 | 4 | @code 5 | { 6 | [Parameter] public string? Title { get; set; } 7 | [Parameter] public string? Description { get; set; } 8 | } -------------------------------------------------------------------------------- /src/Client/Components/Common/PersonCard.razor: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | @if (string.IsNullOrEmpty(this.ImageUri)) 5 | { 6 | @FullName?.ToUpper().FirstOrDefault() 7 | 8 | } 9 | else 10 | { 11 | 12 | } 13 | 14 | 15 | @FullName 16 | @Email 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/Client/Components/Common/PersonCard.razor.cs: -------------------------------------------------------------------------------- 1 | using System.Security.Claims; 2 | using FSH.BlazorWebAssembly.Client.Infrastructure.Common; 3 | using Microsoft.AspNetCore.Components; 4 | using Microsoft.AspNetCore.Components.Authorization; 5 | 6 | namespace FSH.BlazorWebAssembly.Client.Components.Common; 7 | 8 | public partial class PersonCard 9 | { 10 | [Parameter] 11 | public string? Class { get; set; } 12 | [Parameter] 13 | public string? Style { get; set; } 14 | 15 | [CascadingParameter] 16 | protected Task AuthState { get; set; } = default!; 17 | 18 | private string? UserId { get; set; } 19 | private string? Email { get; set; } 20 | private string? FullName { get; set; } 21 | private string? ImageUri { get; set; } 22 | 23 | protected override async Task OnAfterRenderAsync(bool firstRender) 24 | { 25 | if (firstRender) 26 | { 27 | await LoadUserData(); 28 | } 29 | } 30 | 31 | private async Task LoadUserData() 32 | { 33 | var user = (await AuthState).User; 34 | if (user.Identity?.IsAuthenticated == true) 35 | { 36 | if (string.IsNullOrEmpty(UserId)) 37 | { 38 | FullName = user.GetFullName(); 39 | UserId = user.GetUserId(); 40 | Email = user.GetEmail(); 41 | ImageUri = string.IsNullOrEmpty(user?.GetImageUrl()) ? string.Empty : (Config[ConfigNames.ApiBaseUrl] + user?.GetImageUrl()); 42 | StateHasChanged(); 43 | } 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /src/Client/Components/Common/TablePager.razor: -------------------------------------------------------------------------------- 1 | @inject IStringLocalizer L 2 | 3 | -------------------------------------------------------------------------------- /src/Client/Components/Dialogs/DeleteConfirmation.razor: -------------------------------------------------------------------------------- 1 | @inject IStringLocalizer L 2 | 3 | 4 | 5 | 6 | 7 | @L["Delete Confirmation"] 8 | 9 | 10 | 11 | @ContentText 12 | 13 | 14 | @L["Cancel"] 15 | @L["Confirm"] 16 | 17 | 18 | 19 | @code { 20 | [CascadingParameter] 21 | MudDialogInstance MudDialog { get; set; } = default!; 22 | 23 | [Parameter] 24 | public string? ContentText { get; set; } 25 | 26 | void Submit() 27 | { 28 | MudDialog.Close(DialogResult.Ok(true)); 29 | } 30 | void Cancel() => MudDialog.Cancel(); 31 | } -------------------------------------------------------------------------------- /src/Client/Components/Dialogs/Logout.razor: -------------------------------------------------------------------------------- 1 | @namespace FSH.BlazorWebAssembly.Client.Shared.Dialogs 2 | 3 | @inject IStringLocalizer L 4 | @inject IAuthenticationService AuthService 5 | 6 | 7 | 8 | 9 | 10 | @L["Logout Confirmation"] 11 | 12 | 13 | 14 | @ContentText 15 | 16 | 17 | @L["Cancel"] 18 | @ButtonText 19 | 20 | 21 | 22 | @code { 23 | [Parameter] public string? ContentText { get; set; } 24 | 25 | [Parameter] public string? ButtonText { get; set; } 26 | 27 | [Parameter] public Color Color { get; set; } 28 | 29 | [CascadingParameter] MudDialogInstance MudDialog { get; set; } = default!; 30 | 31 | async Task Submit() 32 | { 33 | await AuthService.LogoutAsync(); 34 | Snackbar.Add(@L["Logged out"], Severity.Info); 35 | MudDialog.Close(DialogResult.Ok(true)); 36 | } 37 | 38 | void Cancel() => 39 | MudDialog.Cancel(); 40 | } -------------------------------------------------------------------------------- /src/Client/Components/EntityTable/AddEditModal.razor: -------------------------------------------------------------------------------- 1 | @typeparam TRequest 2 | 3 | @inject IStringLocalizer L 4 | 5 | 6 | 7 | 8 | 9 | 10 | @if (IsCreate) 11 | { 12 | 13 | } 14 | else 15 | { 16 | 17 | } 18 | @Title 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | @ChildContent(RequestModel) 28 | 29 | 30 | 31 | 32 | 33 | 34 | @L["Cancel"] 35 | 36 | @if (IsCreate) 37 | { 38 | 39 | @L["Save"] 40 | 41 | } 42 | else 43 | { 44 | 45 | @L["Update"] 46 | 47 | } 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /src/Client/Components/EntityTable/AddEditModal.razor.cs: -------------------------------------------------------------------------------- 1 | using FSH.BlazorWebAssembly.Client.Components.Common; 2 | using FSH.BlazorWebAssembly.Client.Shared; 3 | using Microsoft.AspNetCore.Components; 4 | using MudBlazor; 5 | 6 | namespace FSH.BlazorWebAssembly.Client.Components.EntityTable; 7 | 8 | public partial class AddEditModal : IAddEditModal 9 | { 10 | [Parameter] 11 | [EditorRequired] 12 | public RenderFragment ChildContent { get; set; } = default!; 13 | [Parameter] 14 | [EditorRequired] 15 | public TRequest RequestModel { get; set; } = default!; 16 | [Parameter] 17 | [EditorRequired] 18 | public Func SaveFunc { get; set; } = default!; 19 | [Parameter] 20 | public Func? OnInitializedFunc { get; set; } 21 | [Parameter] 22 | [EditorRequired] 23 | public string Title { get; set; } = default!; 24 | [Parameter] 25 | public bool IsCreate { get; set; } 26 | [Parameter] 27 | public string? SuccessMessage { get; set; } 28 | 29 | [CascadingParameter] 30 | private MudDialogInstance MudDialog { get; set; } = default!; 31 | 32 | private CustomValidation? _customValidation; 33 | 34 | public void ForceRender() => StateHasChanged(); 35 | 36 | protected override Task OnInitializedAsync() => 37 | OnInitializedFunc is not null 38 | ? OnInitializedFunc() 39 | : Task.CompletedTask; 40 | 41 | private async Task SaveAsync() 42 | { 43 | if (await ApiHelper.ExecuteCallGuardedAsync( 44 | () => SaveFunc(RequestModel), Snackbar, _customValidation, SuccessMessage)) 45 | { 46 | MudDialog.Close(); 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /src/Client/Components/EntityTable/EntityClientTableContext.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.BlazorWebAssembly.Client.Components.EntityTable; 2 | 3 | /// 4 | /// Initialization Context for the EntityTable Component. 5 | /// Use this one if you want to use Client Paging, Sorting and Filtering. 6 | /// 7 | public class EntityClientTableContext 8 | : EntityTableContext 9 | { 10 | /// 11 | /// A function that loads all the data for the table from the api and returns a ListResult of TEntity. 12 | /// 13 | public Func?>> LoadDataFunc { get; } 14 | 15 | /// 16 | /// A function that returns a boolean which indicates whether the supplied entity meets the search criteria 17 | /// (the supplied string is the search string entered). 18 | /// 19 | public Func SearchFunc { get; } 20 | 21 | public EntityClientTableContext( 22 | List> fields, 23 | Func?>> loadDataFunc, 24 | Func searchFunc, 25 | Func? idFunc = null, 26 | Func>? getDefaultsFunc = null, 27 | Func? createFunc = null, 28 | Func>? getDetailsFunc = null, 29 | Func? updateFunc = null, 30 | Func? deleteFunc = null, 31 | string? entityName = null, 32 | string? entityNamePlural = null, 33 | string? entityResource = null, 34 | string? searchAction = null, 35 | string? createAction = null, 36 | string? updateAction = null, 37 | string? deleteAction = null, 38 | string? exportAction = null, 39 | Func? editFormInitializedFunc = null, 40 | Func? hasExtraActionsFunc = null, 41 | Func? canUpdateEntityFunc = null, 42 | Func? canDeleteEntityFunc = null) 43 | : base( 44 | fields, 45 | idFunc, 46 | getDefaultsFunc, 47 | createFunc, 48 | getDetailsFunc, 49 | updateFunc, 50 | deleteFunc, 51 | entityName, 52 | entityNamePlural, 53 | entityResource, 54 | searchAction, 55 | createAction, 56 | updateAction, 57 | deleteAction, 58 | exportAction, 59 | editFormInitializedFunc, 60 | hasExtraActionsFunc, 61 | canUpdateEntityFunc, 62 | canDeleteEntityFunc) 63 | { 64 | LoadDataFunc = loadDataFunc; 65 | SearchFunc = searchFunc; 66 | } 67 | } -------------------------------------------------------------------------------- /src/Client/Components/EntityTable/EntityField.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Components; 2 | 3 | namespace FSH.BlazorWebAssembly.Client.Components.EntityTable; 4 | 5 | public record EntityField(Func ValueFunc, string DisplayName, string SortLabel = "", Type? Type = null, RenderFragment? Template = null) 6 | { 7 | /// 8 | /// A function that returns the actual value of this field from the supplied entity. 9 | /// 10 | public Func ValueFunc { get; init; } = ValueFunc; 11 | 12 | /// 13 | /// The string that's shown on the UI for this field. 14 | /// 15 | public string DisplayName { get; init; } = DisplayName; 16 | 17 | /// 18 | /// The string that's sent to the api as property to sort on for this field. 19 | /// This is only relevant when using server side sorting. 20 | /// 21 | public string SortLabel { get; init; } = SortLabel; 22 | 23 | /// 24 | /// The type of the field. Default is string, but when boolean, it shows as a checkbox. 25 | /// 26 | public Type? Type { get; init; } = Type; 27 | 28 | /// 29 | /// When supplied this template will be used for this field in stead of the default template. 30 | /// For an example on how to do this, see . 31 | /// 32 | public RenderFragment? Template { get; init; } = Template; 33 | 34 | public bool CheckedForSearch { get; set; } = true; 35 | } -------------------------------------------------------------------------------- /src/Client/Components/EntityTable/EntityServerTableContext.cs: -------------------------------------------------------------------------------- 1 | using FSH.BlazorWebAssembly.Client.Infrastructure.ApiClient; 2 | 3 | namespace FSH.BlazorWebAssembly.Client.Components.EntityTable; 4 | 5 | /// 6 | /// Initialization Context for the EntityTable Component. 7 | /// Use this one if you want to use Server Paging, Sorting and Filtering. 8 | /// 9 | public class EntityServerTableContext 10 | : EntityTableContext 11 | { 12 | /// 13 | /// A function that loads the specified page from the api with the specified search criteria 14 | /// and returns a PaginatedResult of TEntity. 15 | /// 16 | public Func>> SearchFunc { get; } 17 | 18 | /// 19 | /// A function that exports the specified data from the API. 20 | /// 21 | public Func>? ExportFunc { get; } 22 | 23 | public bool EnableAdvancedSearch { get; } 24 | 25 | public EntityServerTableContext( 26 | List> fields, 27 | Func>> searchFunc, 28 | Func>? exportFunc = null, 29 | bool enableAdvancedSearch = false, 30 | Func? idFunc = null, 31 | Func>? getDefaultsFunc = null, 32 | Func? createFunc = null, 33 | Func>? getDetailsFunc = null, 34 | Func? updateFunc = null, 35 | Func? deleteFunc = null, 36 | string? entityName = null, 37 | string? entityNamePlural = null, 38 | string? entityResource = null, 39 | string? searchAction = null, 40 | string? createAction = null, 41 | string? updateAction = null, 42 | string? deleteAction = null, 43 | string? exportAction = null, 44 | Func? editFormInitializedFunc = null, 45 | Func? hasExtraActionsFunc = null, 46 | Func? canUpdateEntityFunc = null, 47 | Func? canDeleteEntityFunc = null) 48 | : base( 49 | fields, 50 | idFunc, 51 | getDefaultsFunc, 52 | createFunc, 53 | getDetailsFunc, 54 | updateFunc, 55 | deleteFunc, 56 | entityName, 57 | entityNamePlural, 58 | entityResource, 59 | searchAction, 60 | createAction, 61 | updateAction, 62 | deleteAction, 63 | exportAction, 64 | editFormInitializedFunc, 65 | hasExtraActionsFunc, 66 | canUpdateEntityFunc, 67 | canDeleteEntityFunc) 68 | { 69 | SearchFunc = searchFunc; 70 | ExportFunc = exportFunc; 71 | EnableAdvancedSearch = enableAdvancedSearch; 72 | } 73 | } -------------------------------------------------------------------------------- /src/Client/Components/EntityTable/IAddEditModal.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.BlazorWebAssembly.Client.Components.EntityTable; 2 | 3 | public interface IAddEditModal 4 | { 5 | TRequest RequestModel { get; } 6 | bool IsCreate { get; } 7 | void ForceRender(); 8 | } -------------------------------------------------------------------------------- /src/Client/Components/EntityTable/PaginationResponse.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.BlazorWebAssembly.Client.Components.EntityTable; 2 | 3 | public class PaginationResponse 4 | { 5 | public List Data { get; set; } = default!; 6 | public int TotalCount { get; set; } 7 | public int CurrentPage { get; set; } = 1; 8 | public int PageSize { get; set; } = 10; 9 | } 10 | -------------------------------------------------------------------------------- /src/Client/Components/Localization/LanguageSelector.razor: -------------------------------------------------------------------------------- 1 | @inject IStringLocalizer L 2 | 3 | 4 | 6 | @foreach (var language in LocalizationConstants.SupportedLanguages) 7 | { 8 | if (language.Code == CurrentLanguage) 9 | { 10 | @L[language.DisplayName] 11 | } 12 | else 13 | { 14 | @L[language.DisplayName] 15 | } 16 | } 17 | 18 | 19 | 20 | @code { 21 | public string? CurrentLanguage { get; set; } = "en-US"; 22 | 23 | protected override async Task OnInitializedAsync() 24 | { 25 | var currentPreference = await ClientPreferences.GetPreference() as ClientPreference; 26 | if (currentPreference != null) 27 | { 28 | CurrentLanguage = currentPreference.LanguageCode; 29 | } 30 | else 31 | { 32 | CurrentLanguage = "en-US"; 33 | } 34 | 35 | } 36 | private async Task ChangeLanguageAsync(string languageCode) 37 | { 38 | var result = await ClientPreferences.ChangeLanguageAsync(languageCode); 39 | Navigation.NavigateTo(Navigation.Uri, forceLoad: true); 40 | } 41 | } -------------------------------------------------------------------------------- /src/Client/Components/Notifications/NotificationConnection.razor: -------------------------------------------------------------------------------- 1 |  2 | @ChildContent 3 | -------------------------------------------------------------------------------- /src/Client/Components/Notifications/NotificationConnectionStatus.razor: -------------------------------------------------------------------------------- 1 | @inject ICourier Courier 2 | 3 | 4 | 5 | 6 | 7 | @code { 8 | public string TooltipText { get; set; } = "No Connection"; 9 | public string Icon { get; set; } = Icons.Filled.SignalWifi0Bar; 10 | public Color IconColor { get; set; } = Color.Error; 11 | 12 | [CascadingParameter] private NotificationConnection _notifications { get; set; } = default!; 13 | 14 | protected override void OnInitialized() 15 | { 16 | SetConnectionState(_notifications.ConnectionState, _notifications.ConnectionId); 17 | 18 | Courier.SubscribeWeak>(wrapper => 19 | { 20 | SetConnectionState(wrapper.Notification.State, wrapper.Notification.Message); 21 | StateHasChanged(); 22 | }); 23 | } 24 | 25 | private void SetConnectionState(ConnectionState state, string? message) 26 | { 27 | switch (state) 28 | { 29 | case ConnectionState.Connected: 30 | TooltipText = $"Connected to Server with ConnectionId {message}"; 31 | Icon = @Icons.Filled.SignalWifiStatusbar4Bar; 32 | IconColor = Color.Success; 33 | break; 34 | 35 | case ConnectionState.Connecting: 36 | TooltipText = $"(Re)connecting... ({message})."; 37 | Icon = @Icons.Filled.SignalWifiStatusbarConnectedNoInternet4; 38 | IconColor = Color.Warning; 39 | break; 40 | 41 | case ConnectionState.Disconnected: 42 | TooltipText = $"Connection Closed ({message})."; 43 | Icon = @Icons.Filled.SignalWifiOff; 44 | IconColor = Color.Error; 45 | break; 46 | } 47 | StateHasChanged(); 48 | } 49 | } -------------------------------------------------------------------------------- /src/Client/Components/ThemeManager/ColorPanel.razor: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | @ColorType 5 | 6 | 7 |
8 |
9 | 10 | 11 | 12 | 13 | @foreach (var color in Colors) 14 | { 15 | 17 | 18 |
19 |
20 |
21 |
22 | } 23 |
24 |
25 |
26 |
27 |
-------------------------------------------------------------------------------- /src/Client/Components/ThemeManager/ColorPanel.razor.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Components; 2 | using MudBlazor; 3 | 4 | namespace FSH.BlazorWebAssembly.Client.Components.ThemeManager; 5 | 6 | public partial class ColorPanel 7 | { 8 | [Parameter] 9 | public List Colors { get; set; } = new(); 10 | 11 | [Parameter] 12 | public string ColorType { get; set; } = string.Empty; 13 | 14 | [Parameter] 15 | public Color CurrentColor { get; set; } 16 | 17 | [Parameter] 18 | public EventCallback OnColorClicked { get; set; } 19 | 20 | protected async Task ColorClicked(string color) 21 | { 22 | await OnColorClicked.InvokeAsync(color); 23 | } 24 | } -------------------------------------------------------------------------------- /src/Client/Components/ThemeManager/DarkModePanel.razor: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | @if (_isDarkMode) 5 | { 6 | Light Mode 7 | } 8 | else 9 | { 10 | Dark Mode 11 | } 12 | 15 |
16 |
17 |
-------------------------------------------------------------------------------- /src/Client/Components/ThemeManager/DarkModePanel.razor.cs: -------------------------------------------------------------------------------- 1 | using FSH.BlazorWebAssembly.Client.Infrastructure.Preferences; 2 | using Microsoft.AspNetCore.Components; 3 | 4 | namespace FSH.BlazorWebAssembly.Client.Components.ThemeManager; 5 | 6 | public partial class DarkModePanel 7 | { 8 | private bool _isDarkMode; 9 | 10 | protected override async Task OnInitializedAsync() 11 | { 12 | if (await ClientPreferences.GetPreference() is not ClientPreference themePreference) themePreference = new ClientPreference(); 13 | _isDarkMode = themePreference.IsDarkMode; 14 | } 15 | 16 | [Parameter] 17 | public EventCallback OnIconClicked { get; set; } 18 | 19 | private async Task ToggleDarkMode() 20 | { 21 | _isDarkMode = !_isDarkMode; 22 | await OnIconClicked.InvokeAsync(_isDarkMode); 23 | } 24 | } -------------------------------------------------------------------------------- /src/Client/Components/ThemeManager/RadiusPanel.razor: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | Border Radius 5 | @Radius.ToString() 7 | 8 |
9 |
10 | 11 | 13 | @Radius.ToString() 14 | 15 |
-------------------------------------------------------------------------------- /src/Client/Components/ThemeManager/RadiusPanel.razor.cs: -------------------------------------------------------------------------------- 1 | using FSH.BlazorWebAssembly.Client.Infrastructure.Preferences; 2 | using Microsoft.AspNetCore.Components; 3 | 4 | namespace FSH.BlazorWebAssembly.Client.Components.ThemeManager; 5 | 6 | public partial class RadiusPanel 7 | { 8 | [Parameter] 9 | public double Radius { get; set; } 10 | 11 | [Parameter] 12 | public double MaxValue { get; set; } = 30; 13 | 14 | [Parameter] 15 | public EventCallback OnSliderChanged { get; set; } 16 | 17 | protected override async Task OnInitializedAsync() 18 | { 19 | if (await ClientPreferences.GetPreference() is not ClientPreference themePreference) themePreference = new ClientPreference(); 20 | Radius = themePreference.BorderRadius; 21 | } 22 | 23 | private async Task ChangedSelection(ChangeEventArgs args) 24 | { 25 | Radius = int.Parse(args?.Value?.ToString() ?? "0"); 26 | await OnSliderChanged.InvokeAsync(Radius); 27 | } 28 | } -------------------------------------------------------------------------------- /src/Client/Components/ThemeManager/TableCustomizationPanel.razor: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | Table Customization 5 | T 7 | 8 |
9 |
10 | 11 | 12 | 14 | 16 | 18 | 19 |
-------------------------------------------------------------------------------- /src/Client/Components/ThemeManager/TableCustomizationPanel.razor.cs: -------------------------------------------------------------------------------- 1 | using FSH.BlazorWebAssembly.Client.Infrastructure.Notifications; 2 | using FSH.BlazorWebAssembly.Client.Infrastructure.Preferences; 3 | using Microsoft.AspNetCore.Components; 4 | 5 | namespace FSH.BlazorWebAssembly.Client.Components.ThemeManager; 6 | 7 | public partial class TableCustomizationPanel 8 | { 9 | [Parameter] 10 | public bool IsDense { get; set; } 11 | [Parameter] 12 | public bool IsStriped { get; set; } 13 | [Parameter] 14 | public bool HasBorder { get; set; } 15 | [Parameter] 16 | public bool IsHoverable { get; set; } 17 | [Inject] 18 | protected INotificationPublisher Notifications { get; set; } = default!; 19 | 20 | private FshTablePreference _tablePreference = new(); 21 | 22 | protected override async Task OnInitializedAsync() 23 | { 24 | if (await ClientPreferences.GetPreference() is ClientPreference clientPreference) 25 | { 26 | _tablePreference = clientPreference.TablePreference; 27 | } 28 | 29 | IsDense = _tablePreference.IsDense; 30 | IsStriped = _tablePreference.IsStriped; 31 | HasBorder = _tablePreference.HasBorder; 32 | IsHoverable = _tablePreference.IsHoverable; 33 | } 34 | 35 | [Parameter] 36 | public EventCallback OnDenseSwitchToggled { get; set; } 37 | 38 | [Parameter] 39 | public EventCallback OnStripedSwitchToggled { get; set; } 40 | 41 | [Parameter] 42 | public EventCallback OnBorderdedSwitchToggled { get; set; } 43 | 44 | [Parameter] 45 | public EventCallback OnHoverableSwitchToggled { get; set; } 46 | 47 | private async Task ToggleDenseSwitch() 48 | { 49 | _tablePreference.IsDense = !_tablePreference.IsDense; 50 | await OnDenseSwitchToggled.InvokeAsync(_tablePreference.IsDense); 51 | await Notifications.PublishAsync(_tablePreference); 52 | } 53 | 54 | private async Task ToggleStripedSwitch() 55 | { 56 | _tablePreference.IsStriped = !_tablePreference.IsStriped; 57 | await OnStripedSwitchToggled.InvokeAsync(_tablePreference.IsStriped); 58 | await Notifications.PublishAsync(_tablePreference); 59 | } 60 | 61 | private async Task ToggleBorderedSwitch() 62 | { 63 | _tablePreference.HasBorder = !_tablePreference.HasBorder; 64 | await OnBorderdedSwitchToggled.InvokeAsync(_tablePreference.HasBorder); 65 | await Notifications.PublishAsync(_tablePreference); 66 | } 67 | 68 | private async Task ToggleHoverableSwitch() 69 | { 70 | _tablePreference.IsHoverable = !_tablePreference.IsHoverable; 71 | await OnHoverableSwitchToggled.InvokeAsync(_tablePreference.IsHoverable); 72 | await Notifications.PublishAsync(_tablePreference); 73 | } 74 | } -------------------------------------------------------------------------------- /src/Client/Components/ThemeManager/ThemeButton.razor: -------------------------------------------------------------------------------- 1 | 
2 | 3 | 4 | 5 |
6 | 7 | -------------------------------------------------------------------------------- /src/Client/Components/ThemeManager/ThemeButton.razor.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Components; 2 | using Microsoft.AspNetCore.Components.Web; 3 | 4 | namespace FSH.BlazorWebAssembly.Client.Components.ThemeManager; 5 | 6 | public partial class ThemeButton 7 | { 8 | [Parameter] 9 | public EventCallback OnClick { get; set; } 10 | } -------------------------------------------------------------------------------- /src/Client/Components/ThemeManager/ThemeDrawer.razor: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | Theme Manager 6 | 7 | 8 | 10 | 11 | 12 |
13 | 14 | 15 | 17 | 19 | 21 | 24 | 25 |
26 |
27 | -------------------------------------------------------------------------------- /src/Client/Components/ThemeManager/ThemeDrawer.razor.cs: -------------------------------------------------------------------------------- 1 | using FSH.BlazorWebAssembly.Client.Infrastructure.Preferences; 2 | using FSH.BlazorWebAssembly.Client.Infrastructure.Theme; 3 | using Microsoft.AspNetCore.Components; 4 | 5 | namespace FSH.BlazorWebAssembly.Client.Components.ThemeManager; 6 | 7 | public partial class ThemeDrawer 8 | { 9 | [Parameter] 10 | public bool ThemeDrawerOpen { get; set; } 11 | 12 | [Parameter] 13 | public EventCallback ThemeDrawerOpenChanged { get; set; } 14 | 15 | [EditorRequired] 16 | [Parameter] 17 | public ClientPreference ThemePreference { get; set; } = default!; 18 | 19 | [EditorRequired] 20 | [Parameter] 21 | public EventCallback ThemePreferenceChanged { get; set; } 22 | 23 | private readonly List _colors = CustomColors.ThemeColors; 24 | 25 | private async Task UpdateThemePrimaryColor(string color) 26 | { 27 | if (ThemePreference is not null) 28 | { 29 | ThemePreference.PrimaryColor = color; 30 | await ThemePreferenceChanged.InvokeAsync(ThemePreference); 31 | } 32 | } 33 | 34 | private async Task UpdateThemeSecondaryColor(string color) 35 | { 36 | if (ThemePreference is not null) 37 | { 38 | ThemePreference.SecondaryColor = color; 39 | await ThemePreferenceChanged.InvokeAsync(ThemePreference); 40 | } 41 | } 42 | 43 | private async Task UpdateBorderRadius(double radius) 44 | { 45 | if (ThemePreference is not null) 46 | { 47 | ThemePreference.BorderRadius = radius; 48 | await ThemePreferenceChanged.InvokeAsync(ThemePreference); 49 | } 50 | } 51 | 52 | private async Task ToggleDarkLightMode(bool isDarkMode) 53 | { 54 | if (ThemePreference is not null) 55 | { 56 | ThemePreference.IsDarkMode = isDarkMode; 57 | await ThemePreferenceChanged.InvokeAsync(ThemePreference); 58 | } 59 | } 60 | 61 | private async Task ToggleEntityTableDense(bool isDense) 62 | { 63 | if (ThemePreference is not null) 64 | { 65 | ThemePreference.TablePreference.IsDense = isDense; 66 | await ThemePreferenceChanged.InvokeAsync(ThemePreference); 67 | } 68 | } 69 | 70 | private async Task ToggleEntityTableStriped(bool isStriped) 71 | { 72 | if (ThemePreference is not null) 73 | { 74 | ThemePreference.TablePreference.IsStriped = isStriped; 75 | await ThemePreferenceChanged.InvokeAsync(ThemePreference); 76 | } 77 | } 78 | 79 | private async Task ToggleEntityTableBorder(bool hasBorder) 80 | { 81 | if (ThemePreference is not null) 82 | { 83 | ThemePreference.TablePreference.HasBorder = hasBorder; 84 | await ThemePreferenceChanged.InvokeAsync(ThemePreference); 85 | } 86 | } 87 | 88 | private async Task ToggleEntityTableHoverable(bool isHoverable) 89 | { 90 | if (ThemePreference is not null) 91 | { 92 | ThemePreference.TablePreference.IsHoverable = isHoverable; 93 | await ThemePreferenceChanged.InvokeAsync(ThemePreference); 94 | } 95 | } 96 | } -------------------------------------------------------------------------------- /src/Client/Pages/Authentication/Authentication.razor: -------------------------------------------------------------------------------- 1 | @page "/authentication/{action}" 2 | @attribute [AllowAnonymous] 3 | 4 | @inject IStringLocalizer L 5 | 6 | 7 | @* Override the different child items to adapt them to our own style 8 | I've done this for LogOutSucceeded now, already added the rest but with the defaults (and localization). *@ 9 | 10 | 11 | 12 | @L["You were successfully logged out."] 13 | 14 | 15 | 16 | 17 | @L["Click here to log back in"]. 18 | 19 | 20 | 21 | 22 | @L["Logging you in..."] 23 | 24 | 25 | @L["Checking permissions..."] 26 | 27 | 28 | 29 | 30 | @L["Sorry, your login failed. Please try again or contact support."] 31 | 32 | 33 | @if (GetMessage() is string message && !string.IsNullOrWhiteSpace(message)) 34 | { 35 | 36 | @message 37 | 38 | } 39 | 40 | 41 | @L["Logging you out..."] 42 | 43 | 44 | @L["Logging you out..."] 45 | 46 | 47 | @L["Sorry, log out operation failed. Please try again or contact support."] 48 | 49 | 50 | @L["Retrieving profile..."] 51 | 52 | 53 | @L["Registering account..."] 54 | 55 | 56 | 57 | @code { 58 | [Parameter] public string? Action { get; set; } 59 | 60 | private void GoHome() => Navigation.NavigateTo("/"); 61 | 62 | private string? GetMessage() 63 | { 64 | var query = Navigation.ToAbsoluteUri(Navigation.Uri).Query; 65 | 66 | if (!string.IsNullOrWhiteSpace(query) 67 | && QueryHelpers.ParseQuery(query).TryGetValue("message", out var message)) 68 | { 69 | return message.ToString(); 70 | } 71 | 72 | return null; 73 | } 74 | } -------------------------------------------------------------------------------- /src/Client/Pages/Authentication/ForgotPassword.razor: -------------------------------------------------------------------------------- 1 | @page "/account/forgot-password" 2 | @attribute [AllowAnonymous] 3 | @inject IStringLocalizer L 4 | @inject IStringLocalizer LS 5 | 6 | 7 | 8 | 9 | 10 | 11 | 13 | 14 |
15 | 16 |
17 |
18 | @L["Forgot Password?"] 19 | 20 | @L["We can help you by resetting your password."] 21 |
22 |
23 |
24 |
25 | 26 | 27 | 28 | 29 | 30 | 32 | 33 | 34 | 37 | 38 | 39 | 40 | 41 | 42 | @L["Forgot Password"] 43 | 44 |
45 |
-------------------------------------------------------------------------------- /src/Client/Pages/Authentication/ForgotPassword.razor.cs: -------------------------------------------------------------------------------- 1 | using FSH.BlazorWebAssembly.Client.Components.Common; 2 | using FSH.BlazorWebAssembly.Client.Infrastructure.ApiClient; 3 | using FSH.BlazorWebAssembly.Client.Shared; 4 | using FSH.WebApi.Shared.Multitenancy; 5 | using Microsoft.AspNetCore.Components; 6 | 7 | namespace FSH.BlazorWebAssembly.Client.Pages.Authentication; 8 | 9 | public partial class ForgotPassword 10 | { 11 | private readonly ForgotPasswordRequest _forgotPasswordRequest = new(); 12 | private CustomValidation? _customValidation; 13 | private bool BusySubmitting { get; set; } 14 | 15 | [Inject] 16 | private IUsersClient UsersClient { get; set; } = default!; 17 | 18 | private string Tenant { get; set; } = MultitenancyConstants.Root.Id; 19 | 20 | private async Task SubmitAsync() 21 | { 22 | BusySubmitting = true; 23 | 24 | await ApiHelper.ExecuteCallGuardedAsync( 25 | () => UsersClient.ForgotPasswordAsync(Tenant, _forgotPasswordRequest), 26 | Snackbar, 27 | _customValidation); 28 | 29 | BusySubmitting = false; 30 | } 31 | } -------------------------------------------------------------------------------- /src/Client/Pages/Authentication/Login.razor: -------------------------------------------------------------------------------- 1 | @page "/login" 2 | @attribute [AllowAnonymous] 3 | @inject IStringLocalizer L 4 | @inject IStringLocalizer LS 5 | 6 |
7 | 8 |
9 |
10 | Sign In 11 | @L["Enter your credentials to get started."] 12 | 13 |
14 |
15 | 16 | 17 | 18 | 19 | 20 | 22 | 23 | 24 | 27 | 28 | 29 | 31 | 32 | 33 | 34 | @L["Register?"] 35 | 36 | 37 | @L["Forgot password?"] 38 | 39 | 40 | @L["Sign In"] 42 | 43 | 44 | @L["Fill Administrator Credentials"] 45 | 46 | 47 | -------------------------------------------------------------------------------- /src/Client/Pages/Authentication/Login.razor.cs: -------------------------------------------------------------------------------- 1 | using FSH.BlazorWebAssembly.Client.Components.Common; 2 | using FSH.BlazorWebAssembly.Client.Infrastructure.ApiClient; 3 | using FSH.BlazorWebAssembly.Client.Infrastructure.Auth; 4 | using FSH.BlazorWebAssembly.Client.Shared; 5 | using FSH.WebApi.Shared.Multitenancy; 6 | using Microsoft.AspNetCore.Components; 7 | using Microsoft.AspNetCore.Components.Authorization; 8 | using MudBlazor; 9 | 10 | namespace FSH.BlazorWebAssembly.Client.Pages.Authentication; 11 | 12 | public partial class Login 13 | { 14 | [CascadingParameter] 15 | public Task AuthState { get; set; } = default!; 16 | [Inject] 17 | public IAuthenticationService AuthService { get; set; } = default!; 18 | 19 | private CustomValidation? _customValidation; 20 | 21 | public bool BusySubmitting { get; set; } 22 | 23 | private readonly TokenRequest _tokenRequest = new(); 24 | private string TenantId { get; set; } = string.Empty; 25 | private bool _passwordVisibility; 26 | private InputType _passwordInput = InputType.Password; 27 | private string _passwordInputIcon = Icons.Material.Filled.VisibilityOff; 28 | 29 | protected override async Task OnInitializedAsync() 30 | { 31 | if (AuthService.ProviderType == AuthProvider.AzureAd) 32 | { 33 | AuthService.NavigateToExternalLogin(Navigation.Uri); 34 | return; 35 | } 36 | 37 | var authState = await AuthState; 38 | if (authState.User.Identity?.IsAuthenticated is true) 39 | { 40 | Navigation.NavigateTo("/"); 41 | } 42 | } 43 | 44 | private void TogglePasswordVisibility() 45 | { 46 | if (_passwordVisibility) 47 | { 48 | _passwordVisibility = false; 49 | _passwordInputIcon = Icons.Material.Filled.VisibilityOff; 50 | _passwordInput = InputType.Password; 51 | } 52 | else 53 | { 54 | _passwordVisibility = true; 55 | _passwordInputIcon = Icons.Material.Filled.Visibility; 56 | _passwordInput = InputType.Text; 57 | } 58 | } 59 | 60 | private void FillAdministratorCredentials() 61 | { 62 | _tokenRequest.Email = MultitenancyConstants.Root.EmailAddress; 63 | _tokenRequest.Password = MultitenancyConstants.DefaultPassword; 64 | TenantId = MultitenancyConstants.Root.Id; 65 | } 66 | 67 | private async Task SubmitAsync() 68 | { 69 | BusySubmitting = true; 70 | 71 | if (await ApiHelper.ExecuteCallGuardedAsync( 72 | () => AuthService.LoginAsync(TenantId, _tokenRequest), 73 | Snackbar, 74 | _customValidation)) 75 | { 76 | Snackbar.Add($"Logged in as {_tokenRequest.Email}", Severity.Info); 77 | } 78 | 79 | BusySubmitting = false; 80 | } 81 | } -------------------------------------------------------------------------------- /src/Client/Pages/Authentication/SelfRegister.razor: -------------------------------------------------------------------------------- 1 | @page "/users/self-register" 2 | @attribute [AllowAnonymous] 3 | @inject IStringLocalizer L 4 | @inject IStringLocalizer LS 5 | 6 | 7 | 8 | 9 | 10 | 11 | 13 | 14 |
15 | 16 |
17 |
18 | 19 | @L["New User Registration"] 20 | 21 | @L["Enter your details below to set up your new account"] 22 |
23 |
24 |
25 |
26 | 27 | 28 | 29 | 30 | 31 | 33 | 34 | 35 | 38 | 39 | 40 | 43 | 44 | 45 | 48 | 49 | 50 | 53 | 54 | 55 | 56 | 57 | 60 | 61 | 62 | 65 | 66 | 67 | 68 | 69 | 70 | @L["Register"] 71 | 72 | 73 |
74 |
-------------------------------------------------------------------------------- /src/Client/Pages/Authentication/SelfRegister.razor.cs: -------------------------------------------------------------------------------- 1 | using FSH.BlazorWebAssembly.Client.Components.Common; 2 | using FSH.BlazorWebAssembly.Client.Infrastructure.ApiClient; 3 | using FSH.BlazorWebAssembly.Client.Shared; 4 | using FSH.WebApi.Shared.Multitenancy; 5 | using Microsoft.AspNetCore.Components; 6 | using MudBlazor; 7 | 8 | namespace FSH.BlazorWebAssembly.Client.Pages.Authentication; 9 | 10 | public partial class SelfRegister 11 | { 12 | private readonly CreateUserRequest _createUserRequest = new(); 13 | private CustomValidation? _customValidation; 14 | private bool BusySubmitting { get; set; } 15 | 16 | [Inject] 17 | private IUsersClient UsersClient { get; set; } = default!; 18 | 19 | private string Tenant { get; set; } = MultitenancyConstants.Root.Id; 20 | 21 | private bool _passwordVisibility; 22 | private InputType _passwordInput = InputType.Password; 23 | private string _passwordInputIcon = Icons.Material.Filled.VisibilityOff; 24 | 25 | private async Task SubmitAsync() 26 | { 27 | BusySubmitting = true; 28 | 29 | string? sucessMessage = await ApiHelper.ExecuteCallGuardedAsync( 30 | () => UsersClient.SelfRegisterAsync(Tenant, _createUserRequest), 31 | Snackbar, 32 | _customValidation); 33 | 34 | if (sucessMessage != null) 35 | { 36 | Snackbar.Add(sucessMessage, Severity.Info); 37 | Navigation.NavigateTo("/login"); 38 | } 39 | 40 | BusySubmitting = false; 41 | } 42 | 43 | private void TogglePasswordVisibility() 44 | { 45 | if (_passwordVisibility) 46 | { 47 | _passwordVisibility = false; 48 | _passwordInputIcon = Icons.Material.Filled.VisibilityOff; 49 | _passwordInput = InputType.Password; 50 | } 51 | else 52 | { 53 | _passwordVisibility = true; 54 | _passwordInputIcon = Icons.Material.Filled.Visibility; 55 | _passwordInput = InputType.Text; 56 | } 57 | } 58 | } -------------------------------------------------------------------------------- /src/Client/Pages/Catalog/BrandAutocomplete.cs: -------------------------------------------------------------------------------- 1 | using FSH.BlazorWebAssembly.Client.Infrastructure.ApiClient; 2 | using FSH.BlazorWebAssembly.Client.Shared; 3 | using Microsoft.AspNetCore.Components; 4 | using Microsoft.Extensions.Localization; 5 | using MudBlazor; 6 | 7 | namespace FSH.BlazorWebAssembly.Client.Pages.Catalog; 8 | 9 | public class BrandAutocomplete : MudAutocomplete 10 | { 11 | [Inject] 12 | private IStringLocalizer L { get; set; } = default!; 13 | [Inject] 14 | private IBrandsClient BrandsClient { get; set; } = default!; 15 | [Inject] 16 | private ISnackbar Snackbar { get; set; } = default!; 17 | 18 | private List _brands = new(); 19 | 20 | // supply default parameters, but leave the possibility to override them 21 | public override Task SetParametersAsync(ParameterView parameters) 22 | { 23 | Label = L["Brand"]; 24 | Variant = Variant.Filled; 25 | Dense = true; 26 | Margin = Margin.Dense; 27 | ResetValueOnEmptyText = true; 28 | SearchFunc = SearchBrands; 29 | ToStringFunc = GetBrandName; 30 | Clearable = true; 31 | return base.SetParametersAsync(parameters); 32 | } 33 | 34 | // when the value parameter is set, we have to load that one brand to be able to show the name 35 | // we can't do that in OnInitialized because of a strange bug (https://github.com/MudBlazor/MudBlazor/issues/3818) 36 | protected override async Task OnAfterRenderAsync(bool firstRender) 37 | { 38 | if (firstRender && 39 | _value != default && 40 | await ApiHelper.ExecuteCallGuardedAsync( 41 | () => BrandsClient.GetAsync(_value), Snackbar) is { } brand) 42 | { 43 | _brands.Add(brand); 44 | ForceRender(true); 45 | } 46 | } 47 | 48 | private async Task> SearchBrands(string value) 49 | { 50 | var filter = new SearchBrandsRequest 51 | { 52 | PageSize = 10, 53 | AdvancedSearch = new() { Fields = new[] { "name" }, Keyword = value } 54 | }; 55 | 56 | if (await ApiHelper.ExecuteCallGuardedAsync( 57 | () => BrandsClient.SearchAsync(filter), Snackbar) 58 | is PaginationResponseOfBrandDto response) 59 | { 60 | _brands = response.Data.ToList(); 61 | } 62 | 63 | return _brands.Select(x => x.Id); 64 | } 65 | 66 | private string GetBrandName(Guid id) => 67 | _brands.Find(b => b.Id == id)?.Name ?? string.Empty; 68 | } -------------------------------------------------------------------------------- /src/Client/Pages/Catalog/Brands.razor: -------------------------------------------------------------------------------- 1 | @page "/catalog/brands" 2 | @attribute [MustHavePermission(FSHAction.View, FSHResource.Brands)] 3 | 4 | @inject IStringLocalizer L 5 | @inject IBrandsClient BrandsClient 6 | 7 | 8 | 9 | 10 | 11 | 12 | @if (!Context.AddEditModal.IsCreate) 13 | { 14 | 15 | 16 | 17 | } 18 | 19 | 20 | 21 | 22 | 24 | 25 | 26 | 27 | 28 | 29 | @code 30 | { 31 | protected EntityServerTableContext Context { get; set; } = default!; 32 | 33 | protected override void OnInitialized() => 34 | Context = new( 35 | entityName: L["Brand"], 36 | entityNamePlural: L["Brands"], 37 | entityResource: FSHResource.Brands, 38 | fields: new() 39 | { 40 | new(brand => brand.Id, L["Id"], "Id"), 41 | new(brand => brand.Name, L["Name"], "Name"), 42 | new(brand => brand.Description, L["Description"], "Description"), 43 | }, 44 | idFunc: brand => brand.Id, 45 | searchFunc: async filter => (await BrandsClient 46 | .SearchAsync(filter.Adapt())) 47 | .Adapt>(), 48 | createFunc: async brand => await BrandsClient.CreateAsync(brand.Adapt()), 49 | updateFunc: async (id, brand) => await BrandsClient.UpdateAsync(id, brand), 50 | deleteFunc: async id => await BrandsClient.DeleteAsync(id), 51 | exportAction: string.Empty); 52 | } -------------------------------------------------------------------------------- /src/Client/Pages/Catalog/Products.razor: -------------------------------------------------------------------------------- 1 | @page "/catalog/products" 2 | @attribute [MustHavePermission(FSHAction.View, FSHResource.Products)] 3 | 4 | @inject IStringLocalizer L 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | @L["Minimum Rate"]: @_searchMinimumRate.ToString() 13 | @L["Maximum Rate"]: @_searchMaximumRate.ToString() 14 | 15 | 16 | 17 | @if (!Context.AddEditModal.IsCreate) 18 | { 19 | 20 | 21 | 22 | } 23 | 24 | 25 | 26 | 27 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | @if(!string.IsNullOrEmpty(context.ImageInBytes)) 39 | { 40 | 41 | } 42 | else 43 | { 44 | 45 | } 46 | 47 | 48 |
49 | 51 | @L["Upload"] 52 | 53 | @if(!Context.AddEditModal.IsCreate && !string.IsNullOrEmpty(context.ImagePath) && string.IsNullOrEmpty(context.ImageInBytes)) 54 | { 55 | 57 | @L["View"] 58 | 59 | 60 | 62 | @L["Delete"] 63 | 64 | } 65 | @if(!string.IsNullOrEmpty(context.ImageInBytes)) 66 | { 67 | 69 | @L["Clear"] 70 | 71 | } 72 |
73 |
75 |
76 |
77 | 78 |
-------------------------------------------------------------------------------- /src/Client/Pages/Identity/Account/Account.razor: -------------------------------------------------------------------------------- 1 | @page "/account" 2 | 3 | @inject IStringLocalizer L 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | @if (!SecurityTabHidden) 12 | { 13 | 14 | 15 | 16 | } 17 | 18 | 19 | @code 20 | { 21 | [Inject] 22 | public IAuthenticationService AuthService { get; set; } = default!; 23 | 24 | public bool SecurityTabHidden { get; set; } = false; 25 | 26 | protected override void OnInitialized() 27 | { 28 | if (AuthService.ProviderType == AuthProvider.AzureAd) 29 | { 30 | SecurityTabHidden = true; 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /src/Client/Pages/Identity/Account/Security.razor: -------------------------------------------------------------------------------- 1 | @inject IStringLocalizer L 2 | 3 | 4 | 5 | 6 | 7 | @L["Change Password"] 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 19 | 20 | 21 | 26 | 27 | 28 | 33 | 34 | 35 | 36 | 37 | 38 | @L["Change Password"] 39 | 40 | 41 | -------------------------------------------------------------------------------- /src/Client/Pages/Identity/Account/Security.razor.cs: -------------------------------------------------------------------------------- 1 | using FSH.BlazorWebAssembly.Client.Components.Common; 2 | using FSH.BlazorWebAssembly.Client.Infrastructure.ApiClient; 3 | using FSH.BlazorWebAssembly.Client.Shared; 4 | using Microsoft.AspNetCore.Components; 5 | using MudBlazor; 6 | 7 | namespace FSH.BlazorWebAssembly.Client.Pages.Identity.Account; 8 | 9 | public partial class Security 10 | { 11 | [Inject] 12 | public IPersonalClient PersonalClient { get; set; } = default!; 13 | 14 | private readonly ChangePasswordRequest _passwordModel = new(); 15 | 16 | private CustomValidation? _customValidation; 17 | 18 | private async Task ChangePasswordAsync() 19 | { 20 | if (await ApiHelper.ExecuteCallGuardedAsync( 21 | () => PersonalClient.ChangePasswordAsync(_passwordModel), 22 | Snackbar, 23 | _customValidation, 24 | L["Password Changed!"])) 25 | { 26 | _passwordModel.Password = string.Empty; 27 | _passwordModel.NewPassword = string.Empty; 28 | _passwordModel.ConfirmNewPassword = string.Empty; 29 | } 30 | } 31 | 32 | private bool _currentPasswordVisibility; 33 | private InputType _currentPasswordInput = InputType.Password; 34 | private string _currentPasswordInputIcon = Icons.Material.Filled.VisibilityOff; 35 | private bool _newPasswordVisibility; 36 | private InputType _newPasswordInput = InputType.Password; 37 | private string _newPasswordInputIcon = Icons.Material.Filled.VisibilityOff; 38 | 39 | private void TogglePasswordVisibility(bool newPassword) 40 | { 41 | if (newPassword) 42 | { 43 | if (_newPasswordVisibility) 44 | { 45 | _newPasswordVisibility = false; 46 | _newPasswordInputIcon = Icons.Material.Filled.VisibilityOff; 47 | _newPasswordInput = InputType.Password; 48 | } 49 | else 50 | { 51 | _newPasswordVisibility = true; 52 | _newPasswordInputIcon = Icons.Material.Filled.Visibility; 53 | _newPasswordInput = InputType.Text; 54 | } 55 | } 56 | else 57 | { 58 | if (_currentPasswordVisibility) 59 | { 60 | _currentPasswordVisibility = false; 61 | _currentPasswordInputIcon = Icons.Material.Filled.VisibilityOff; 62 | _currentPasswordInput = InputType.Password; 63 | } 64 | else 65 | { 66 | _currentPasswordVisibility = true; 67 | _currentPasswordInputIcon = Icons.Material.Filled.Visibility; 68 | _currentPasswordInput = InputType.Text; 69 | } 70 | } 71 | } 72 | } -------------------------------------------------------------------------------- /src/Client/Pages/Identity/Roles/RolePermissions.razor: -------------------------------------------------------------------------------- 1 | @page "/roles/{Id}/permissions" 2 | @attribute [MustHavePermission(FSHAction.View, FSHResource.RoleClaims)] 3 | 4 | @inject IStringLocalizer L 5 | 6 | 7 | 8 | @if (!_loaded) 9 | { 10 | 11 | } 12 | else 13 | { 14 | 15 | @foreach (var group in _groupedRoleClaims.Keys) 16 | { 17 | var selectedRoleClaimsInGroup = _groupedRoleClaims[group].Where(c => c.Enabled).ToList(); 18 | var allRoleClaimsInGroup = _groupedRoleClaims[group].ToList(); 19 | 21 | 22 | 23 |
24 | @L["Back"] 25 | 26 | @if (_canEditRoleClaims) 27 | { 28 | @L["Update Permissions"] 30 | 31 | } 32 |
33 | 34 | @if (_canSearchRoleClaims) 35 | { 36 | 39 | 40 | } 41 |
42 | 43 | 44 | 45 | @L["Permission Name"] 46 | 47 | 48 | 49 | @L["Description"] 50 | 51 | 52 | @L["Status"] 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 |
72 |
73 | } 74 |
75 | } -------------------------------------------------------------------------------- /src/Client/Pages/Identity/Roles/Roles.razor: -------------------------------------------------------------------------------- 1 | @page "/roles" 2 | @attribute [MustHavePermission(FSHAction.View, FSHResource.Roles)] 3 | 4 | @inject IStringLocalizer L 5 | 6 | 7 | 8 | 9 | 10 | 11 | @if (_canViewRoleClaims) 12 | { 13 | @L["Manage Permission"] 14 | } 15 | 16 | 17 | 18 | @if (!Context.AddEditModal.IsCreate) 19 | { 20 | 21 | 22 | 23 | } 24 | 25 | 26 | 27 | 28 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /src/Client/Pages/Identity/Roles/Roles.razor.cs: -------------------------------------------------------------------------------- 1 | using FSH.BlazorWebAssembly.Client.Components.EntityTable; 2 | using FSH.BlazorWebAssembly.Client.Infrastructure.ApiClient; 3 | using FSH.BlazorWebAssembly.Client.Infrastructure.Auth; 4 | using FSH.WebApi.Shared.Authorization; 5 | using Microsoft.AspNetCore.Authorization; 6 | using Microsoft.AspNetCore.Components; 7 | using Microsoft.AspNetCore.Components.Authorization; 8 | 9 | namespace FSH.BlazorWebAssembly.Client.Pages.Identity.Roles; 10 | 11 | public partial class Roles 12 | { 13 | [CascadingParameter] 14 | protected Task AuthState { get; set; } = default!; 15 | [Inject] 16 | protected IAuthorizationService AuthService { get; set; } = default!; 17 | [Inject] 18 | private IRolesClient RolesClient { get; set; } = default!; 19 | 20 | protected EntityClientTableContext Context { get; set; } = default!; 21 | 22 | private bool _canViewRoleClaims; 23 | 24 | protected override async Task OnInitializedAsync() 25 | { 26 | var state = await AuthState; 27 | _canViewRoleClaims = await AuthService.HasPermissionAsync(state.User, FSHAction.View, FSHResource.RoleClaims); 28 | 29 | Context = new( 30 | entityName: L["Role"], 31 | entityNamePlural: L["Roles"], 32 | entityResource: FSHResource.Roles, 33 | searchAction: FSHAction.View, 34 | fields: new() 35 | { 36 | new(role => role.Id, L["Id"]), 37 | new(role => role.Name, L["Name"]), 38 | new(role => role.Description, L["Description"]) 39 | }, 40 | idFunc: role => role.Id, 41 | loadDataFunc: async () => (await RolesClient.GetListAsync()).ToList(), 42 | searchFunc: (searchString, role) => 43 | string.IsNullOrWhiteSpace(searchString) 44 | || role.Name?.Contains(searchString, StringComparison.OrdinalIgnoreCase) == true 45 | || role.Description?.Contains(searchString, StringComparison.OrdinalIgnoreCase) == true, 46 | createFunc: async role => await RolesClient.RegisterRoleAsync(role), 47 | updateFunc: async (_, role) => await RolesClient.RegisterRoleAsync(role), 48 | deleteFunc: async id => await RolesClient.DeleteAsync(id), 49 | hasExtraActionsFunc: () => _canViewRoleClaims, 50 | canUpdateEntityFunc: e => !FSHRoles.IsDefault(e.Name), 51 | canDeleteEntityFunc: e => !FSHRoles.IsDefault(e.Name), 52 | exportAction: string.Empty); 53 | } 54 | 55 | private void ManagePermissions(string? roleId) 56 | { 57 | ArgumentNullException.ThrowIfNull(roleId, nameof(roleId)); 58 | Navigation.NavigateTo($"/roles/{roleId}/permissions"); 59 | } 60 | } -------------------------------------------------------------------------------- /src/Client/Pages/Identity/Users/UserProfile.razor.cs: -------------------------------------------------------------------------------- 1 | using FSH.BlazorWebAssembly.Client.Infrastructure.ApiClient; 2 | using FSH.BlazorWebAssembly.Client.Infrastructure.Auth; 3 | using FSH.BlazorWebAssembly.Client.Infrastructure.Common; 4 | using FSH.BlazorWebAssembly.Client.Shared; 5 | using FSH.WebApi.Shared.Authorization; 6 | using Microsoft.AspNetCore.Authorization; 7 | using Microsoft.AspNetCore.Components; 8 | using Microsoft.AspNetCore.Components.Authorization; 9 | 10 | namespace FSH.BlazorWebAssembly.Client.Pages.Identity.Users; 11 | 12 | public partial class UserProfile 13 | { 14 | [CascadingParameter] 15 | protected Task AuthState { get; set; } = default!; 16 | [Inject] 17 | protected IAuthorizationService AuthService { get; set; } = default!; 18 | [Inject] 19 | protected IUsersClient UsersClient { get; set; } = default!; 20 | 21 | [Parameter] 22 | public string? Id { get; set; } 23 | [Parameter] 24 | public string? Title { get; set; } 25 | [Parameter] 26 | public string? Description { get; set; } 27 | 28 | private bool _active; 29 | private bool _emailConfirmed; 30 | private char _firstLetterOfName; 31 | private string? _firstName; 32 | private string? _lastName; 33 | private string? _phoneNumber; 34 | private string? _email; 35 | private string? _imageUrl; 36 | private bool _loaded; 37 | private bool _canToggleUserStatus; 38 | 39 | private async Task ToggleUserStatus() 40 | { 41 | var request = new ToggleUserStatusRequest { ActivateUser = _active, UserId = Id }; 42 | await ApiHelper.ExecuteCallGuardedAsync(() => UsersClient.ToggleStatusAsync(Id, request), Snackbar); 43 | Navigation.NavigateTo("/users"); 44 | } 45 | 46 | [Parameter] 47 | public string? ImageUrl { get; set; } 48 | 49 | protected override async Task OnInitializedAsync() 50 | { 51 | if (await ApiHelper.ExecuteCallGuardedAsync( 52 | () => UsersClient.GetByIdAsync(Id), Snackbar) 53 | is UserDetailsDto user) 54 | { 55 | _firstName = user.FirstName; 56 | _lastName = user.LastName; 57 | _email = user.Email; 58 | _phoneNumber = user.PhoneNumber; 59 | _active = user.IsActive; 60 | _emailConfirmed = user.EmailConfirmed; 61 | _imageUrl = string.IsNullOrEmpty(user.ImageUrl) ? string.Empty : (Config[ConfigNames.ApiBaseUrl] + user.ImageUrl); 62 | Title = $"{_firstName} {_lastName}'s {_localizer["Profile"]}"; 63 | Description = _email; 64 | if (_firstName?.Length > 0) 65 | { 66 | _firstLetterOfName = _firstName.ToUpper().FirstOrDefault(); 67 | } 68 | } 69 | 70 | var state = await AuthState; 71 | _canToggleUserStatus = await AuthService.HasPermissionAsync(state.User, FSHAction.Update, FSHResource.Users); 72 | _loaded = true; 73 | } 74 | } -------------------------------------------------------------------------------- /src/Client/Pages/Identity/Users/UserRoles.razor: -------------------------------------------------------------------------------- 1 | @page "/users/{Id}/roles" 2 | @attribute [MustHavePermission(FSHAction.View, FSHResource.UserRoles)] 3 | 4 | @inject IStringLocalizer L 5 | 6 | 7 | 8 | @if (!_loaded) 9 | { 10 | 11 | } 12 | else 13 | { 14 | 15 | 16 |
17 | 18 | @L["Back"] 19 | 20 | @if (_canEditUsers) 21 | { 22 | 24 | @L["Update"] 25 | 26 | } 27 |
28 | 29 | @if (_canSearchRoles) 30 | { 31 | 34 | 35 | } 36 |
37 | 38 | 39 | @L["Role Name"] 40 | 41 | 42 | 43 | @L["Description"] 44 | 45 | 46 | 47 | @L["Status"] 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 |
67 | } -------------------------------------------------------------------------------- /src/Client/Pages/Identity/Users/UserRoles.razor.cs: -------------------------------------------------------------------------------- 1 | using FSH.BlazorWebAssembly.Client.Infrastructure.ApiClient; 2 | using FSH.BlazorWebAssembly.Client.Infrastructure.Auth; 3 | using FSH.BlazorWebAssembly.Client.Shared; 4 | using FSH.WebApi.Shared.Authorization; 5 | using Microsoft.AspNetCore.Authorization; 6 | using Microsoft.AspNetCore.Components; 7 | using Microsoft.AspNetCore.Components.Authorization; 8 | 9 | namespace FSH.BlazorWebAssembly.Client.Pages.Identity.Users; 10 | 11 | public partial class UserRoles 12 | { 13 | [Parameter] 14 | public string? Id { get; set; } 15 | [CascadingParameter] 16 | protected Task AuthState { get; set; } = default!; 17 | [Inject] 18 | protected IAuthorizationService AuthService { get; set; } = default!; 19 | [Inject] 20 | protected IUsersClient UsersClient { get; set; } = default!; 21 | 22 | private List _userRolesList = default!; 23 | 24 | private string _title = string.Empty; 25 | private string _description = string.Empty; 26 | 27 | private string _searchString = string.Empty; 28 | 29 | private bool _canEditUsers; 30 | private bool _canSearchRoles; 31 | private bool _loaded; 32 | 33 | protected override async Task OnInitializedAsync() 34 | { 35 | var state = await AuthState; 36 | _canEditUsers = await AuthService.HasPermissionAsync(state.User, FSHAction.Update, FSHResource.Users); 37 | _canSearchRoles = await AuthService.HasPermissionAsync(state.User, FSHAction.View, FSHResource.UserRoles); 38 | 39 | if (await ApiHelper.ExecuteCallGuardedAsync( 40 | () => UsersClient.GetByIdAsync(Id), Snackbar) 41 | is UserDetailsDto user) 42 | { 43 | _title = $"{user.FirstName} {user.LastName}"; 44 | _description = string.Format(L["Manage {0} {1}'s Roles"], user.FirstName, user.LastName); 45 | 46 | if (await ApiHelper.ExecuteCallGuardedAsync( 47 | () => UsersClient.GetRolesAsync(user.Id.ToString()), Snackbar) 48 | is ICollection response) 49 | { 50 | _userRolesList = response.ToList(); 51 | } 52 | } 53 | 54 | _loaded = true; 55 | } 56 | 57 | private async Task SaveAsync() 58 | { 59 | var request = new UserRolesRequest() 60 | { 61 | UserRoles = _userRolesList 62 | }; 63 | 64 | if (await ApiHelper.ExecuteCallGuardedAsync( 65 | () => UsersClient.AssignRolesAsync(Id, request), 66 | Snackbar, 67 | successMessage: L["Updated User Roles."]) 68 | is not null) 69 | { 70 | Navigation.NavigateTo("/users"); 71 | } 72 | } 73 | 74 | private bool Search(UserRoleDto userRole) => 75 | string.IsNullOrWhiteSpace(_searchString) 76 | || userRole.RoleName?.Contains(_searchString, StringComparison.OrdinalIgnoreCase) is true; 77 | } -------------------------------------------------------------------------------- /src/Client/Pages/Identity/Users/Users.razor: -------------------------------------------------------------------------------- 1 | @page "/users" 2 | @attribute [MustHavePermission(FSHAction.View, FSHResource.Users)] 3 | 4 | @inject IStringLocalizer L 5 | 6 | 7 | 8 | 9 | 10 | @L["View Profile"] 11 | @if (_canViewRoles) 12 | { 13 | @L["Manage Roles"] 14 | } 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 30 | 31 | 32 | 34 | 35 | 36 | 39 | 40 | 41 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /src/Client/Pages/Identity/Users/Users.razor.cs: -------------------------------------------------------------------------------- 1 | using FSH.BlazorWebAssembly.Client.Components.EntityTable; 2 | using FSH.BlazorWebAssembly.Client.Infrastructure.ApiClient; 3 | using FSH.BlazorWebAssembly.Client.Infrastructure.Auth; 4 | using FSH.WebApi.Shared.Authorization; 5 | using Microsoft.AspNetCore.Authorization; 6 | using Microsoft.AspNetCore.Components; 7 | using Microsoft.AspNetCore.Components.Authorization; 8 | using MudBlazor; 9 | 10 | namespace FSH.BlazorWebAssembly.Client.Pages.Identity.Users; 11 | 12 | public partial class Users 13 | { 14 | [CascadingParameter] 15 | protected Task AuthState { get; set; } = default!; 16 | [Inject] 17 | protected IAuthorizationService AuthService { get; set; } = default!; 18 | 19 | [Inject] 20 | protected IUsersClient UsersClient { get; set; } = default!; 21 | 22 | protected EntityClientTableContext Context { get; set; } = default!; 23 | 24 | private bool _canExportUsers; 25 | private bool _canViewRoles; 26 | 27 | // Fields for editform 28 | protected string Password { get; set; } = string.Empty; 29 | protected string ConfirmPassword { get; set; } = string.Empty; 30 | 31 | private bool _passwordVisibility; 32 | private InputType _passwordInput = InputType.Password; 33 | private string _passwordInputIcon = Icons.Material.Filled.VisibilityOff; 34 | 35 | protected override async Task OnInitializedAsync() 36 | { 37 | var user = (await AuthState).User; 38 | _canExportUsers = await AuthService.HasPermissionAsync(user, FSHAction.Export, FSHResource.Users); 39 | _canViewRoles = await AuthService.HasPermissionAsync(user, FSHAction.View, FSHResource.UserRoles); 40 | 41 | Context = new( 42 | entityName: L["User"], 43 | entityNamePlural: L["Users"], 44 | entityResource: FSHResource.Users, 45 | searchAction: FSHAction.View, 46 | updateAction: string.Empty, 47 | deleteAction: string.Empty, 48 | fields: new() 49 | { 50 | new(user => user.FirstName, L["First Name"]), 51 | new(user => user.LastName, L["Last Name"]), 52 | new(user => user.UserName, L["UserName"]), 53 | new(user => user.Email, L["Email"]), 54 | new(user => user.PhoneNumber, L["PhoneNumber"]), 55 | new(user => user.EmailConfirmed, L["Email Confirmation"], Type: typeof(bool)), 56 | new(user => user.IsActive, L["Active"], Type: typeof(bool)) 57 | }, 58 | idFunc: user => user.Id, 59 | loadDataFunc: async () => (await UsersClient.GetListAsync()).ToList(), 60 | searchFunc: (searchString, user) => 61 | string.IsNullOrWhiteSpace(searchString) 62 | || user.FirstName?.Contains(searchString, StringComparison.OrdinalIgnoreCase) == true 63 | || user.LastName?.Contains(searchString, StringComparison.OrdinalIgnoreCase) == true 64 | || user.Email?.Contains(searchString, StringComparison.OrdinalIgnoreCase) == true 65 | || user.PhoneNumber?.Contains(searchString, StringComparison.OrdinalIgnoreCase) == true 66 | || user.UserName?.Contains(searchString, StringComparison.OrdinalIgnoreCase) == true, 67 | createFunc: user => UsersClient.CreateAsync(user), 68 | hasExtraActionsFunc: () => true, 69 | exportAction: string.Empty); 70 | } 71 | 72 | private void ViewProfile(in Guid userId) => 73 | Navigation.NavigateTo($"/users/{userId}/profile"); 74 | 75 | private void ManageRoles(in Guid userId) => 76 | Navigation.NavigateTo($"/users/{userId}/roles"); 77 | 78 | private void TogglePasswordVisibility() 79 | { 80 | if (_passwordVisibility) 81 | { 82 | _passwordVisibility = false; 83 | _passwordInputIcon = Icons.Material.Filled.VisibilityOff; 84 | _passwordInput = InputType.Password; 85 | } 86 | else 87 | { 88 | _passwordVisibility = true; 89 | _passwordInputIcon = Icons.Material.Filled.Visibility; 90 | _passwordInput = InputType.Text; 91 | } 92 | 93 | Context.AddEditModal.ForceRender(); 94 | } 95 | } -------------------------------------------------------------------------------- /src/Client/Pages/Multitenancy/UpgradeSubscriptionModal.razor: -------------------------------------------------------------------------------- 1 | @inject IStringLocalizer L 2 | @inject ITenantsClient TenantsClient 3 | 4 | 5 | 6 | 7 | 8 | 9 | @L["Upgrade Subscription"] 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | @L["Cancel"] 27 | @L["Upgrade"] 28 | 29 | 30 | 31 | 32 | @code 33 | { 34 | [Parameter] public UpgradeSubscriptionRequest Request { get; set; } = new(); 35 | [CascadingParameter] private MudDialogInstance MudDialog { get; set; } = default!; 36 | DateTime? date = DateTime.Today; 37 | 38 | protected override void OnInitialized() => 39 | date = Request.ExtendedExpiryDate; 40 | 41 | private async Task UpgradeSubscriptionAsync() 42 | { 43 | Request.ExtendedExpiryDate = date.HasValue ? date.Value : Request.ExtendedExpiryDate; 44 | if (await ApiHelper.ExecuteCallGuardedAsync( 45 | () => TenantsClient.UpgradeSubscriptionAsync(Request.TenantId, Request), 46 | Snackbar, 47 | null, 48 | L["Upgraded Subscription."]) is not null) 49 | { 50 | MudDialog.Close(); 51 | } 52 | } 53 | 54 | public void Cancel() 55 | { 56 | MudDialog.Cancel(); 57 | } 58 | } -------------------------------------------------------------------------------- /src/Client/Pages/Personal/AuditLogs.razor.cs: -------------------------------------------------------------------------------- 1 | using FSH.BlazorWebAssembly.Client.Components.EntityTable; 2 | using FSH.BlazorWebAssembly.Client.Infrastructure.ApiClient; 3 | using Mapster; 4 | using Microsoft.AspNetCore.Components; 5 | using MudBlazor; 6 | 7 | namespace FSH.BlazorWebAssembly.Client.Pages.Personal; 8 | 9 | public partial class AuditLogs 10 | { 11 | [Inject] 12 | private IPersonalClient PersonalClient { get; set; } = default!; 13 | 14 | protected EntityClientTableContext Context { get; set; } = default!; 15 | 16 | private string? _searchString; 17 | private MudDateRangePicker _dateRangePicker = default!; 18 | private DateRange? _dateRange; 19 | private bool _searchInOldValues; 20 | private bool _searchInNewValues; 21 | private List _trails = new(); 22 | 23 | // Configure Automapper 24 | static AuditLogs() => 25 | TypeAdapterConfig.NewConfig().Map( 26 | dest => dest.LocalTime, 27 | src => DateTime.SpecifyKind(src.DateTime, DateTimeKind.Utc).ToLocalTime()); 28 | 29 | protected override void OnInitialized() 30 | { 31 | Context = new( 32 | entityNamePlural: L["Trails"], 33 | searchAction: true.ToString(), 34 | fields: new() 35 | { 36 | new(audit => audit.Id, L["Id"]), 37 | new(audit => audit.TableName, L["Table Name"]), 38 | new(audit => audit.DateTime, L["Date"], Template: DateFieldTemplate), 39 | new(audit => audit.Type, L["Type"]) 40 | }, 41 | loadDataFunc: async () => _trails = (await PersonalClient.GetLogsAsync()).Adapt>(), 42 | searchFunc: (searchString, trail) => 43 | (string.IsNullOrWhiteSpace(searchString) // check Search String 44 | || trail.TableName?.Contains(searchString, StringComparison.OrdinalIgnoreCase) == true 45 | || (_searchInOldValues && 46 | trail.OldValues?.Contains(searchString, StringComparison.OrdinalIgnoreCase) == true) 47 | || (_searchInNewValues && 48 | trail.NewValues?.Contains(searchString, StringComparison.OrdinalIgnoreCase) == true)) 49 | && ((_dateRange?.Start is null && _dateRange?.End is null) // check Date Range 50 | || (_dateRange?.Start is not null && _dateRange.End is null && trail.DateTime >= _dateRange.Start) 51 | || (_dateRange?.Start is null && _dateRange?.End is not null && trail.DateTime <= _dateRange.End + new TimeSpan(0, 11, 59, 59, 999)) 52 | || (trail.DateTime >= _dateRange!.Start && trail.DateTime <= _dateRange.End + new TimeSpan(0, 11, 59, 59, 999))), 53 | hasExtraActionsFunc: () => true); 54 | } 55 | 56 | private void ShowBtnPress(Guid id) 57 | { 58 | var trail = _trails.First(f => f.Id == id); 59 | trail.ShowDetails = !trail.ShowDetails; 60 | foreach (var otherTrail in _trails.Except(new[] { trail })) 61 | { 62 | otherTrail.ShowDetails = false; 63 | } 64 | } 65 | 66 | public class RelatedAuditTrail : AuditDto 67 | { 68 | public bool ShowDetails { get; set; } 69 | public DateTime LocalTime { get; set; } 70 | } 71 | } -------------------------------------------------------------------------------- /src/Client/Pages/Personal/Dashboard.razor: -------------------------------------------------------------------------------- 1 | @page "/dashboard" 2 | @attribute [MustHavePermission(FSHAction.View, FSHResource.Dashboard)] 3 | 4 | @inject IStringLocalizer L 5 | 6 | 7 | 8 | @if (!_loaded) 9 | { 10 | 11 | } 12 | else 13 | { 14 | 15 | 16 | 17 | 19 |
20 | @L["Products"] 21 | @ProductCount 22 |
23 |
24 |
25 | 26 | 27 | 29 |
30 | @L["Brands"] 31 | @BrandCount 32 |
33 |
34 |
35 | 36 | 37 | 39 |
40 | @L["Registered Users"] 41 | 42 | @UserCount 43 |
44 |
45 |
46 | 47 | 48 | 50 |
51 | @L["Registered Roles"] 52 | 53 | @RoleCount 54 |
55 |
56 |
57 | 58 | 59 | 62 | 63 | 64 |
65 | } -------------------------------------------------------------------------------- /src/Client/Pages/Personal/Dashboard.razor.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | using FSH.BlazorWebAssembly.Client.Infrastructure.ApiClient; 3 | using FSH.BlazorWebAssembly.Client.Infrastructure.Notifications; 4 | using FSH.BlazorWebAssembly.Client.Shared; 5 | using FSH.WebApi.Shared.Notifications; 6 | using MediatR.Courier; 7 | using Microsoft.AspNetCore.Components; 8 | 9 | namespace FSH.BlazorWebAssembly.Client.Pages.Personal; 10 | 11 | public partial class Dashboard 12 | { 13 | [Parameter] 14 | public int ProductCount { get; set; } 15 | [Parameter] 16 | public int BrandCount { get; set; } 17 | [Parameter] 18 | public int UserCount { get; set; } 19 | [Parameter] 20 | public int RoleCount { get; set; } 21 | 22 | [Inject] 23 | private IDashboardClient DashboardClient { get; set; } = default!; 24 | [Inject] 25 | private ICourier Courier { get; set; } = default!; 26 | 27 | private readonly string[] _dataEnterBarChartXAxisLabels = DateTimeFormatInfo.CurrentInfo.AbbreviatedMonthNames; 28 | private readonly List _dataEnterBarChartSeries = new(); 29 | private bool _loaded; 30 | 31 | protected override async Task OnInitializedAsync() 32 | { 33 | Courier.SubscribeWeak>(async _ => 34 | { 35 | await LoadDataAsync(); 36 | StateHasChanged(); 37 | }); 38 | 39 | await LoadDataAsync(); 40 | 41 | _loaded = true; 42 | } 43 | 44 | private async Task LoadDataAsync() 45 | { 46 | if (await ApiHelper.ExecuteCallGuardedAsync( 47 | () => DashboardClient.GetAsync(), 48 | Snackbar) 49 | is StatsDto statsDto) 50 | { 51 | ProductCount = statsDto.ProductCount; 52 | BrandCount = statsDto.BrandCount; 53 | UserCount = statsDto.UserCount; 54 | RoleCount = statsDto.RoleCount; 55 | foreach (var item in statsDto.DataEnterBarChart) 56 | { 57 | _dataEnterBarChartSeries 58 | .RemoveAll(x => x.Name.Equals(item.Name, StringComparison.OrdinalIgnoreCase)); 59 | _dataEnterBarChartSeries.Add(new MudBlazor.ChartSeries { Name = item.Name, Data = item.Data?.ToArray() }); 60 | } 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Client/Program.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | using FSH.BlazorWebAssembly.Client; 3 | using FSH.BlazorWebAssembly.Client.Infrastructure; 4 | using FSH.BlazorWebAssembly.Client.Infrastructure.Common; 5 | using FSH.BlazorWebAssembly.Client.Infrastructure.Preferences; 6 | using Microsoft.AspNetCore.Components.Web; 7 | using Microsoft.AspNetCore.Components.WebAssembly.Hosting; 8 | 9 | var builder = WebAssemblyHostBuilder.CreateDefault(args); 10 | 11 | builder.RootComponents.Add("#app"); 12 | builder.RootComponents.Add("head::after"); 13 | 14 | builder.Services.AddClientServices(builder.Configuration); 15 | 16 | var host = builder.Build(); 17 | 18 | var storageService = host.Services.GetRequiredService(); 19 | if (storageService != null) 20 | { 21 | CultureInfo culture; 22 | if (await storageService.GetPreference() is ClientPreference preference) 23 | culture = new CultureInfo(preference.LanguageCode); 24 | else 25 | culture = new CultureInfo(LocalizationConstants.SupportedLanguages.FirstOrDefault()?.Code ?? "en-US"); 26 | CultureInfo.DefaultThreadCurrentCulture = culture; 27 | CultureInfo.DefaultThreadCurrentUICulture = culture; 28 | } 29 | 30 | await host.RunAsync(); -------------------------------------------------------------------------------- /src/Client/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:21716", 7 | "sslPort": 44331 8 | } 9 | }, 10 | "profiles": { 11 | "FSH.BlazorWebAssembly": { 12 | "commandName": "Project", 13 | "dotnetRunMessages": true, 14 | "launchBrowser": true, 15 | "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", 16 | "applicationUrl": "https://localhost:5002;http://localhost:5003", 17 | "environmentVariables": { 18 | "ASPNETCORE_ENVIRONMENT": "Development" 19 | } 20 | }, 21 | "IIS Express": { 22 | "commandName": "IISExpress", 23 | "launchBrowser": true, 24 | "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", 25 | "environmentVariables": { 26 | "ASPNETCORE_ENVIRONMENT": "Development" 27 | } 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /src/Client/Shared/ApiHelper.cs: -------------------------------------------------------------------------------- 1 | using FSH.BlazorWebAssembly.Client.Components.Common; 2 | using FSH.BlazorWebAssembly.Client.Infrastructure.ApiClient; 3 | using MudBlazor; 4 | 5 | namespace FSH.BlazorWebAssembly.Client.Shared; 6 | 7 | public static class ApiHelper 8 | { 9 | public static async Task ExecuteCallGuardedAsync( 10 | Func> call, 11 | ISnackbar snackbar, 12 | CustomValidation? customValidation = null, 13 | string? successMessage = null) 14 | { 15 | customValidation?.ClearErrors(); 16 | try 17 | { 18 | var result = await call(); 19 | 20 | if (!string.IsNullOrWhiteSpace(successMessage)) 21 | { 22 | snackbar.Add(successMessage, Severity.Info); 23 | } 24 | 25 | return result; 26 | } 27 | catch (ApiException ex) 28 | { 29 | if (ex.Result.Errors is not null) 30 | { 31 | customValidation?.DisplayErrors(ex.Result.Errors); 32 | } 33 | else 34 | { 35 | snackbar.Add("Something went wrong!", Severity.Error); 36 | } 37 | } 38 | catch (ApiException ex) 39 | { 40 | snackbar.Add(ex.Result.Exception, Severity.Error); 41 | } 42 | catch (Exception ex) 43 | { 44 | snackbar.Add(ex.Message, Severity.Error); 45 | } 46 | 47 | return default; 48 | } 49 | 50 | public static async Task ExecuteCallGuardedAsync( 51 | Func call, 52 | ISnackbar snackbar, 53 | CustomValidation? customValidation = null, 54 | string? successMessage = null) 55 | { 56 | customValidation?.ClearErrors(); 57 | try 58 | { 59 | await call(); 60 | 61 | if (!string.IsNullOrWhiteSpace(successMessage)) 62 | { 63 | snackbar.Add(successMessage, Severity.Success); 64 | } 65 | 66 | return true; 67 | } 68 | catch (ApiException ex) 69 | { 70 | if (ex.Result.Errors is not null) 71 | { 72 | customValidation?.DisplayErrors(ex.Result.Errors); 73 | } 74 | else 75 | { 76 | snackbar.Add("Something went wrong!", Severity.Error); 77 | } 78 | } 79 | catch (ApiException ex) 80 | { 81 | snackbar.Add(ex.Result.Exception, Severity.Error); 82 | } 83 | 84 | return false; 85 | } 86 | } -------------------------------------------------------------------------------- /src/Client/Shared/BaseLayout.razor: -------------------------------------------------------------------------------- 1 | @inherits LayoutComponentBase 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 12 | 13 | 14 | 15 | 16 | @Body 17 | 18 | 19 | 20 | 21 | 22 | 23 | 25 | 26 | 27 | @Body 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /src/Client/Shared/BaseLayout.razor.cs: -------------------------------------------------------------------------------- 1 | using FSH.BlazorWebAssembly.Client.Infrastructure.Preferences; 2 | using FSH.BlazorWebAssembly.Client.Infrastructure.Theme; 3 | using MudBlazor; 4 | 5 | namespace FSH.BlazorWebAssembly.Client.Shared; 6 | 7 | public partial class BaseLayout 8 | { 9 | private ClientPreference? _themePreference; 10 | private MudTheme _currentTheme = new LightTheme(); 11 | private bool _themeDrawerOpen; 12 | private bool _rightToLeft; 13 | 14 | protected override async Task OnInitializedAsync() 15 | { 16 | _themePreference = await ClientPreferences.GetPreference() as ClientPreference; 17 | if (_themePreference == null) _themePreference = new ClientPreference(); 18 | SetCurrentTheme(_themePreference); 19 | 20 | Snackbar.Add("Like this boilerplate? ", Severity.Normal, config => 21 | { 22 | config.BackgroundBlurred = true; 23 | config.Icon = Icons.Custom.Brands.GitHub; 24 | config.Action = "Star us on Github!"; 25 | config.ActionColor = Color.Primary; 26 | config.Onclick = snackbar => 27 | { 28 | Navigation.NavigateTo("https://github.com/fullstackhero/blazor-wasm-boilerplate"); 29 | return Task.CompletedTask; 30 | }; 31 | }); 32 | } 33 | 34 | private async Task ThemePreferenceChanged(ClientPreference themePreference) 35 | { 36 | SetCurrentTheme(themePreference); 37 | await ClientPreferences.SetPreference(themePreference); 38 | } 39 | 40 | private void SetCurrentTheme(ClientPreference themePreference) 41 | { 42 | _currentTheme = themePreference.IsDarkMode ? new DarkTheme() : new LightTheme(); 43 | _currentTheme.Palette.Primary = themePreference.PrimaryColor; 44 | _currentTheme.Palette.Secondary = themePreference.SecondaryColor; 45 | _currentTheme.LayoutProperties.DefaultBorderRadius = $"{themePreference.BorderRadius}px"; 46 | _currentTheme.LayoutProperties.DefaultBorderRadius = $"{themePreference.BorderRadius}px"; 47 | _rightToLeft = themePreference.IsRTL; 48 | } 49 | } -------------------------------------------------------------------------------- /src/Client/Shared/DialogServiceExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Components; 2 | using MudBlazor; 3 | 4 | namespace FSH.BlazorWebAssembly.Client.Shared; 5 | 6 | public static class DialogServiceExtensions 7 | { 8 | public static Task ShowModalAsync(this IDialogService dialogService, DialogParameters parameters) 9 | where TDialog : ComponentBase => 10 | dialogService.ShowModal(parameters).Result; 11 | 12 | public static IDialogReference ShowModal(this IDialogService dialogService, DialogParameters parameters) 13 | where TDialog : ComponentBase 14 | { 15 | var options = new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.Medium, FullWidth = true, DisableBackdropClick = true }; 16 | 17 | return dialogService.Show(string.Empty, parameters, options); 18 | } 19 | } -------------------------------------------------------------------------------- /src/Client/Shared/MainLayout.razor.cs: -------------------------------------------------------------------------------- 1 | using FSH.BlazorWebAssembly.Client.Infrastructure.Preferences; 2 | using Microsoft.AspNetCore.Components; 3 | using MudBlazor; 4 | 5 | namespace FSH.BlazorWebAssembly.Client.Shared; 6 | 7 | public partial class MainLayout 8 | { 9 | [Parameter] 10 | public RenderFragment ChildContent { get; set; } = default!; 11 | [Parameter] 12 | public EventCallback OnDarkModeToggle { get; set; } 13 | [Parameter] 14 | public EventCallback OnRightToLeftToggle { get; set; } 15 | 16 | private bool _drawerOpen; 17 | private bool _rightToLeft; 18 | 19 | protected override async Task OnInitializedAsync() 20 | { 21 | if (await ClientPreferences.GetPreference() is ClientPreference preference) 22 | { 23 | _rightToLeft = preference.IsRTL; 24 | _drawerOpen = preference.IsDrawerOpen; 25 | } 26 | } 27 | 28 | private async Task RightToLeftToggle() 29 | { 30 | bool isRtl = await ClientPreferences.ToggleLayoutDirectionAsync(); 31 | _rightToLeft = isRtl; 32 | 33 | await OnRightToLeftToggle.InvokeAsync(isRtl); 34 | } 35 | 36 | public async Task ToggleDarkMode() 37 | { 38 | await OnDarkModeToggle.InvokeAsync(); 39 | } 40 | 41 | private async Task DrawerToggle() 42 | { 43 | _drawerOpen = await ClientPreferences.ToggleDrawerAsync(); 44 | } 45 | 46 | private void Logout() 47 | { 48 | var parameters = new DialogParameters 49 | { 50 | { nameof(Dialogs.Logout.ContentText), $"{L["Logout Confirmation"]}"}, 51 | { nameof(Dialogs.Logout.ButtonText), $"{L["Logout"]}"}, 52 | { nameof(Dialogs.Logout.Color), Color.Error} 53 | }; 54 | 55 | var options = new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.Small, FullWidth = true }; 56 | DialogService.Show(L["Logout"], parameters, options); 57 | } 58 | 59 | private void Profile() 60 | { 61 | Navigation.NavigateTo("/account"); 62 | } 63 | } -------------------------------------------------------------------------------- /src/Client/Shared/NavMenu.razor: -------------------------------------------------------------------------------- 1 | @inject IStringLocalizer L 2 | 3 | 5 | 6 | @L["Start"] 7 | @L["Home"] 8 | @L["Getting Started"] 10 | @if (_canViewHangfire) 11 | { 12 | @L["Hangfire"] 13 | } 14 | @L["Personal"] 15 | @if (_canViewDashboard) 16 | { 17 | @L["Dashboard"] 18 | } 19 | @L["Account"] 20 | @L["Logs"] 21 | @if (_canViewProducts || _canViewBrands) 22 | { 23 | @L["Management"] 24 | 25 | @if (_canViewProducts) 26 | { 27 | 28 | @L["Products"] 29 | 30 | } 31 | @if (_canViewBrands) 32 | { 33 | 34 | @L["Brands"] 35 | 36 | } 37 | 38 | } 39 | @if (CanViewAdministrationGroup) 40 | { 41 | @L["Administration"] 42 | @if (_canViewUsers) 43 | { 44 | @L["Users"] 45 | } 46 | @if (_canViewRoles) 47 | { 48 | @L["Roles"] 49 | } 50 | @if(_canViewTenants) 51 | { 52 | @L["Tenants"] 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Client/Shared/NavMenu.razor.cs: -------------------------------------------------------------------------------- 1 | using FSH.BlazorWebAssembly.Client.Infrastructure.Auth; 2 | using FSH.BlazorWebAssembly.Client.Infrastructure.Common; 3 | using FSH.WebApi.Shared.Authorization; 4 | using Microsoft.AspNetCore.Authorization; 5 | using Microsoft.AspNetCore.Components; 6 | using Microsoft.AspNetCore.Components.Authorization; 7 | 8 | namespace FSH.BlazorWebAssembly.Client.Shared; 9 | 10 | public partial class NavMenu 11 | { 12 | [CascadingParameter] 13 | protected Task AuthState { get; set; } = default!; 14 | [Inject] 15 | protected IAuthorizationService AuthService { get; set; } = default!; 16 | 17 | private string? _hangfireUrl; 18 | private bool _canViewHangfire; 19 | private bool _canViewDashboard; 20 | private bool _canViewRoles; 21 | private bool _canViewUsers; 22 | private bool _canViewProducts; 23 | private bool _canViewBrands; 24 | private bool _canViewTenants; 25 | private bool CanViewAdministrationGroup => _canViewUsers || _canViewRoles || _canViewTenants; 26 | 27 | protected override async Task OnParametersSetAsync() 28 | { 29 | _hangfireUrl = Config[ConfigNames.ApiBaseUrl] + "jobs"; 30 | var user = (await AuthState).User; 31 | _canViewHangfire = await AuthService.HasPermissionAsync(user, FSHAction.View, FSHResource.Hangfire); 32 | _canViewDashboard = await AuthService.HasPermissionAsync(user, FSHAction.View, FSHResource.Dashboard); 33 | _canViewRoles = await AuthService.HasPermissionAsync(user, FSHAction.View, FSHResource.Roles); 34 | _canViewUsers = await AuthService.HasPermissionAsync(user, FSHAction.View, FSHResource.Users); 35 | _canViewProducts = await AuthService.HasPermissionAsync(user, FSHAction.View, FSHResource.Products); 36 | _canViewBrands = await AuthService.HasPermissionAsync(user, FSHAction.View, FSHResource.Brands); 37 | _canViewTenants = await AuthService.HasPermissionAsync(user, FSHAction.View, FSHResource.Tenants); 38 | } 39 | } -------------------------------------------------------------------------------- /src/Client/Shared/NotFound.razor: -------------------------------------------------------------------------------- 1 | @using Infrastructure.Theme 2 | @inherits LayoutComponentBase 3 | 4 | 5 | 6 | 7 |
8 | 10 | 12 | 14 | 16 | 18 | 20 | 22 | 24 | 26 | 28 | 30 | 32 | 34 | 36 | 38 | 39 | Not Found 40 | 41 |
42 | Go Home 43 |
44 |
45 |
46 |
47 | 48 | @code { 49 | private MudTheme _currentTheme = new LightTheme(); 50 | 51 | protected override async Task OnInitializedAsync() 52 | { 53 | _currentTheme = new LightTheme(); 54 | _currentTheme = await ClientPreferences.GetCurrentThemeAsync(); 55 | } 56 | } -------------------------------------------------------------------------------- /src/Client/Shared/SharedResource.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.BlazorWebAssembly.Client.Shared; 2 | 3 | public class SharedResource 4 | { 5 | } 6 | -------------------------------------------------------------------------------- /src/Client/_Imports.razor: -------------------------------------------------------------------------------- 1 | @using System.Security.Claims 2 | @using Microsoft.AspNetCore.Authorization 3 | @using Microsoft.AspNetCore.Components 4 | @using Microsoft.AspNetCore.Components.Authorization 5 | @using Microsoft.AspNetCore.Components.Forms 6 | @using Microsoft.AspNetCore.Components.Routing 7 | @using Microsoft.AspNetCore.Components.Web 8 | @using Microsoft.AspNetCore.Components.Web.Virtualization 9 | @using Microsoft.AspNetCore.Components.WebAssembly.Authentication 10 | @using Microsoft.AspNetCore.Components.WebAssembly.Http 11 | @using Microsoft.AspNetCore.WebUtilities 12 | @using Microsoft.Extensions.Configuration 13 | @using Microsoft.Extensions.DependencyInjection 14 | @using Microsoft.Extensions.Localization 15 | @using Microsoft.JSInterop 16 | @using FSH.BlazorWebAssembly.Client 17 | @using FSH.BlazorWebAssembly.Client.Shared 18 | @using FSH.BlazorWebAssembly.Client.Pages.Authentication 19 | @using FSH.BlazorWebAssembly.Client.Pages.Multitenancy 20 | @using FSH.BlazorWebAssembly.Client.Components.Common 21 | @using FSH.BlazorWebAssembly.Client.Components.EntityTable 22 | @using FSH.BlazorWebAssembly.Client.Components.Localization 23 | @using FSH.BlazorWebAssembly.Client.Components.Notifications 24 | @using FSH.BlazorWebAssembly.Client.Components.ThemeManager 25 | @using FSH.BlazorWebAssembly.Client.Infrastructure.ApiClient 26 | @using FSH.BlazorWebAssembly.Client.Infrastructure.Auth 27 | @using FSH.BlazorWebAssembly.Client.Infrastructure.Common 28 | @using FSH.BlazorWebAssembly.Client.Infrastructure.Notifications 29 | @using FSH.BlazorWebAssembly.Client.Infrastructure.Preferences 30 | @using FSH.WebApi.Shared.Authorization 31 | @using Blazored.LocalStorage 32 | @using Mapster 33 | @using MediatR.Courier 34 | @using MudBlazor 35 | 36 | @attribute [Authorize] 37 | 38 | @inject NavigationManager Navigation 39 | @inject ISnackbar Snackbar 40 | @inject IDialogService DialogService 41 | @inject IConfiguration Config 42 | @inject IClientPreferenceManager ClientPreferences -------------------------------------------------------------------------------- /src/Client/wwwroot/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /src/Client/wwwroot/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "ApiBaseUrl": "https://localhost:5001/", 3 | "AuthProvider": "Jwt", 4 | "AzureAd": { 5 | "Authority": "https://login.microsoftonline.com/organizations", 6 | "ClientId": "", 7 | "ValidateAuthority": true, 8 | "ApiScope": "api:///access_as_user" 9 | } 10 | } -------------------------------------------------------------------------------- /src/Client/wwwroot/css/fsh.css: -------------------------------------------------------------------------------- 1 | .fsh-center-text { 2 | text-align: center !important; 3 | } 4 | 5 | .mud-button-filled { 6 | box-shadow: 0 3px 1px -2px rgb(0 0 0 / 30%), 0 2px 2px 0 rgb(0 0 0 / 0), 0 1px 5px 0 rgb(0 0 0 / 10%) !important; 7 | } 8 | 9 | .mud-dialog { 10 | box-shadow: 0 3px 1px -2px rgb(0 0 0 / 10%), 0 2px 2px 0 rgb(0 0 0 / 0), 0 10px 10px 0 rgb(0 0 0 / 5%) !important; 11 | } 12 | 13 | .mud-nav-link { 14 | white-space: normal !important; 15 | padding: 12px 16px 12px 38px; 16 | } 17 | 18 | .mud-nav-link.active:not(.mud-nav-link-disabled) { 19 | border-right: 3px solid var(--mud-palette-primary); 20 | background-color: rgba(var(--mud-palette-primary-rgb), 0.1); 21 | } 22 | 23 | .mud-table { 24 | padding: 20px !important; 25 | margin-bottom: 20px !important; 26 | } 27 | 28 | .mud-card { 29 | margin-bottom: 20px !important; 30 | } 31 | 32 | #blazor-error-ui { 33 | color: var(--mud-palette-drawer-text); 34 | background: var(--mud-palette-drawer-background); 35 | } 36 | 37 | .mud-overlay-dark { 38 | backdrop-filter: blur(2px); 39 | } 40 | 41 | .mud-card-header .mud-card-header-avatar { 42 | margin-inline-end: 10px !important; 43 | } 44 | 45 | .mud-nav-link { 46 | padding: 12px 16px 12px 15px !important; 47 | } 48 | 49 | .mud-dialog-content { 50 | max-height: 75vh !important; 51 | overflow: auto !important; 52 | overflow-x: hidden !important; 53 | } 54 | 55 | .mud-grid-spacing-xs-3 { 56 | margin: 0px !important; 57 | margin-left: -12px !important; 58 | } 59 | 60 | .mud-table-toolbar 61 | { 62 | flex-wrap:wrap; 63 | margin-bottom:20px!important; 64 | } 65 | 66 | ::-webkit-scrollbar { 67 | width: 2px!important; 68 | height: 6px!important; 69 | } 70 | .fsh-nav-child { 71 | padding-left: 10px !important; 72 | } -------------------------------------------------------------------------------- /src/Client/wwwroot/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fullstackhero/blazor-wasm-boilerplate/a3e3eeb70897e9bc9e6ce5d0d2b7a50800081620/src/Client/wwwroot/favicon.ico -------------------------------------------------------------------------------- /src/Client/wwwroot/full-stack-hero-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fullstackhero/blazor-wasm-boilerplate/a3e3eeb70897e9bc9e6ce5d0d2b7a50800081620/src/Client/wwwroot/full-stack-hero-logo.png -------------------------------------------------------------------------------- /src/Client/wwwroot/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Blazor WebAssembly Boilerplate", 3 | "short_name": "Blazor WebAssembly Boilerplate", 4 | "start_url": "./", 5 | "display": "standalone", 6 | "background_color": "#ffffff", 7 | "theme_color": "#3eaf7c", 8 | "prefer_related_applications": false, 9 | "icons": [ 10 | { 11 | "src": "full-stack-hero-logo.png", 12 | "type": "image/png", 13 | "sizes": "512x512" 14 | }, 15 | { 16 | "src": "full-stack-hero-logo.png", 17 | "type": "image/png", 18 | "sizes": "192x192" 19 | } 20 | ] 21 | } -------------------------------------------------------------------------------- /src/Client/wwwroot/service-worker.js: -------------------------------------------------------------------------------- 1 | // In development, always fetch from the network and do not enable offline support. 2 | // This is because caching would make development more difficult (changes would not 3 | // be reflected on the first load after each change). 4 | self.addEventListener('fetch', () => { }); -------------------------------------------------------------------------------- /src/Client/wwwroot/service-worker.published.js: -------------------------------------------------------------------------------- 1 | // Caution! Be sure you understand the caveats before publishing an application with 2 | // offline support. See https://aka.ms/blazor-offline-considerations 3 | 4 | self.importScripts('./service-worker-assets.js'); 5 | self.addEventListener('install', event => event.waitUntil(onInstall(event))); 6 | self.addEventListener('activate', event => event.waitUntil(onActivate(event))); 7 | self.addEventListener('fetch', event => event.respondWith(onFetch(event))); 8 | 9 | const cacheNamePrefix = 'offline-cache-'; 10 | const cacheName = `${cacheNamePrefix}${self.assetsManifest.version}`; 11 | const offlineAssetsInclude = [/\.dll$/, /\.pdb$/, /\.wasm/, /\.html/, /\.js$/, /\.json$/, /\.css$/, /\.woff$/, /\.png$/, /\.jpe?g$/, /\.gif$/, /\.ico$/, /\.blat$/, /\.dat$/]; 12 | const offlineAssetsExclude = [/^service-worker\.js$/]; 13 | 14 | async function onInstall(event) { 15 | console.info('Service worker: Install'); 16 | 17 | // Fetch and cache all matching items from the assets manifest 18 | const assetsRequests = self.assetsManifest.assets 19 | .filter(asset => offlineAssetsInclude.some(pattern => pattern.test(asset.url))) 20 | .filter(asset => !offlineAssetsExclude.some(pattern => pattern.test(asset.url))) 21 | .map(asset => new Request(asset.url, { integrity: asset.hash, cache: 'no-cache' })); 22 | await caches.open(cacheName).then(cache => cache.addAll(assetsRequests)); 23 | } 24 | 25 | async function onActivate(event) { 26 | console.info('Service worker: Activate'); 27 | 28 | // Delete unused caches 29 | const cacheKeys = await caches.keys(); 30 | await Promise.all(cacheKeys 31 | .filter(key => key.startsWith(cacheNamePrefix) && key !== cacheName) 32 | .map(key => caches.delete(key))); 33 | } 34 | 35 | async function onFetch(event) { 36 | let cachedResponse = null; 37 | if (event.request.method === 'GET') { 38 | // For all navigation requests, try to serve index.html from cache 39 | // If you need some URLs to be server-rendered, edit the following check to exclude those URLs 40 | const shouldServeIndexHtml = event.request.mode === 'navigate'; 41 | 42 | const request = shouldServeIndexHtml ? 'index.html' : event.request; 43 | const cache = await caches.open(cacheName); 44 | cachedResponse = await cache.match(request); 45 | } 46 | 47 | return cachedResponse || fetch(event.request); 48 | } -------------------------------------------------------------------------------- /src/Host/Host.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net6.0 5 | FSH.BlazorWebAssembly.Host 6 | FSH.BlazorWebAssembly.Host 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | Always 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/Host/Pages/Error.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model FSH.BlazorWebAssembly.Server.Pages.ErrorModel 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Error 11 | 12 | 13 | 14 | 15 | 16 |
17 |
18 |

Error.

19 |

An error occurred while processing your request.

20 | 21 | @if (Model.ShowRequestId) 22 | { 23 |

24 | Request ID: @Model.RequestId 25 |

26 | } 27 | 28 |

Development Mode

29 |

30 | Swapping to the Development environment displays detailed information about the error 31 | that occurred. 32 |

33 |

34 | The Development environment shouldn't be enabled for deployed applications. 35 | It can result in displaying sensitive information from exceptions to end users. 36 | For local debugging, enable the Development environment by setting the 37 | ASPNETCORE_ENVIRONMENT environment variable to Development 38 | and restarting the app. 39 |

40 |
41 |
42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /src/Host/Pages/Error.cshtml.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using Microsoft.AspNetCore.Mvc; 3 | using Microsoft.AspNetCore.Mvc.RazorPages; 4 | 5 | namespace FSH.BlazorWebAssembly.Server.Pages; 6 | 7 | [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] 8 | [IgnoreAntiforgeryToken] 9 | public class ErrorModel : PageModel 10 | { 11 | public string? RequestId { get; set; } 12 | 13 | public bool ShowRequestId => !string.IsNullOrEmpty(RequestId); 14 | 15 | public void OnGet() 16 | { 17 | RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier; 18 | } 19 | } -------------------------------------------------------------------------------- /src/Host/Program.cs: -------------------------------------------------------------------------------- 1 | var builder = WebApplication.CreateBuilder(args); 2 | builder.Services.AddControllersWithViews(); 3 | builder.Services.AddRazorPages(); 4 | 5 | var app = builder.Build(); 6 | 7 | if (app.Environment.IsDevelopment()) 8 | { 9 | app.UseWebAssemblyDebugging(); 10 | } 11 | else 12 | { 13 | app.UseExceptionHandler("/Error"); 14 | app.UseHsts(); 15 | } 16 | 17 | app.UseHttpsRedirection(); 18 | app.UseBlazorFrameworkFiles(); 19 | app.UseStaticFiles(); 20 | app.UseRouting(); 21 | 22 | app.MapRazorPages(); 23 | app.MapControllers(); 24 | app.MapFallbackToFile("index.html"); 25 | 26 | app.Run(); -------------------------------------------------------------------------------- /src/Host/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:21716", 7 | "sslPort": 44331 8 | } 9 | }, 10 | "profiles": { 11 | "FSH.BlazorWebAssembly.Server": { 12 | "commandName": "Project", 13 | "dotnetRunMessages": true, 14 | "launchBrowser": true, 15 | "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", 16 | "applicationUrl": "https://localhost:5002;http://localhost:5003", 17 | "environmentVariables": { 18 | "ASPNETCORE_ENVIRONMENT": "Development" 19 | } 20 | }, 21 | "IIS Express": { 22 | "commandName": "IISExpress", 23 | "launchBrowser": true, 24 | "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", 25 | "environmentVariables": { 26 | "ASPNETCORE_ENVIRONMENT": "Development" 27 | } 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /src/Host/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | } 8 | } -------------------------------------------------------------------------------- /src/Host/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | }, 8 | "AllowedHosts": "*" 9 | } -------------------------------------------------------------------------------- /src/Shared/Authorization/ClaimsPrincipalExtensions.cs: -------------------------------------------------------------------------------- 1 | using FSH.WebApi.Shared.Authorization; 2 | 3 | namespace System.Security.Claims; 4 | 5 | public static class ClaimsPrincipalExtensions 6 | { 7 | public static string? GetEmail(this ClaimsPrincipal principal) 8 | => principal.FindFirstValue(ClaimTypes.Email); 9 | 10 | public static string? GetTenant(this ClaimsPrincipal principal) 11 | => principal.FindFirstValue(FSHClaims.Tenant); 12 | 13 | public static string? GetFullName(this ClaimsPrincipal principal) 14 | => principal?.FindFirst(FSHClaims.Fullname)?.Value; 15 | 16 | public static string? GetFirstName(this ClaimsPrincipal principal) 17 | => principal?.FindFirst(ClaimTypes.Name)?.Value; 18 | 19 | public static string? GetSurname(this ClaimsPrincipal principal) 20 | => principal?.FindFirst(ClaimTypes.Surname)?.Value; 21 | 22 | public static string? GetPhoneNumber(this ClaimsPrincipal principal) 23 | => principal.FindFirstValue(ClaimTypes.MobilePhone); 24 | 25 | public static string? GetUserId(this ClaimsPrincipal principal) 26 | => principal.FindFirstValue(ClaimTypes.NameIdentifier); 27 | 28 | public static string? GetImageUrl(this ClaimsPrincipal principal) 29 | => principal.FindFirstValue(FSHClaims.ImageUrl); 30 | 31 | public static DateTimeOffset GetExpiration(this ClaimsPrincipal principal) => 32 | DateTimeOffset.FromUnixTimeSeconds(Convert.ToInt64( 33 | principal.FindFirstValue(FSHClaims.Expiration))); 34 | 35 | private static string? FindFirstValue(this ClaimsPrincipal principal, string claimType) => 36 | principal is null 37 | ? throw new ArgumentNullException(nameof(principal)) 38 | : principal.FindFirst(claimType)?.Value; 39 | } -------------------------------------------------------------------------------- /src/Shared/Authorization/FSHClaims.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.WebApi.Shared.Authorization; 2 | 3 | public static class FSHClaims 4 | { 5 | public const string Tenant = "tenant"; 6 | public const string Fullname = "fullName"; 7 | public const string Permission = "permission"; 8 | public const string ImageUrl = "image_url"; 9 | public const string IpAddress = "ipAddress"; 10 | public const string Expiration = "exp"; 11 | } -------------------------------------------------------------------------------- /src/Shared/Authorization/FSHRoles.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.ObjectModel; 2 | 3 | namespace FSH.WebApi.Shared.Authorization; 4 | 5 | public static class FSHRoles 6 | { 7 | public static string Admin = nameof(Admin); 8 | public static string Basic = nameof(Basic); 9 | 10 | public static IReadOnlyList DefaultRoles { get; } = new ReadOnlyCollection(new[] 11 | { 12 | Admin, 13 | Basic 14 | }); 15 | 16 | public static bool IsDefault(string roleName) => DefaultRoles.Any(r => r == roleName); 17 | } -------------------------------------------------------------------------------- /src/Shared/Events/IEvent.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.WebApi.Shared.Events; 2 | 3 | public interface IEvent 4 | { 5 | } -------------------------------------------------------------------------------- /src/Shared/MultiTenancy/MultitenancyConstants.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.WebApi.Shared.Multitenancy; 2 | 3 | public class MultitenancyConstants 4 | { 5 | public static class Root 6 | { 7 | public const string Id = "root"; 8 | public const string Name = "Root"; 9 | public const string EmailAddress = "admin@root.com"; 10 | } 11 | 12 | public const string DefaultPassword = "123Pa$$word!"; 13 | 14 | public const string TenantIdName = "tenant"; 15 | } -------------------------------------------------------------------------------- /src/Shared/Notifications/BasicNotification.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.WebApi.Shared.Notifications; 2 | 3 | public class BasicNotification : INotificationMessage 4 | { 5 | public enum LabelType 6 | { 7 | Information, 8 | Success, 9 | Warning, 10 | Error 11 | } 12 | 13 | public string? Message { get; set; } 14 | public LabelType Label { get; set; } 15 | } -------------------------------------------------------------------------------- /src/Shared/Notifications/INotificationMessage.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.WebApi.Shared.Notifications; 2 | 3 | public interface INotificationMessage 4 | { 5 | } -------------------------------------------------------------------------------- /src/Shared/Notifications/JobNotification.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.WebApi.Shared.Notifications; 2 | 3 | public class JobNotification : INotificationMessage 4 | { 5 | public string? Message { get; set; } 6 | public string? JobId { get; set; } 7 | public decimal Progress { get; set; } 8 | } -------------------------------------------------------------------------------- /src/Shared/Notifications/NotificationConstants.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.WebApi.Shared.Notifications; 2 | 3 | public static class NotificationConstants 4 | { 5 | public const string NotificationFromServer = nameof(NotificationFromServer); 6 | } -------------------------------------------------------------------------------- /src/Shared/Notifications/StatsChangedNotification.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.WebApi.Shared.Notifications; 2 | 3 | public class StatsChangedNotification : INotificationMessage 4 | { 5 | } -------------------------------------------------------------------------------- /src/Shared/Shared.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | net6.0 4 | FSH.WebApi.Shared 5 | FSH.WebApi.Shared 6 | 7 | -------------------------------------------------------------------------------- /stylecop.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json", 3 | "settings": { 4 | "orderingRules": { 5 | "systemUsingDirectivesFirst": true, 6 | "usingDirectivesPlacement": "outsideNamespace" 7 | }, 8 | "layoutRules": { 9 | "newlineAtEndOfFile": "omit" 10 | } 11 | } 12 | } --------------------------------------------------------------------------------