├── gpts.png ├── src ├── CleanAspire.ClientApp │ ├── Pages │ │ ├── _Imports.razor │ │ ├── Account │ │ │ ├── Profile │ │ │ │ ├── _Imports.razor │ │ │ │ └── Setting.razor │ │ │ ├── _Imports.razor │ │ │ ├── ForgetPasswordSuccessful.razor │ │ │ ├── SignupSuccessful.razor │ │ │ ├── ForgetPassword.razor │ │ │ └── SignupConfirmation.razor │ │ ├── Authentication.razor │ │ ├── PrivacyPolicy.razor │ │ └── TermsOfService.razor │ ├── wwwroot │ │ ├── webpushr-sw.js │ │ ├── favicon.png │ │ ├── icon-192.png │ │ ├── icon-512.png │ │ ├── appsettings.Development.json │ │ ├── service-worker.js │ │ ├── appsettings.json │ │ ├── js │ │ │ ├── downloadfile.js │ │ │ ├── onlinestatus.js │ │ │ ├── webpushr.js │ │ │ ├── carousel.js │ │ │ └── fancybox.js │ │ ├── manifest.webmanifest │ │ ├── loading.svg │ │ ├── syncing.svg │ │ ├── green-point.svg │ │ ├── red-point.svg │ │ └── icon.svg │ ├── Layout │ │ ├── Redirections │ │ │ ├── RedirectToHome.razor │ │ │ └── RedirectToSignIn.razor │ │ ├── RedirectToLogin.razor │ │ ├── AuthenticationLayout.razor │ │ ├── ApplicationLayout.razor │ │ ├── Appbar.razor │ │ ├── MainLayout.razor │ │ └── MudBlazorLogo.razor │ ├── Configurations │ │ └── ClientAppSettings.cs │ ├── Services │ │ ├── JsInterop │ │ │ ├── DownloadFileInterop.cs │ │ │ ├── Webpushr.cs │ │ │ ├── Swiper.cs │ │ │ ├── Fancybox.cs │ │ │ ├── DisplayModeInterop.cs │ │ │ └── OnlineStatusInterop.cs │ │ ├── PushNotifications │ │ │ ├── IWebpushrService.cs │ │ │ ├── WebpushrOptionsCache.cs │ │ │ └── WebpushrAuthHandler.cs │ │ ├── UserPreferences │ │ │ ├── UserPreferences.cs │ │ │ └── UserPreferencesService.cs │ │ ├── Identity │ │ │ ├── UserProfileStoreEvent.cs │ │ │ ├── ISignInManagement.cs │ │ │ ├── UserProfileStore.cs │ │ │ └── CookieHandler.cs │ │ ├── LanguageService.cs │ │ ├── OfflineSyncService.cs │ │ ├── Navigation │ │ │ └── MenuItem.cs │ │ ├── Interfaces │ │ │ └── IStorageService.cs │ │ ├── OfflineModeState.cs │ │ ├── SupportedLocalization.cs │ │ └── LocalStorageService.cs │ ├── Components │ │ ├── WebpushrSetup.razor │ │ ├── ConfirmationDialog.razor │ │ ├── FileSizeFormatter.razor │ │ ├── Autocompletes │ │ │ ├── PicklistDataSource.cs │ │ │ ├── PicklistAutocomplete.razor.cs │ │ │ ├── TimeZoneAutocomplete.cs │ │ │ ├── LanguageAutocomplete.cs │ │ │ ├── MultiTenantAutocomplete.cs │ │ │ └── ProductAutocomplete.cs │ │ ├── Thumbnail.razor │ │ ├── PasswordInput.razor │ │ └── LanguageSelector.razor │ ├── Routes.razor │ ├── App.razor │ ├── Program.cs │ ├── nginx.conf │ ├── Properties │ │ └── launchSettings.json │ ├── Client │ │ ├── Models │ │ │ └── ProductCategory.cs │ │ ├── Webpushr │ │ │ └── WebpushrRequestBuilder.cs │ │ └── Manage │ │ │ └── ManageRequestBuilder.cs │ ├── _Imports.razor │ ├── Dockerfile │ └── CleanAspire.ClientApp.csproj ├── Migrators │ ├── Migrators.PostgreSQL │ │ ├── Class1.cs │ │ └── Migrators.PostgreSQL.csproj │ ├── Migrators.SQLite │ │ └── Migrators.SQLite.csproj │ └── Migrators.MSSQL │ │ └── Migrators.MSSQL.csproj ├── CleanAspire.AppHost │ ├── appsettings.Development.json │ ├── Program.cs │ ├── appsettings.json │ ├── CleanAspire.AppHost.csproj │ └── Properties │ │ └── launchSettings.json ├── CleanAspire.Domain │ ├── Common │ │ ├── IAuditTrial.cs │ │ ├── IMustHaveTenant.cs │ │ ├── IEntity.cs │ │ ├── ISoftDelete.cs │ │ ├── BaseAuditableSoftDeleteEntity.cs │ │ ├── BaseAuditableEntity.cs │ │ └── BaseEntity.cs │ ├── Events │ │ └── DomainEvent.cs │ ├── CleanAspire.Domain.csproj │ ├── Entities │ │ ├── Tenant.cs │ │ ├── Stock.cs │ │ ├── AuditTrail.cs │ │ └── Product.cs │ └── Identities │ │ └── ApplicationUser.cs ├── CleanAspire.WebApp │ ├── appsettings.Development.json │ ├── appsettings.json │ ├── Components │ │ ├── _Imports.razor │ │ └── Pages │ │ │ └── Error.razor │ ├── CleanAspire.WebApp.csproj │ ├── Properties │ │ └── launchSettings.json │ └── Program.cs ├── CleanAspire.Application │ ├── Common │ │ ├── Interfaces │ │ │ ├── IDateTime.cs │ │ │ ├── ICurrentUserContext.cs │ │ │ ├── ICurrentUserAccessor.cs │ │ │ ├── FusionCache │ │ │ │ ├── IFusionCacheRequest.cs │ │ │ │ └── IFusionCacheRefreshRequest.cs │ │ │ ├── ICurrentUserContextSetter.cs │ │ │ ├── IApplicationDbContext.cs │ │ │ └── IUploadService.cs │ │ └── Models │ │ │ └── PaginatedResult.cs │ ├── Features │ │ ├── Tenants │ │ │ ├── DTOs │ │ │ │ └── TenantDto.cs │ │ │ ├── Queries │ │ │ │ ├── GetTenantByIdQuery.cs │ │ │ │ └── GetAllTenantsQuery.cs │ │ │ └── Commands │ │ │ │ ├── DeleteTenantCommand.cs │ │ │ │ ├── CreateTenantCommand.cs │ │ │ │ └── UpdateTenantCommand.cs │ │ ├── Products │ │ │ ├── Validators │ │ │ │ └── DeleteProductCommandValidator.cs │ │ │ ├── DTOs │ │ │ │ └── ProductDto.cs │ │ │ ├── EventHandlers │ │ │ │ ├── ProductCreatedEvent.cs │ │ │ │ ├── ProductDeletedEvent.cs │ │ │ │ └── ProductUpdatedEvent.cs │ │ │ └── Commands │ │ │ │ ├── DeleteProductCommand.cs │ │ │ │ ├── CreateProductCommand.cs │ │ │ │ ├── ImportProductsCommand.cs │ │ │ │ └── UpdateProductCommand.cs │ │ └── Stocks │ │ │ ├── Validators │ │ │ ├── StockReceivingCommandValidator.cs │ │ │ └── StockDispatchingCommandValidator.cs │ │ │ └── DTOs │ │ │ └── StockDto.cs │ ├── _Imports.cs │ ├── DependencyInjection.cs │ ├── CleanAspire.Application.csproj │ └── Pipeline │ │ ├── FusionCacheBehaviour.cs │ │ └── MessageValidatorBehaviour.cs ├── CleanAspire.Api │ ├── Webpushr │ │ └── WebpushrOptions.cs │ ├── Properties │ │ └── launchSettings.json │ ├── Endpoints │ │ ├── EndpointExtensions.cs │ │ ├── IEndpointRegistrar.cs │ │ ├── WebpushrEndpointRegistrar.cs │ │ └── TenantEndpointRegistrar.cs │ ├── OpenApiTransformersExtensions.cs │ ├── NumberSchemaTransformer.cs │ ├── Identity │ │ └── EmailSender.cs │ ├── CleanAspire.Api.csproj │ ├── appsettings.json │ └── Dockerfile ├── CleanAspire.Infrastructure │ ├── Services │ │ ├── UtcDateTime.cs │ │ ├── CurrentUserContext.cs │ │ ├── CurrentUserAccessor.cs │ │ └── CurrentUserContextSetter.cs │ ├── Persistence │ │ ├── BlazorContextFactory.cs │ │ ├── Configurations │ │ │ ├── TenantConfiguration.cs │ │ │ ├── IdentityUserConfiguration.cs │ │ │ ├── AuditTrailConfiguration.cs │ │ │ ├── StockConfiguration.cs │ │ │ └── ProductConfiguration.cs │ │ ├── Conversions │ │ │ ├── StringListConverter.cs │ │ │ └── ValueConversionExtensions.cs │ │ └── Extensions │ │ │ └── ModelBuilderExtensions.cs │ ├── Configurations │ │ ├── MinioOptions.cs │ │ └── DatabaseSettings.cs │ └── CleanAspire.Infrastructure.csproj └── CleanAspire.ServiceDefaults │ └── CleanAspire.ServiceDefaults.csproj ├── blazorclient.jpg ├── .kiota └── workspace.json ├── .dockerignore ├── tests └── CleanAspire.Tests │ ├── WebTests.cs │ └── CleanAspire.Tests.csproj ├── .github └── workflows │ ├── dotnet.yml │ └── docker.yml ├── LICENSE.txt └── CleanAspire.slnx /gpts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neozhu/cleanaspire/HEAD/gpts.png -------------------------------------------------------------------------------- /src/CleanAspire.ClientApp/Pages/_Imports.razor: -------------------------------------------------------------------------------- 1 | 2 | @layout ApplicationLayout 3 | -------------------------------------------------------------------------------- /blazorclient.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neozhu/cleanaspire/HEAD/blazorclient.jpg -------------------------------------------------------------------------------- /src/CleanAspire.ClientApp/Pages/Account/Profile/_Imports.razor: -------------------------------------------------------------------------------- 1 | 2 | @layout ApplicationLayout -------------------------------------------------------------------------------- /.kiota/workspace.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.0", 3 | "clients": {}, 4 | "plugins": {} 5 | } -------------------------------------------------------------------------------- /src/CleanAspire.ClientApp/wwwroot/webpushr-sw.js: -------------------------------------------------------------------------------- 1 | importScripts('https://cdn.webpushr.com/sw-server.min.js'); -------------------------------------------------------------------------------- /src/CleanAspire.ClientApp/wwwroot/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neozhu/cleanaspire/HEAD/src/CleanAspire.ClientApp/wwwroot/favicon.png -------------------------------------------------------------------------------- /src/CleanAspire.ClientApp/wwwroot/icon-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neozhu/cleanaspire/HEAD/src/CleanAspire.ClientApp/wwwroot/icon-192.png -------------------------------------------------------------------------------- /src/CleanAspire.ClientApp/wwwroot/icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neozhu/cleanaspire/HEAD/src/CleanAspire.ClientApp/wwwroot/icon-512.png -------------------------------------------------------------------------------- /src/Migrators/Migrators.PostgreSQL/Class1.cs: -------------------------------------------------------------------------------- 1 | namespace Migrators.PostgreSQL 2 | { 3 | public class Class1 4 | { 5 | 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/CleanAspire.ClientApp/Pages/Account/_Imports.razor: -------------------------------------------------------------------------------- 1 | @using System.ComponentModel.DataAnnotations 2 | @using Api.Client.Models 3 | @layout AuthenticationLayout -------------------------------------------------------------------------------- /src/CleanAspire.AppHost/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/CleanAspire.ClientApp/Layout/Redirections/RedirectToHome.razor: -------------------------------------------------------------------------------- 1 | 2 | @code { 3 | 4 | protected override void OnAfterRender(bool firstRender) 5 | { 6 | Navigation.NavigateTo("/"); 7 | } 8 | 9 | } 10 | -------------------------------------------------------------------------------- /src/CleanAspire.ClientApp/Layout/Redirections/RedirectToSignIn.razor: -------------------------------------------------------------------------------- 1 | 2 | @code { 3 | 4 | protected override void OnAfterRender(bool firstRender) 5 | { 6 | Navigation.NavigateTo("/account/signin"); 7 | } 8 | 9 | } 10 | -------------------------------------------------------------------------------- /src/CleanAspire.Domain/Common/IAuditTrial.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | namespace CleanAspire.Domain.Common; 5 | 6 | public interface IAuditTrial 7 | { 8 | } -------------------------------------------------------------------------------- /src/CleanAspire.Domain/Common/IMustHaveTenant.cs: -------------------------------------------------------------------------------- 1 | namespace CleanAspire.Domain.Common; 2 | 3 | public interface IMustHaveTenant 4 | { 5 | string TenantId { get; set; } 6 | } 7 | 8 | public interface IMayHaveTenant 9 | { 10 | string? TenantId { get; set; } 11 | } -------------------------------------------------------------------------------- /src/CleanAspire.ClientApp/Pages/Authentication.razor: -------------------------------------------------------------------------------- 1 | @page "/authentication/{action}" 2 | 3 | @using Microsoft.AspNetCore.Components.WebAssembly.Authentication 4 | 5 | 6 | @code{ 7 | [Parameter] public string? Action { get; set; } 8 | } 9 | -------------------------------------------------------------------------------- /src/CleanAspire.WebApp/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | }, 8 | "ClientAppSettings": { 9 | "ServiceBaseUrl": "https://localhost:7341" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/CleanAspire.ClientApp/wwwroot/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | }, 8 | "ClientAppSettings": { 9 | "ServiceBaseUrl": "https://localhost:7341" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/CleanAspire.ClientApp/Layout/RedirectToLogin.razor: -------------------------------------------------------------------------------- 1 | 2 | @using Microsoft.AspNetCore.Components.WebAssembly.Authentication 3 | @inject NavigationManager Navigation 4 | 5 | @code { 6 | protected override void OnInitialized() 7 | { 8 | Navigation.NavigateToLogin("authentication/login"); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/CleanAspire.ClientApp/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 | 5 | self.addEventListener('fetch', event => { 6 | 7 | }); 8 | -------------------------------------------------------------------------------- /src/CleanAspire.Domain/Common/IEntity.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | namespace CleanAspire.Domain.Common; 5 | 6 | public interface IEntity 7 | { 8 | } 9 | 10 | public interface IEntity : IEntity 11 | { 12 | T Id { get; set; } 13 | } -------------------------------------------------------------------------------- /src/CleanAspire.Domain/Common/ISoftDelete.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | namespace CleanAspire.Domain.Common; 5 | 6 | public interface ISoftDelete 7 | { 8 | DateTime? Deleted { get; set; } 9 | string? DeletedBy { get; set; } 10 | } -------------------------------------------------------------------------------- /src/CleanAspire.ClientApp/wwwroot/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | }, 8 | "ClientAppSettings": { 9 | "AppName": "Blazor Aspire", 10 | "Version": "v0.97", 11 | "ServiceBaseUrl": "https://cleanaspireservice.blazorserver.com" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/CleanAspire.AppHost/Program.cs: -------------------------------------------------------------------------------- 1 | var builder = DistributedApplication.CreateBuilder(args); 2 | 3 | var apiService = builder.AddProject("apiservice"); 4 | 5 | builder.AddProject("blazorweb") 6 | .WithExternalHttpEndpoints() 7 | .WithReference(apiService) 8 | .WaitFor(apiService); 9 | 10 | builder.Build().Run(); 11 | -------------------------------------------------------------------------------- /src/CleanAspire.WebApp/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | }, 8 | "AllowedHosts": "*", 9 | "ClientAppSettings": { 10 | "AppName": "Blazor Aspire", 11 | "Version": "v0.97", 12 | "ServiceBaseUrl": "https://cleanaspireservice.blazorserver.com" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/CleanAspire.Application/Common/Interfaces/IDateTime.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for more information. 4 | 5 | namespace CleanAspire.Application.Common.Interfaces; 6 | 7 | public interface IDateTime 8 | { 9 | DateTime Now { get; } 10 | } 11 | 12 | -------------------------------------------------------------------------------- /src/CleanAspire.Application/Common/Interfaces/ICurrentUserContext.cs: -------------------------------------------------------------------------------- 1 | using System.Security.Claims; 2 | 3 | namespace CleanAspire.Application.Common.Services; 4 | 5 | /// 6 | /// Interface representing the current user context. 7 | /// 8 | public interface ICurrentUserContext 9 | { 10 | 11 | ClaimsPrincipal? GetCurrentUser(); 12 | 13 | void Set(ClaimsPrincipal? user); 14 | void Clear(); 15 | } 16 | -------------------------------------------------------------------------------- /src/CleanAspire.ClientApp/Configurations/ClientAppSettings.cs: -------------------------------------------------------------------------------- 1 | namespace CleanAspire.ClientApp.Configurations; 2 | 3 | public class ClientAppSettings 4 | { 5 | public const string KEY = nameof(ClientAppSettings); 6 | public string AppName { get; set; } = "Blazor Aspire"; 7 | public string Version { get; set; } = "0.0.1"; 8 | public string ServiceBaseUrl { get; set; } = "https://apiservice.blazorserver.com"; 9 | 10 | } 11 | 12 | -------------------------------------------------------------------------------- /src/CleanAspire.ClientApp/Services/JsInterop/DownloadFileInterop.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.JSInterop; 2 | 3 | namespace CleanAspire.ClientApp.Services.JsInterop; 4 | 5 | public sealed class DownloadFileInterop(IJSRuntime jsRuntime) 6 | { 7 | public async Task DownloadFileFromStream(string fileName, DotNetStreamReference stream) 8 | { 9 | await jsRuntime.InvokeVoidAsync("downloadFileFromStream", fileName, stream); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/CleanAspire.Domain/Common/BaseAuditableSoftDeleteEntity.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | namespace CleanAspire.Domain.Common; 5 | 6 | public abstract class BaseAuditableSoftDeleteEntity : BaseAuditableEntity, ISoftDelete 7 | { 8 | public DateTime? Deleted { get; set; } 9 | public string? DeletedBy { get; set; } 10 | } -------------------------------------------------------------------------------- /src/CleanAspire.ClientApp/Components/WebpushrSetup.razor: -------------------------------------------------------------------------------- 1 | 2 | @code { 3 | protected override async Task OnInitializedAsync() 4 | { 5 | var online = await OnlineStatusInterop.GetOnlineStatusAsync(); 6 | if (online) 7 | { 8 | var publicKey = await WebpushrService.GetPublicKeyAsync(); 9 | var webpushr = new Webpushr(JS); 10 | await webpushr.SetupWebpushrAsync(publicKey!); 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /src/CleanAspire.WebApp/Components/_Imports.razor: -------------------------------------------------------------------------------- 1 | @using System.Net.Http 2 | @using System.Net.Http.Json 3 | @using Microsoft.AspNetCore.Components.Forms 4 | @using Microsoft.AspNetCore.Components.Routing 5 | @using Microsoft.AspNetCore.Components.Web 6 | @using static Microsoft.AspNetCore.Components.Web.RenderMode 7 | @using Microsoft.AspNetCore.Components.Web.Virtualization 8 | @using Microsoft.JSInterop 9 | @using CleanAspire.WebApp 10 | @using CleanAspire.ClientApp 11 | @using CleanAspire.WebApp.Components 12 | 13 | -------------------------------------------------------------------------------- /.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 26 | !**/.gitignore 27 | !.git/HEAD 28 | !.git/config 29 | !.git/packed-refs 30 | !.git/refs/heads/** -------------------------------------------------------------------------------- /src/CleanAspire.ClientApp/wwwroot/js/downloadfile.js: -------------------------------------------------------------------------------- 1 | window.downloadFileFromStream = async (fileName, contentStreamReference) => { 2 | const arrayBuffer = await contentStreamReference.arrayBuffer(); 3 | const blob = new Blob([arrayBuffer]); 4 | const url = URL.createObjectURL(blob); 5 | const anchorElement = document.createElement('a'); 6 | anchorElement.href = url; 7 | anchorElement.download = fileName ?? ''; 8 | anchorElement.click(); 9 | anchorElement.remove(); 10 | URL.revokeObjectURL(url); 11 | } -------------------------------------------------------------------------------- /src/CleanAspire.Application/Common/Interfaces/ICurrentUserAccessor.cs: -------------------------------------------------------------------------------- 1 | using System.Security.Claims; 2 | 3 | namespace CleanAspire.Application.Common.Services; 4 | 5 | /// 6 | /// Interface to access the current user's session information. 7 | /// 8 | public interface ICurrentUserAccessor 9 | { 10 | /// 11 | /// Gets the current session information of the user. 12 | /// 13 | ClaimsPrincipal? User { get; } 14 | string? UserId { get; } 15 | string? TenantId { get; } 16 | } 17 | -------------------------------------------------------------------------------- /src/CleanAspire.ClientApp/Routes.razor: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Not found 8 | 9 |

Sorry, there's nothing at this address.

10 |
11 |
12 |
13 | -------------------------------------------------------------------------------- /src/CleanAspire.ClientApp/App.razor: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Not found 9 | 10 |

Sorry, there's nothing at this address.

11 |
12 |
13 |
14 | -------------------------------------------------------------------------------- /src/CleanAspire.Domain/Events/DomainEvent.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | using Mediator; 5 | 6 | namespace CleanAspire.Domain; 7 | 8 | public abstract class DomainEvent : INotification 9 | { 10 | protected DomainEvent() 11 | { 12 | DateOccurred = DateTimeOffset.UtcNow; 13 | } 14 | 15 | public bool IsPublished { get; set; } 16 | public DateTimeOffset DateOccurred { get; protected set; } 17 | } 18 | 19 | -------------------------------------------------------------------------------- /src/CleanAspire.Api/Webpushr/WebpushrOptions.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for more information. 4 | 5 | namespace CleanAspire.Api.Webpushr; 6 | 7 | public class WebpushrOptions 8 | { 9 | public static string Key = "Webpushr"; 10 | public string Token { get; set; } = string.Empty; 11 | public string ApiKey { get; set; } = string.Empty; 12 | public string PublicKey { get; set; } = string.Empty; 13 | } 14 | -------------------------------------------------------------------------------- /src/CleanAspire.ClientApp/Services/PushNotifications/IWebpushrService.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for more information. 4 | 5 | using System.Threading.Tasks; 6 | 7 | namespace CleanAspire.ClientApp.Services.PushNotifications; 8 | 9 | public interface IWebpushrService 10 | { 11 | Task GetPublicKeyAsync(); 12 | Task SendNotificationAsync(string title, string message, string url, string? sid = null); 13 | } 14 | -------------------------------------------------------------------------------- /src/CleanAspire.ClientApp/Services/JsInterop/Webpushr.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for more information. 4 | 5 | using Microsoft.JSInterop; 6 | 7 | namespace CleanAspire.ClientApp.Services.JsInterop; 8 | 9 | public class Webpushr(IJSRuntime jsRuntime) 10 | { 11 | public async Task SetupWebpushrAsync(string key) 12 | { 13 | await jsRuntime.InvokeVoidAsync("webpushrInterop.setupWebpushr", key); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/CleanAspire.ClientApp/wwwroot/manifest.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "CleanAspire.ClientApp", 3 | "short_name": "CleanAspire.ClientApp", 4 | "id": "./", 5 | "start_url": "./", 6 | "display": "standalone", 7 | "background_color": "#ffffff", 8 | "theme_color": "#03173d", 9 | "prefer_related_applications": false, 10 | "icons": [ 11 | { 12 | "src": "icon-512.png", 13 | "type": "image/png", 14 | "sizes": "512x512" 15 | }, 16 | { 17 | "src": "icon-192.png", 18 | "type": "image/png", 19 | "sizes": "192x192" 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /src/CleanAspire.ClientApp/Services/UserPreferences/UserPreferences.cs: -------------------------------------------------------------------------------- 1 | namespace CleanAspire.ClientApp.Services.UserPreferences; 2 | 3 | public class UserPreferences 4 | { 5 | /// 6 | /// Set the direction layout of the docs to RTL or LTR. If true RTL is used 7 | /// 8 | public bool RightToLeft { get; set; } 9 | 10 | /// 11 | /// The current dark light mode that is used 12 | /// 13 | public DarkLightMode DarkLightTheme { get; set; } 14 | } 15 | 16 | public enum DarkLightMode 17 | { 18 | System = 0, 19 | Light = 1, 20 | Dark = 2 21 | } 22 | -------------------------------------------------------------------------------- /src/CleanAspire.Infrastructure/Services/UtcDateTime.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for more information. 4 | 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | using System.Text; 9 | using System.Threading.Tasks; 10 | using CleanAspire.Application.Common.Interfaces; 11 | 12 | namespace CleanAspire.Infrastructure.Services; 13 | 14 | public class UtcDateTime : IDateTime 15 | { 16 | public DateTime Now => DateTime.UtcNow; 17 | } 18 | 19 | -------------------------------------------------------------------------------- /src/CleanAspire.ClientApp/Services/JsInterop/Swiper.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.JSInterop; 2 | 3 | namespace CleanAspire.ClientApp.Services.JsInterop; 4 | 5 | public sealed class Swiper 6 | { 7 | private readonly IJSRuntime _jsRuntime; 8 | 9 | public Swiper(IJSRuntime jsRuntime) 10 | { 11 | _jsRuntime = jsRuntime; 12 | } 13 | 14 | public async Task Initialize(string elment,bool reverse=false) 15 | { 16 | var jsmodule = await _jsRuntime.InvokeAsync("import", "/js/carousel.js"); 17 | return jsmodule.InvokeVoidAsync("initializeSwiper", elment, reverse); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/CleanAspire.Application/Features/Tenants/DTOs/TenantDto.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for more information. 4 | 5 | namespace CleanAspire.Application.Features.Tenants.DTOs; 6 | 7 | public class TenantDto 8 | { 9 | // Unique identifier for the tenant 10 | public string Id { get; set; } = string.Empty; 11 | // Tenant name, can be null 12 | public string? Name { get; set; } 13 | // Tenant description, can be null 14 | public string? Description { get; set; } 15 | } 16 | -------------------------------------------------------------------------------- /src/CleanAspire.Domain/CleanAspire.Domain.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net10.0 5 | enable 6 | enable 7 | CleanAspire.Domain 8 | CleanAspire.Domain 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/CleanAspire.ClientApp/wwwroot/js/onlinestatus.js: -------------------------------------------------------------------------------- 1 | 2 | window.onlineStatusInterop = { 3 | getOnlineStatus: function () { 4 | return navigator.onLine; 5 | }, 6 | addOnlineStatusListener: function (dotNetObjectRef) { 7 | const onlineHandler = () => { 8 | dotNetObjectRef.invokeMethodAsync('UpdateOnlineStatus', true); 9 | }; 10 | const offlineHandler = () => { 11 | dotNetObjectRef.invokeMethodAsync('UpdateOnlineStatus', false); 12 | }; 13 | 14 | window.addEventListener('online', onlineHandler); 15 | window.addEventListener('offline', offlineHandler); 16 | } 17 | }; -------------------------------------------------------------------------------- /src/CleanAspire.ClientApp/Services/JsInterop/Fancybox.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.JSInterop; 2 | 3 | namespace CleanAspire.ClientApp.Services.JsInterop; 4 | public sealed class Fancybox 5 | { 6 | private readonly IJSRuntime _jsRuntime; 7 | 8 | public Fancybox(IJSRuntime jsRuntime) 9 | { 10 | _jsRuntime = jsRuntime; 11 | } 12 | 13 | public async Task Preview(string defaultUrl, IEnumerable images) 14 | { 15 | var jsmodule = await _jsRuntime.InvokeAsync("import", "/js/fancybox.js").ConfigureAwait(false); 16 | return jsmodule.InvokeVoidAsync("filepreview", defaultUrl, 17 | images); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/CleanAspire.Application/Common/Interfaces/FusionCache/IFusionCacheRequest.cs: -------------------------------------------------------------------------------- 1 | namespace CleanAspire.Application.Common.Interfaces.FusionCache; 2 | 3 | /// 4 | /// Represents a request that supports caching with FusionCache. 5 | /// 6 | /// The type of the response. 7 | public interface IFusionCacheRequest : IRequest 8 | { 9 | /// 10 | /// Gets the cache key for the request. 11 | /// 12 | string CacheKey => string.Empty; 13 | 14 | /// 15 | /// Gets the tags associated with the cache entry. 16 | /// 17 | IEnumerable? Tags { get; } 18 | } 19 | -------------------------------------------------------------------------------- /src/CleanAspire.Domain/Entities/Tenant.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for more information. 4 | 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | using System.Text; 9 | using System.Threading.Tasks; 10 | using CleanAspire.Domain.Common; 11 | 12 | namespace CleanAspire.Domain.Entities; 13 | 14 | public class Tenant : IEntity 15 | { 16 | public string? Name { get; set; } 17 | public string? Description { get; set; } 18 | public string Id { get; set; } = Guid.CreateVersion7().ToString(); 19 | } 20 | -------------------------------------------------------------------------------- /src/CleanAspire.Infrastructure/Services/CurrentUserContext.cs: -------------------------------------------------------------------------------- 1 | using System.Security.Claims; 2 | using CleanAspire.Application.Common.Services; 3 | 4 | namespace CleanAspire.Infrastructure.Services; 5 | 6 | /// 7 | /// Represents the current user context, holding session information. 8 | /// 9 | public class CurrentUserContext : ICurrentUserContext 10 | { 11 | private static AsyncLocal _currentUser = new AsyncLocal(); 12 | 13 | public ClaimsPrincipal? GetCurrentUser() => _currentUser.Value; 14 | 15 | public void Set(ClaimsPrincipal? user) => _currentUser.Value = user; 16 | public void Clear() => _currentUser.Value = null; 17 | } 18 | -------------------------------------------------------------------------------- /src/CleanAspire.AppHost/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "UseInMemoryDatabase": false, 3 | "DatabaseSettings": { 4 | "DBProvider": "sqlite", 5 | "ConnectionString": "Data Source=CleanAspireDb.db" 6 | //"DBProvider": "mssql", 7 | //"ConnectionString": "Server=(localdb)\\mssqllocaldb;Database=CleanAspireDb;Trusted_Connection=True;MultipleActiveResultSets=true;" 8 | //"DBProvider": "postgresql", 9 | //"ConnectionString": "Server=127.0.0.1;Database=CleanAspireDb;User Id=root;Password=root;Port=5432" 10 | }, 11 | "Logging": { 12 | "LogLevel": { 13 | "Default": "Information", 14 | "Microsoft.AspNetCore": "Warning", 15 | "Aspire.Hosting.Dcp": "Warning" 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/CleanAspire.Application/Common/Interfaces/ICurrentUserContextSetter.cs: -------------------------------------------------------------------------------- 1 | using System.Security.Claims; 2 | 3 | namespace CleanAspire.Application.Common.Services; 4 | 5 | /// 6 | /// Interface for setting and clearing the current user context. 7 | /// 8 | public interface ICurrentUserContextSetter 9 | { 10 | /// 11 | /// Sets the current user context with the provided session information. 12 | /// 13 | /// The session information of the current user. 14 | void SetCurrentUser(ClaimsPrincipal user); 15 | 16 | /// 17 | /// Clears the current user context. 18 | /// 19 | void Clear(); 20 | } 21 | -------------------------------------------------------------------------------- /src/CleanAspire.ClientApp/Components/ConfirmationDialog.razor: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | @ContentText 5 | 6 | 7 | Cancel 8 | Confirm 9 | 10 | 11 | 12 | @code 13 | { 14 | [CascadingParameter] private IMudDialogInstance MudDialog { get; set; } = default!; 15 | 16 | [EditorRequired][Parameter] public string? ContentText { get; set; } 17 | 18 | } -------------------------------------------------------------------------------- /src/CleanAspire.Infrastructure/Persistence/BlazorContextFactory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.EntityFrameworkCore; 3 | using Microsoft.Extensions.DependencyInjection; 4 | 5 | namespace CleanAspire.Infrastructure.Persistence; 6 | 7 | public class BlazorContextFactory : IDbContextFactory where TContext : DbContext 8 | { 9 | private readonly IServiceProvider _provider; 10 | 11 | public BlazorContextFactory(IServiceProvider provider) 12 | { 13 | _provider = provider; 14 | } 15 | 16 | public TContext CreateDbContext() 17 | { 18 | var scope = _provider.CreateScope(); 19 | return scope.ServiceProvider.GetRequiredService(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/CleanAspire.Application/Common/Interfaces/FusionCache/IFusionCacheRefreshRequest.cs: -------------------------------------------------------------------------------- 1 | namespace CleanAspire.Application.Common.Interfaces.FusionCache; 2 | 3 | /// 4 | /// Represents a request to refresh a cache entry in FusionCache. 5 | /// 6 | /// The type of the response. 7 | public interface IFusionCacheRefreshRequest : IRequest 8 | { 9 | /// 10 | /// Gets the cache key associated with the request. 11 | /// 12 | string CacheKey => string.Empty; 13 | 14 | /// 15 | /// Gets the tags associated with the cache entry. 16 | /// 17 | IEnumerable? Tags { get; } 18 | } 19 | -------------------------------------------------------------------------------- /src/CleanAspire.ClientApp/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Components.WebAssembly.Hosting; 2 | using CleanAspire.ClientApp; 3 | using Microsoft.AspNetCore.Components.Web; 4 | 5 | var builder = WebAssemblyHostBuilder.CreateDefault(args); 6 | 7 | #if STANDALONE 8 | builder.RootComponents.Add("#app"); 9 | builder.RootComponents.Add("head::after"); 10 | #endif 11 | // register the cookie handler 12 | builder.Services.AddCoreServices(builder.Configuration); 13 | builder.Services.AddHttpClients(builder.Configuration); 14 | builder.Services.AddAuthenticationAndLocalization(builder.Configuration); 15 | var app = builder.Build(); 16 | 17 | await app.InitializeCultureAsync(); 18 | 19 | await app.RunAsync(); 20 | 21 | -------------------------------------------------------------------------------- /src/CleanAspire.ClientApp/Services/Identity/UserProfileStoreEvent.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for more information. 4 | 5 | using CleanAspire.Api.Client.Models; 6 | 7 | namespace CleanAspire.ClientApp.Services.Identity; 8 | 9 | public class UserProfileStoreEvent 10 | { 11 | public ProfileResponse? Profile { get; private set; } 12 | 13 | 14 | public UserProfileStoreEvent(ProfileResponse? profile) 15 | { 16 | Profile = profile; 17 | } 18 | 19 | public override string ToString() 20 | { 21 | return $"userId: {Profile?.UserId}"; 22 | } 23 | } 24 | 25 | -------------------------------------------------------------------------------- /src/CleanAspire.ClientApp/Services/LanguageService.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for more information. 4 | 5 | using System.Globalization; 6 | 7 | namespace CleanAspire.ClientApp.Services; 8 | 9 | public class LanguageService 10 | { 11 | public event Action? OnLanguageChanged; 12 | 13 | public void SetLanguage(string cultureCode) 14 | { 15 | var culture = new CultureInfo(cultureCode); 16 | CultureInfo.DefaultThreadCurrentCulture = culture; 17 | CultureInfo.DefaultThreadCurrentUICulture = culture; 18 | OnLanguageChanged?.Invoke(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/CleanAspire.ClientApp/Components/FileSizeFormatter.razor: -------------------------------------------------------------------------------- 1 | 2 | @FormattedSize 3 | 4 | @code { 5 | [Parameter] 6 | public int FileSizeInBytes { get; set; } 7 | 8 | private string FormattedSize => FormatFileSize(FileSizeInBytes); 9 | 10 | private string FormatFileSize(int fileSizeInBytes) 11 | { 12 | const int scale = 1024; 13 | string[] units = { "B", "KB", "MB", "GB", "TB", "PB", "EB" }; 14 | 15 | int size = fileSizeInBytes; 16 | int unitIndex = 0; 17 | 18 | while (size >= scale && unitIndex < units.Length - 1) 19 | { 20 | size /= scale; 21 | unitIndex++; 22 | } 23 | 24 | return $"{size:0.#} {units[unitIndex]}"; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Migrators/Migrators.PostgreSQL/Migrators.PostgreSQL.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net10.0 5 | enable 6 | enable 7 | CleanAspire.Migrators.PostgreSQL 8 | CleanAspire.Migrators.PostgreSQL 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/CleanAspire.ClientApp/Services/JsInterop/DisplayModeInterop.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for more information. 4 | 5 | using Microsoft.JSInterop; 6 | 7 | namespace CleanAspire.ClientApp.Services.JsInterop; 8 | 9 | public sealed class DisplayModeInterop 10 | { 11 | private readonly IJSRuntime _jsRuntime; 12 | 13 | public DisplayModeInterop(IJSRuntime jsRuntime) 14 | { 15 | _jsRuntime = jsRuntime; 16 | } 17 | public async Task GetDisplayModeAsync() 18 | { 19 | return await _jsRuntime.InvokeAsync("displayModeInterop.getDisplayMode"); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/CleanAspire.ClientApp/Layout/AuthenticationLayout.razor: -------------------------------------------------------------------------------- 1 | @using CleanAspire.ClientApp.Layout.Redirections 2 | 3 | @using Microsoft.AspNetCore.Components.Authorization 4 | 5 | @inherits LayoutComponentBase 6 | @layout MainLayout 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | @Body 16 |
17 |
18 |
19 | 20 |
21 |
22 | 23 | @code { 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/CleanAspire.Application/Common/Models/PaginatedResult.cs: -------------------------------------------------------------------------------- 1 | 2 | namespace CleanAspire.Application.Common.Models; 3 | public class PaginatedResult 4 | { 5 | public PaginatedResult() { } 6 | public PaginatedResult(IEnumerable items, int total, int pageIndex, int pageSize) 7 | { 8 | Items = items; 9 | TotalItems = total; 10 | CurrentPage = pageIndex; 11 | TotalPages = (int)Math.Ceiling(total / (double)pageSize); 12 | } 13 | 14 | public int CurrentPage { get; } 15 | public int TotalItems { get; private set; } 16 | public int TotalPages { get; } 17 | public bool HasPreviousPage => CurrentPage > 1; 18 | public bool HasNextPage => CurrentPage < TotalPages; 19 | public IEnumerable Items { get; set; } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/CleanAspire.ClientApp/Components/Autocompletes/PicklistDataSource.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for more information. 4 | 5 | namespace CleanAspire.ClientApp.Components.Autocompletes; 6 | 7 | public static class PicklistDataSource 8 | { 9 | public static readonly Dictionary Data = new Dictionary 10 | { 11 | { PicklistType.UOM, new[] { "EA", "PCS", "KG", "M", "L", "BOX", "PKG" } }, 12 | { PicklistType.Currency, new[] { "USD", "EUR", "GBP", "CNY", "JPY", "AUD", "CAD" } } 13 | }; 14 | } 15 | public enum PicklistType 16 | { 17 | UOM, 18 | Currency 19 | } 20 | -------------------------------------------------------------------------------- /src/CleanAspire.ClientApp/wwwroot/js/webpushr.js: -------------------------------------------------------------------------------- 1 | window.webpushrInterop = { 2 | setupWebpushr: function (key) { 3 | (function (w, d, s, id) { 4 | if (typeof (w.webpushr) !== 'undefined') return; 5 | w.webpushr = w.webpushr || function () { (w.webpushr.q = w.webpushr.q || []).push(arguments); }; 6 | var js, fjs = d.getElementsByTagName(s)[0]; 7 | js = d.createElement(s); js.id = id; js.async = 1; 8 | js.src = "https://cdn.webpushr.com/app.min.js"; 9 | fjs.parentNode.appendChild(js); 10 | }(window, document, 'script', 'webpushr-jssdk')); 11 | 12 | webpushr('setup', { 'key': key }); 13 | 14 | webpushr('fetch_id', function (sid) { 15 | console.log(sid); 16 | }); 17 | } 18 | }; -------------------------------------------------------------------------------- /src/CleanAspire.Infrastructure/Configurations/MinioOptions.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for more information. 4 | 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | using System.Text; 9 | using System.Threading.Tasks; 10 | 11 | namespace CleanAspire.Infrastructure.Configurations; 12 | public class MinioOptions 13 | { 14 | public const string Key = "Minio"; 15 | public string Endpoint { get; set; } = "minio.blazorserver.com"; 16 | public string AccessKey { get; set; } = string.Empty; 17 | public string SecretKey { get; set; } = string.Empty; 18 | public string BucketName { get; set; } = "demo"; 19 | } 20 | -------------------------------------------------------------------------------- /src/CleanAspire.ClientApp/Pages/Account/Profile/Setting.razor: -------------------------------------------------------------------------------- 1 | @page "/profile/setting" 2 | 3 | @L["Setting"] 4 | @L["Settings"] 5 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | @code { 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/CleanAspire.Domain/Common/BaseAuditableEntity.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | namespace CleanAspire.Domain.Common; 5 | 6 | public abstract class BaseAuditableEntity : BaseEntity, IAuditableEntity 7 | { 8 | public virtual DateTime? Created { get; set; } 9 | 10 | public virtual string? CreatedBy { get; set; } 11 | 12 | public virtual DateTime? LastModified { get; set; } 13 | 14 | public virtual string? LastModifiedBy { get; set; } 15 | } 16 | 17 | public interface IAuditableEntity 18 | { 19 | DateTime? Created { get; set; } 20 | 21 | string? CreatedBy { get; set; } 22 | 23 | DateTime? LastModified { get; set; } 24 | 25 | string? LastModifiedBy { get; set; } 26 | } -------------------------------------------------------------------------------- /src/CleanAspire.ClientApp/Layout/ApplicationLayout.razor: -------------------------------------------------------------------------------- 1 | @using CleanAspire.ClientApp.Layout.Redirections 2 | 3 | @using Microsoft.AspNetCore.Components.Authorization 4 | 5 | @inherits LayoutComponentBase 6 | @layout MainLayout 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | @Body 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | @code { 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/CleanAspire.ClientApp/nginx.conf: -------------------------------------------------------------------------------- 1 | worker_processes auto; 2 | 3 | events { 4 | worker_connections 1024; 5 | } 6 | 7 | http { 8 | include /etc/nginx/mime.types; 9 | default_type application/octet-stream; 10 | 11 | # Define a server block here 12 | server { 13 | listen 80; 14 | listen 443 ssl; 15 | 16 | ssl_certificate /etc/ssl/certs/nginx-selfsigned.crt; 17 | ssl_certificate_key /etc/ssl/private/nginx-selfsigned.key; 18 | 19 | server_name localhost; 20 | 21 | location / { 22 | root /usr/share/nginx/html; 23 | index index.html index.htm; 24 | try_files $uri $uri/ /index.html; 25 | } 26 | 27 | error_page 404 /404.html; 28 | location = /40x.html { 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /src/CleanAspire.Api/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/launchsettings.json", 3 | "profiles": { 4 | "http": { 5 | "commandName": "Project", 6 | "dotnetRunMessages": true, 7 | "launchBrowser": true, 8 | "launchUrl": "scalar/v1", 9 | "applicationUrl": "http://localhost:5519", 10 | "environmentVariables": { 11 | "ASPNETCORE_ENVIRONMENT": "Development" 12 | } 13 | }, 14 | "https": { 15 | "commandName": "Project", 16 | "dotnetRunMessages": true, 17 | "launchBrowser": true, 18 | "launchUrl": "scalar/v1", 19 | "applicationUrl": "https://localhost:7341;http://localhost:5519", 20 | "environmentVariables": { 21 | "ASPNETCORE_ENVIRONMENT": "Development" 22 | } 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Migrators/Migrators.SQLite/Migrators.SQLite.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net10.0 5 | enable 6 | enable 7 | CleanAspire.Migrators.SQLite 8 | CleanAspire.Migrators.SQLite 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/CleanAspire.Api/Endpoints/EndpointExtensions.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for more information. 4 | 5 | namespace CleanAspire.Api.Endpoints; 6 | 7 | public static class EndpointExtensions 8 | { 9 | public static IEndpointRouteBuilder MapEndpointDefinitions(this IEndpointRouteBuilder routes) 10 | { 11 | using (var scope = routes.ServiceProvider.CreateScope()) 12 | { 13 | var registrars = scope.ServiceProvider.GetServices(); 14 | foreach (var registrar in registrars) 15 | { 16 | registrar.RegisterRoutes(routes); 17 | } 18 | } 19 | return routes; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/CleanAspire.ClientApp/Services/Identity/ISignInManagement.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for more information. 4 | 5 | using CleanAspire.Api.Client.Models; 6 | 7 | namespace CleanAspire.ClientApp.Services.Identity; 8 | 9 | public interface ISignInManagement 10 | { 11 | Task LoginAsync(LoginRequest request, bool remember = true, CancellationToken cancellationToken = default); 12 | Task LoginWithGoogle(string authorizationCode, string state, CancellationToken cancellationToken = default); 13 | Task LoginWithMicrosoft(string authorizationCode, string state, CancellationToken cancellationToken = default); 14 | Task LogoutAsync(CancellationToken cancellationToken = default); 15 | } 16 | 17 | -------------------------------------------------------------------------------- /src/CleanAspire.ClientApp/wwwroot/loading.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | 11 | 12 | 19 | 20 | -------------------------------------------------------------------------------- /src/CleanAspire.Api/Endpoints/IEndpointRegistrar.cs: -------------------------------------------------------------------------------- 1 | // This namespace contains utilities for defining and registering API endpoint routes in a minimal API setup. 2 | 3 | // Purpose: 4 | // 1. **`IEndpointRegistrar` Interface**: 5 | // - Provides a contract for defining endpoint registration logic. 6 | // - Ensures consistency across all endpoint registration implementations by enforcing a common method (`RegisterRoutes`). 7 | 8 | namespace CleanAspire.Api.Endpoints; 9 | 10 | /// 11 | /// Defines a contract for registering endpoint routes. 12 | /// 13 | public interface IEndpointRegistrar 14 | { 15 | /// 16 | /// Registers the routes for the application. 17 | /// 18 | /// The to add routes to. 19 | void RegisterRoutes(IEndpointRouteBuilder routes); 20 | } 21 | -------------------------------------------------------------------------------- /src/Migrators/Migrators.MSSQL/Migrators.MSSQL.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net10.0 5 | enable 6 | enable 7 | CleanAspire.Migrators.MSSQL 8 | CleanAspire.Migrators.MSSQL 9 | default 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/CleanAspire.Infrastructure/Persistence/Configurations/TenantConfiguration.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | using System.Text.Json; 5 | using CleanAspire.Domain.Entities; 6 | using Microsoft.EntityFrameworkCore; 7 | using Microsoft.EntityFrameworkCore.ChangeTracking; 8 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 9 | 10 | namespace CleanAspire.Infrastructure.Persistence.Configurations; 11 | #nullable disable 12 | public class TenantConfiguration : IEntityTypeConfiguration 13 | { 14 | public void Configure(EntityTypeBuilder builder) 15 | { 16 | builder.HasIndex(x => x.Name).IsUnique(); 17 | builder.Property(x=>x.Name).HasMaxLength(80).IsRequired(); 18 | builder.Property(x => x.Id).HasMaxLength(36); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/CleanAspire.ClientApp/Services/Identity/UserProfileStore.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for more information. 4 | 5 | using CleanAspire.Api.Client.Models; 6 | 7 | namespace CleanAspire.ClientApp.Services.Identity; 8 | 9 | public class UserProfileStore 10 | { 11 | public event EventHandler? OnChange; 12 | public ProfileResponse? Profile { get; private set; } 13 | 14 | public void Set(ProfileResponse? profile) 15 | { 16 | Profile = profile; 17 | OnChange?.Invoke(this, new UserProfileStoreEvent(Profile)); 18 | } 19 | 20 | public void Clear() 21 | { 22 | Profile = null; 23 | OnChange?.Invoke(this, new UserProfileStoreEvent(Profile)); 24 | } 25 | } 26 | 27 | -------------------------------------------------------------------------------- /src/CleanAspire.Application/_Imports.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for more information. 4 | 5 | global using System; 6 | global using System.Collections.Generic; 7 | global using System.Linq; 8 | global using System.Threading.Tasks; 9 | global using CleanAspire.Domain; 10 | global using Microsoft.EntityFrameworkCore; 11 | global using CleanAspire.Application.Common.Interfaces; 12 | global using CleanAspire.Application.Common.Interfaces.FusionCache; 13 | global using CleanAspire.Application.Common.Models; 14 | global using CleanAspire.Application.Common; 15 | global using CleanAspire.Domain.Entities; 16 | global using FluentValidation; 17 | global using Mediator; 18 | global using Microsoft.Extensions.Logging; 19 | global using ZiggyCreatures.Caching.Fusion; 20 | -------------------------------------------------------------------------------- /src/CleanAspire.ClientApp/Services/PushNotifications/WebpushrOptionsCache.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for more information. 4 | 5 | using CleanAspire.Api.Client.Models; 6 | 7 | namespace CleanAspire.ClientApp.Services.PushNotifications; 8 | 9 | public class WebpushrOptionsCache 10 | { 11 | private WebpushrOptions? _options; 12 | private DateTime _cacheExpiry = DateTime.MinValue; 13 | 14 | public async Task GetOptionsAsync(Func> fetchOptionsFunc) 15 | { 16 | if (_options == null || _cacheExpiry < DateTime.UtcNow) 17 | { 18 | _options = await fetchOptionsFunc(); 19 | _cacheExpiry = DateTime.UtcNow.AddHours(24); 20 | } 21 | return _options; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/CleanAspire.ClientApp/Pages/Account/ForgetPasswordSuccessful.razor: -------------------------------------------------------------------------------- 1 | @page "/account/forgetpasswordsuccessful" 2 | 3 | 4 | @L["Check Your Inbox"] 5 | 6 |
7 | 8 | @L["Check Your Inbox"] 9 |
10 |
11 | 12 |
13 | 14 | @L["We've sent an email to the address you provided. Please check your inbox for a link to reset your password. If you don't see the email, be sure to check your spam or junk folder."] 15 | 16 |
17 | 18 | 19 |
20 | 21 |
22 | @code { 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/CleanAspire.ClientApp/Pages/Account/SignupSuccessful.razor: -------------------------------------------------------------------------------- 1 | @page "/account/signupsuccessful" 2 | 3 | 4 | @L["Signup Successful"] 5 | 6 |
7 | 8 | @L["Signup Successful"] 9 |
10 |
11 | 12 |
13 | @L["Thank you for signing up. To complete your registration, please activate your account by clicking the activation link sent to your email address. If you do not receive the email within a few minutes, please check your spam or junk folder."] 14 |
15 | 16 | 17 |
18 | 19 |
20 | @code { 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/CleanAspire.Domain/Common/BaseEntity.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | using System.ComponentModel.DataAnnotations.Schema; 5 | 6 | namespace CleanAspire.Domain.Common; 7 | 8 | public abstract class BaseEntity : IEntity 9 | { 10 | private readonly List _domainEvents = new(); 11 | 12 | [NotMapped] public IReadOnlyCollection DomainEvents => _domainEvents.AsReadOnly(); 13 | 14 | public virtual string Id { get; set; } = Guid.CreateVersion7().ToString(); 15 | 16 | public void AddDomainEvent(DomainEvent domainEvent) 17 | { 18 | _domainEvents.Add(domainEvent); 19 | } 20 | 21 | public void RemoveDomainEvent(DomainEvent domainEvent) 22 | { 23 | _domainEvents.Remove(domainEvent); 24 | } 25 | 26 | public void ClearDomainEvents() 27 | { 28 | _domainEvents.Clear(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/CleanAspire.AppHost/CleanAspire.AppHost.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Exe 7 | net10.0 8 | enable 9 | enable 10 | true 11 | true 12 | true 13 | ac63aff4-1f44-46a9-9c5d-0b26b517e142 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/CleanAspire.Infrastructure/Persistence/Conversions/StringListConverter.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | using System.Text.Json; 5 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 6 | 7 | namespace CleanAspire.Infrastructure.Persistence.Conversions; 8 | 9 | public class StringListConverter : ValueConverter, string> 10 | { 11 | private readonly static JsonSerializerOptions DefaultJsonSerializerOptions = new JsonSerializerOptions 12 | { 13 | PropertyNameCaseInsensitive = true, 14 | PropertyNamingPolicy = JsonNamingPolicy.CamelCase 15 | }; 16 | public StringListConverter() : base(v => JsonSerializer.Serialize(v, DefaultJsonSerializerOptions), 17 | v => JsonSerializer.Deserialize>(string.IsNullOrEmpty(v) ? "[]" : v, 18 | DefaultJsonSerializerOptions) ?? new List() 19 | ) 20 | { 21 | } 22 | } -------------------------------------------------------------------------------- /src/CleanAspire.Application/Features/Products/Validators/DeleteProductCommandValidator.cs: -------------------------------------------------------------------------------- 1 | using CleanAspire.Application.Features.Products.Commands; 2 | 3 | namespace CleanAspire.Application.Features.Products.Validators; 4 | /// 5 | /// Validator for DeleteProductCommand. 6 | /// Uses FluentValidation to apply validation rules for deleting products. 7 | /// 8 | public class DeleteProductCommandValidator : AbstractValidator 9 | { 10 | /// 11 | /// Initializes validation rules for the IDs of products to be deleted. 12 | /// 13 | public DeleteProductCommandValidator() 14 | { 15 | // Validate that the IDs collection is not empty 16 | RuleFor(command => command.Ids) 17 | .NotEmpty().WithMessage("At least one product ID is required.") 18 | .Must(ids => ids != null && ids.All(id => !string.IsNullOrWhiteSpace(id))) 19 | .WithMessage("Product IDs must not be empty or whitespace."); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/CleanAspire.ClientApp/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "http": { 4 | "commandName": "Project", 5 | "launchBrowser": true, 6 | "environmentVariables": { 7 | "ASPNETCORE_ENVIRONMENT": "Development", 8 | "BLAZOR_RENDER_MODE": "Standalone" 9 | }, 10 | "dotnetRunMessages": true, 11 | "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", 12 | "applicationUrl": "http://localhost:5180" 13 | }, 14 | "https": { 15 | "commandName": "Project", 16 | "launchBrowser": true, 17 | "environmentVariables": { 18 | "ASPNETCORE_ENVIRONMENT": "Standalone" 19 | }, 20 | "dotnetRunMessages": true, 21 | "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", 22 | "applicationUrl": "https://localhost:7123;http://localhost:5180" 23 | } 24 | }, 25 | "$schema": "https://json.schemastore.org/launchsettings.json" 26 | } -------------------------------------------------------------------------------- /src/CleanAspire.WebApp/CleanAspire.WebApp.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net10.0 5 | enable 6 | enable 7 | aa04e12f-2328-4d88-a3b5-5b0dfc063bbe 8 | Linux 9 | ..\.. 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/CleanAspire.ClientApp/Services/OfflineSyncService.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for more information. 4 | 5 | namespace CleanAspire.ClientApp.Services; 6 | 7 | public sealed class OfflineSyncService 8 | { 9 | public SyncStatus CurrentStatus { get; private set; } = SyncStatus.Idle; 10 | public string StatusMessage { get; private set; } = "Idle"; 11 | 12 | public int TotalRequests { get; private set; } 13 | public int RequestsProcessed { get; private set; } 14 | public event Action? OnSyncStateChanged; 15 | public void SetSyncStatus(SyncStatus status, string message, int total = 0, int processed = 0) 16 | { 17 | CurrentStatus = status; 18 | StatusMessage = message; 19 | TotalRequests = total; 20 | RequestsProcessed = processed; 21 | OnSyncStateChanged?.Invoke(); 22 | } 23 | } 24 | public enum SyncStatus 25 | { 26 | Idle, 27 | Syncing, 28 | Completed, 29 | Failed 30 | } 31 | -------------------------------------------------------------------------------- /tests/CleanAspire.Tests/WebTests.cs: -------------------------------------------------------------------------------- 1 | namespace CleanAspire.Tests; 2 | 3 | public class WebTests 4 | { 5 | [Test] 6 | public async Task GetWebResourceRootReturnsOkStatusCode() 7 | { 8 | // Arrange 9 | var appHost = await DistributedApplicationTestingBuilder.CreateAsync(); 10 | appHost.Services.ConfigureHttpClientDefaults(clientBuilder => 11 | { 12 | clientBuilder.AddStandardResilienceHandler(); 13 | }); 14 | 15 | await using var app = await appHost.BuildAsync(); 16 | var resourceNotificationService = app.Services.GetRequiredService(); 17 | await app.StartAsync(); 18 | 19 | // Act 20 | var httpClient = app.CreateHttpClient("blazorweb"); 21 | await resourceNotificationService.WaitForResourceAsync("blazorweb", KnownResourceStates.Running).WaitAsync(TimeSpan.FromSeconds(30)); 22 | var response = await httpClient.GetAsync("/"); 23 | 24 | // Assert 25 | Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK)); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/CleanAspire.ClientApp/wwwroot/js/carousel.js: -------------------------------------------------------------------------------- 1 | export function initializeSwiper(selector, direction) { 2 | 3 | 4 | const link = document.createElement('link'); 5 | link.rel = 'stylesheet'; 6 | link.href = 'https://cdn.jsdelivr.net/npm/swiper@11.1.5/swiper-bundle.min.css'; 7 | document.head.appendChild(link); 8 | 9 | const script = document.createElement('script'); 10 | script.src = 'https://cdn.jsdelivr.net/npm/swiper@11.1.5/swiper-bundle.min.js'; 11 | script.onload = () => { 12 | var swiper = new Swiper(selector, { 13 | slidesPerView: 3, 14 | spaceBetween: 30, 15 | loop: true, 16 | centeredSlides: true, 17 | speed: 4000, // 动画速度 18 | autoplay: { 19 | delay: 0, // 自动滚动的时间间隔 20 | disableOnInteraction: false, // 用户操作后是否禁用自动滚动 21 | pauseOnMouseEnter: true, 22 | reverseDirection: direction??false 23 | } 24 | }); 25 | 26 | }; 27 | document.body.appendChild(script); 28 | 29 | console.log("initializeSwiper") 30 | }; 31 | 32 | -------------------------------------------------------------------------------- /src/CleanAspire.Application/Features/Stocks/Validators/StockReceivingCommandValidator.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for more information. 4 | 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | using System.Text; 9 | using System.Threading.Tasks; 10 | using CleanAspire.Application.Features.Stocks.Commands; 11 | 12 | namespace CleanAspire.Application.Features.Stocks.Validators; 13 | public class StockReceivingCommandValidator : AbstractValidator 14 | { 15 | public StockReceivingCommandValidator() 16 | { 17 | RuleFor(x => x.ProductId) 18 | .NotEmpty() 19 | .WithMessage("ProductId is required."); 20 | 21 | RuleFor(x => x.Quantity) 22 | .GreaterThan(0) 23 | .WithMessage("Quantity must be greater than 0."); 24 | 25 | RuleFor(x => x.Location) 26 | .NotEmpty() 27 | .WithMessage("Location is required."); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/CleanAspire.ClientApp/Services/Navigation/MenuItem.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for more information. 4 | 5 | using System.ComponentModel; 6 | 7 | namespace CleanAspire.ClientApp.Services.Navigation; 8 | 9 | public class MenuItem 10 | { 11 | public string Label { get; set; }=string.Empty; 12 | public string? Description { get; set; } 13 | public string? Href { get; set; } 14 | public string? StartIcon { get; set; } 15 | public string? EndIcon { get; set; } 16 | public List SubItems { get; set; } = new List(); 17 | public PageStatus Status { get; set; } = PageStatus.Completed; // Default to Completed 18 | public bool SpecialMenu => SubItems.Any() && SubItems.First().IsParent; 19 | public bool IsParent => SubItems.Any(); 20 | } 21 | 22 | 23 | public enum PageStatus 24 | { 25 | [Description("Coming Soon")] ComingSoon, 26 | [Description("New")] New, 27 | [Description("Completed")] Completed 28 | } 29 | -------------------------------------------------------------------------------- /src/CleanAspire.Application/Features/Stocks/Validators/StockDispatchingCommandValidator.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for more information. 4 | 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | using System.Text; 9 | using System.Threading.Tasks; 10 | using CleanAspire.Application.Features.Stocks.Commands; 11 | 12 | namespace CleanAspire.Application.Features.Stocks.Validators; 13 | public class StockDispatchingCommandValidator : AbstractValidator 14 | { 15 | public StockDispatchingCommandValidator() 16 | { 17 | RuleFor(x => x.ProductId) 18 | .NotEmpty() 19 | .WithMessage("ProductId is required."); 20 | 21 | RuleFor(x => x.Quantity) 22 | .GreaterThan(0) 23 | .WithMessage("Quantity must be greater than 0."); 24 | 25 | RuleFor(x => x.Location) 26 | .NotEmpty() 27 | .WithMessage("Location is required."); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /.github/workflows/dotnet.yml: -------------------------------------------------------------------------------- 1 | name: .NET 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | paths-ignore: 7 | - "deploy/**" 8 | pull_request: 9 | branches: [ "main" ] 10 | paths-ignore: 11 | - "deploy/**" 12 | 13 | jobs: 14 | build: 15 | 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | # Install CA certificates to ensure SSL trust 20 | - name: Install CA Certificates 21 | run: sudo apt-get update && sudo apt-get install -y ca-certificates 22 | 23 | - uses: actions/checkout@v5 24 | - name: Setup .NET 25 | uses: actions/setup-dotnet@v5 26 | with: 27 | dotnet-version: 10.0.x 28 | 29 | # Trust the ASP.NET Core development certificate 30 | - name: Trust ASP.NET Core HTTPS Development Certificate 31 | run: dotnet dev-certs https --trust 32 | 33 | - name: Restore dependencies 34 | run: dotnet restore CleanAspire.sln 35 | - name: Build 36 | run: dotnet build CleanAspire.sln --no-restore 37 | - name: Run tests 38 | run: dotnet test ./tests/CleanAspire.Tests/CleanAspire.Tests.csproj --no-build --verbosity normal 39 | -------------------------------------------------------------------------------- /src/CleanAspire.Domain/Identities/ApplicationUser.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | 5 | 6 | using CleanAspire.Domain.Common; 7 | using Microsoft.AspNetCore.Identity; 8 | 9 | namespace CleanAspire.Domain.Identities; 10 | 11 | public class ApplicationUser : IdentityUser, IAuditableEntity 12 | { 13 | public string? Nickname { get; set; } 14 | public string? Provider { get; set; } = "Local"; 15 | public string? TenantId { get; set; } 16 | public string? AvatarUrl { get; set; } 17 | public string? RefreshToken { get; set; } 18 | public DateTime RefreshTokenExpiryTime { get; set; } 19 | 20 | public string? TimeZoneId { get; set; } 21 | public string? LanguageCode { get; set; } 22 | public string? SuperiorId { get; set; } = null; 23 | public ApplicationUser? Superior { get; set; } = null; 24 | public DateTime? Created { get; set; } 25 | public string? CreatedBy { get; set; } 26 | public DateTime? LastModified { get; set; } 27 | public string? LastModifiedBy { get; set; } 28 | } 29 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) [year] [fullname] 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 | -------------------------------------------------------------------------------- /src/CleanAspire.AppHost/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/launchsettings.json", 3 | "profiles": { 4 | "https": { 5 | "commandName": "Project", 6 | "dotnetRunMessages": true, 7 | "launchBrowser": true, 8 | "applicationUrl": "https://localhost:17031;http://localhost:15241", 9 | "environmentVariables": { 10 | "ASPNETCORE_ENVIRONMENT": "Development", 11 | "DOTNET_ENVIRONMENT": "Development", 12 | "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21159", 13 | "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22043" 14 | } 15 | }, 16 | "http": { 17 | "commandName": "Project", 18 | "dotnetRunMessages": true, 19 | "launchBrowser": true, 20 | "applicationUrl": "http://localhost:15241", 21 | "environmentVariables": { 22 | "ASPNETCORE_ENVIRONMENT": "Development", 23 | "DOTNET_ENVIRONMENT": "Development", 24 | "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19215", 25 | "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20032" 26 | } 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/CleanAspire.Application/Features/Products/DTOs/ProductDto.cs: -------------------------------------------------------------------------------- 1 | // Summary: 2 | // This file defines a data transfer object (DTO) for products and an enumeration for product categories. 3 | // The ProductDto class encapsulates product details for data transfer between application layers. 4 | // The ProductCategoryDto enum provides predefined categories for products. 5 | 6 | namespace CleanAspire.Application.Features.Products.DTOs; 7 | 8 | // A DTO representing a product, used to transfer data between application layers. 9 | // By default, field names match the corresponding entity fields. For enums or referenced entities, a Dto suffix is used. 10 | public class ProductDto 11 | { 12 | public string Id { get; set; } = string.Empty; 13 | public string SKU { get; set; } = string.Empty; 14 | public string Name { get; set; } = string.Empty; 15 | public ProductCategory Category { get; set; } 16 | public string? Description { get; set; } 17 | public decimal Price { get; set; } = 0; 18 | public string? Currency { get; set; } 19 | public string? UOM { get; set; } 20 | } 21 | 22 | // An enumeration representing possible product categories. 23 | 24 | -------------------------------------------------------------------------------- /src/CleanAspire.Application/DependencyInjection.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for more information. 4 | 5 | using System.Reflection; 6 | using CleanAspire.Application.Pipeline; 7 | using Microsoft.Extensions.DependencyInjection; 8 | 9 | namespace CleanAspire.Application; 10 | 11 | public static class DependencyInjection 12 | { 13 | public static IServiceCollection AddApplication(this IServiceCollection services) 14 | { 15 | services.AddValidatorsFromAssembly(Assembly.GetExecutingAssembly()); 16 | services.AddScoped(typeof(IPipelineBehavior<,>), typeof(FusionCacheBehaviour<,>)); 17 | services.AddScoped(typeof(IPipelineBehavior<,>), typeof(FusionCacheRefreshBehaviour<,>)); 18 | services.AddScoped(typeof(IPipelineBehavior<,>), typeof(MessageValidatorBehaviour<,>)); 19 | services.AddMediator(options=> 20 | { 21 | options.ServiceLifetime = ServiceLifetime.Scoped; 22 | }); 23 | 24 | 25 | return services; 26 | } 27 | } 28 | 29 | -------------------------------------------------------------------------------- /src/CleanAspire.ClientApp/Components/Autocompletes/PicklistAutocomplete.razor.cs: -------------------------------------------------------------------------------- 1 | 2 | using Microsoft.AspNetCore.Components; 3 | using MudBlazor; 4 | 5 | namespace CleanAspire.ClientApp.Components.Autocompletes; 6 | 7 | public class PicklistAutocomplete : MudAutocomplete 8 | { 9 | public PicklistAutocomplete() 10 | { 11 | MaxItems = 50; 12 | SearchFunc = SearchFunc_; 13 | Dense = true; 14 | ResetValueOnEmptyText = true; 15 | } 16 | 17 | [Parameter] 18 | public PicklistType Picklist { get; set; } 19 | 20 | public Dictionary Data => PicklistDataSource.Data; 21 | 22 | private Task> SearchFunc_(string? value, CancellationToken cancellation = default) 23 | { 24 | if (!Data.ContainsKey(Picklist)) 25 | return Task.FromResult(Enumerable.Empty()); 26 | 27 | var list = Data[Picklist]; 28 | 29 | if (string.IsNullOrEmpty(value)) 30 | return Task.FromResult(list.AsEnumerable()); 31 | 32 | return Task.FromResult(list.Where(x => 33 | x.Contains(value, StringComparison.InvariantCultureIgnoreCase))); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/CleanAspire.Infrastructure/Services/CurrentUserAccessor.cs: -------------------------------------------------------------------------------- 1 | 2 | using System.Security.Claims; 3 | using CleanArchitecture.Blazor.Infrastructure.Extensions; 4 | using CleanAspire.Application.Common.Services; 5 | 6 | namespace CleanAspire.Infrastructure.Services; 7 | 8 | /// 9 | /// Provides access to the current user's session information. 10 | /// 11 | public class CurrentUserAccessor : ICurrentUserAccessor 12 | { 13 | private readonly ICurrentUserContext _currentUserContext; 14 | 15 | /// 16 | /// Initializes a new instance of the class. 17 | /// 18 | /// The current user context. 19 | public CurrentUserAccessor(ICurrentUserContext currentUserContext) 20 | { 21 | _currentUserContext = currentUserContext; 22 | } 23 | 24 | /// 25 | /// Gets the session information of the current user. 26 | /// 27 | public ClaimsPrincipal? User => _currentUserContext.GetCurrentUser(); 28 | 29 | public string? UserId => User?.GetUserId(); 30 | 31 | public string? TenantId => User?.GetTenantId(); 32 | } 33 | 34 | -------------------------------------------------------------------------------- /src/CleanAspire.Infrastructure/Persistence/Extensions/ModelBuilderExtensions.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | using System.Linq.Expressions; 5 | using Microsoft.EntityFrameworkCore; 6 | using Microsoft.EntityFrameworkCore.Query; 7 | 8 | namespace CleanAspire.Infrastructure.Persistence.Extensions; 9 | 10 | public static class ModelBuilderExtensions 11 | { 12 | public static void ApplyGlobalFilters(this ModelBuilder modelBuilder, 13 | Expression> expression) 14 | { 15 | var entities = modelBuilder.Model 16 | .GetEntityTypes() 17 | .Where(e => e.ClrType.GetInterface(typeof(TInterface).Name) != null) 18 | .Select(e => e.ClrType); 19 | foreach (var entity in entities) 20 | { 21 | var newParam = Expression.Parameter(entity); 22 | var newBody = ReplacingExpressionVisitor.Replace(expression.Parameters.Single(), newParam, expression.Body); 23 | modelBuilder.Entity(entity).HasQueryFilter(Expression.Lambda(newBody, newParam)); 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /src/CleanAspire.Api/OpenApiTransformersExtensions.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for more information. 4 | 5 | using Bogus; 6 | using CleanAspire.Application.Features.Products.Commands; 7 | using CleanAspire.Application.Features.Stocks.Commands; 8 | using Microsoft.AspNetCore.Authorization; 9 | using Microsoft.AspNetCore.Identity; 10 | using Microsoft.AspNetCore.Identity.Data; 11 | using Microsoft.AspNetCore.OpenApi; 12 | 13 | namespace CleanAspire.Api; 14 | 15 | public static class OpenApiTransformersExtensions 16 | { 17 | public static OpenApiOptions UseCookieAuthentication(this OpenApiOptions options) 18 | { 19 | // Temporarily disabled OpenAPI security scheme wiring due to OpenAPI.NET v2 API changes. 20 | // Documentation generation will still work without explicit cookie auth scheme. 21 | return options; 22 | } 23 | // Examples transformer removed for .NET 10 RC1. If you need example payloads, reintroduce 24 | // a schema transformer targeting the updated OpenAPI model in Microsoft.AspNetCore.OpenApi. 25 | } 26 | -------------------------------------------------------------------------------- /src/CleanAspire.Domain/Entities/Stock.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for more information. 4 | 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | using System.Text; 9 | using System.Threading.Tasks; 10 | using CleanAspire.Domain.Common; 11 | 12 | namespace CleanAspire.Domain.Entities; 13 | 14 | /// 15 | /// Represents a stock entity. 16 | /// 17 | public class Stock : BaseAuditableEntity, IAuditTrial 18 | { 19 | /// 20 | /// Gets or sets the product ID. 21 | /// 22 | public string? ProductId { get; set; } 23 | 24 | /// 25 | /// Gets or sets the product associated with the stock. 26 | /// 27 | public Product? Product { get; set; } 28 | 29 | /// 30 | /// Gets or sets the quantity of the stock. 31 | /// 32 | public int Quantity { get; set; } 33 | 34 | /// 35 | /// Gets or sets the location of the stock. 36 | /// 37 | public string Location { get; set; } = string.Empty; 38 | } 39 | -------------------------------------------------------------------------------- /src/CleanAspire.Application/CleanAspire.Application.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net10.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | all 18 | runtime; build; native; contentfiles; analyzers; buildtransitive 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/CleanAspire.Application/Features/Tenants/Queries/GetTenantByIdQuery.cs: -------------------------------------------------------------------------------- 1 | using CleanAspire.Application.Features.Tenants.DTOs; 2 | 3 | namespace CleanAspire.Application.Features.Tenants.Queries; 4 | 5 | public record GetTenantByIdQuery(string Id) : IFusionCacheRequest 6 | { 7 | public string CacheKey => $"Tenant_{Id}"; 8 | public IEnumerable? Tags => new[] { "tenants" }; 9 | } 10 | 11 | public class GetTenantByIdQueryHandler : IRequestHandler 12 | { 13 | private readonly IApplicationDbContext _dbContext; 14 | 15 | public GetTenantByIdQueryHandler(IApplicationDbContext dbContext) 16 | { 17 | _dbContext = dbContext; 18 | } 19 | 20 | public async ValueTask Handle(GetTenantByIdQuery request, CancellationToken cancellationToken) 21 | { 22 | var tenant = await _dbContext.Tenants.FindAsync(new object[] { request.Id }, cancellationToken); 23 | if (tenant == null) 24 | { 25 | return null; // or throw an exception if preferred 26 | } 27 | return new TenantDto 28 | { 29 | Id = tenant.Id, 30 | Name = tenant.Name, 31 | Description = tenant.Description 32 | }; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/CleanAspire.Api/Endpoints/WebpushrEndpointRegistrar.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for more information. 4 | 5 | 6 | using CleanAspire.Api.Webpushr; 7 | using Microsoft.Extensions.Options; 8 | 9 | 10 | namespace CleanAspire.Api.Endpoints; 11 | 12 | public class WebpushrEndpointRegistrar : IEndpointRegistrar 13 | { 14 | public void RegisterRoutes(IEndpointRouteBuilder routes) 15 | { 16 | var webpushrOptions = routes.ServiceProvider.GetRequiredService>().Value; 17 | var group = routes.MapGroup("/webpushr").WithTags("webpushr").AllowAnonymous(); 18 | 19 | group.MapGet("/config", () => 20 | { 21 | return TypedResults.Ok(webpushrOptions); 22 | }) 23 | .Produces(StatusCodes.Status200OK) 24 | .WithSummary("Retrieve current Webpushr configuration") 25 | .WithDescription("Returns the Webpushr configuration details currently loaded from the application's configuration system. This information includes keys and tokens used for Webpushr push notifications."); 26 | } 27 | } 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/CleanAspire.ServiceDefaults/CleanAspire.ServiceDefaults.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net10.0 5 | enable 6 | enable 7 | true 8 | CleanAspire.ServiceDefaults 9 | CleanAspire.ServiceDefaults 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/CleanAspire.ClientApp/Services/Identity/CookieHandler.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for more information. 4 | 5 | using Microsoft.AspNetCore.Components.WebAssembly.Http; 6 | 7 | namespace CleanAspire.ClientApp.Services.Identity; 8 | 9 | /// 10 | /// Handler to ensure cookie credentials are automatically sent over with each request. 11 | /// 12 | public class CookieHandler : DelegatingHandler 13 | { 14 | /// 15 | /// Main method to override for the handler. 16 | /// 17 | /// The original request. 18 | /// The token to handle cancellations. 19 | /// The . 20 | protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) 21 | { 22 | // include cookies! 23 | request.SetBrowserRequestCredentials(BrowserRequestCredentials.Include); 24 | request.Headers.Add("X-Requested-With", ["XMLHttpRequest"]); 25 | 26 | return base.SendAsync(request, cancellationToken); 27 | } 28 | } 29 | 30 | -------------------------------------------------------------------------------- /CleanAspire.slnx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/CleanAspire.Application/Common/Interfaces/IApplicationDbContext.cs: -------------------------------------------------------------------------------- 1 | namespace CleanAspire.Application.Common.Interfaces; 2 | 3 | /// 4 | /// Represents the application database context interface. 5 | /// 6 | public interface IApplicationDbContext 7 | { 8 | /// 9 | /// Gets or sets the Products DbSet. 10 | /// 11 | DbSet Products { get; set; } 12 | 13 | /// 14 | /// Gets or sets the AuditTrails DbSet. 15 | /// 16 | DbSet AuditTrails { get; set; } 17 | 18 | /// 19 | /// Gets or sets the Tenants DbSet. 20 | /// 21 | DbSet Tenants { get; set; } 22 | 23 | /// 24 | /// Gets or sets the Stocks DbSet. 25 | /// 26 | DbSet Stocks { get; set; } 27 | 28 | /// 29 | /// Saves all changes made in this context to the database. 30 | /// 31 | /// A CancellationToken to observe while waiting for the task to complete. 32 | /// A task that represents the asynchronous save operation. The task result contains the number of state entries written to the database. 33 | Task SaveChangesAsync(CancellationToken cancellationToken = default); 34 | } 35 | 36 | -------------------------------------------------------------------------------- /src/CleanAspire.Domain/Entities/AuditTrail.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | 5 | using CleanAspire.Domain.Common; 6 | using CleanAspire.Domain.Identities; 7 | using Microsoft.EntityFrameworkCore.ChangeTracking; 8 | 9 | namespace CleanAspire.Domain.Entities; 10 | 11 | public class AuditTrail : IEntity 12 | { 13 | public string Id { get; set; } 14 | public string? UserId { get; set; } 15 | public virtual ApplicationUser? Owner { get; set; } 16 | public AuditType AuditType { get; set; } 17 | public string? TableName { get; set; } 18 | public DateTime DateTime { get; set; } 19 | public Dictionary? OldValues { get; set; } 20 | public Dictionary? NewValues { get; set; } 21 | public List? AffectedColumns { get; set; } 22 | public Dictionary PrimaryKey { get; set; } = new(); 23 | public List TemporaryProperties { get; } = new(); 24 | public bool HasTemporaryProperties => TemporaryProperties.Any(); 25 | public string? DebugView { get; set; } 26 | public string? ErrorMessage { get; set; } 27 | } 28 | 29 | public enum AuditType 30 | { 31 | None, 32 | Create, 33 | Update, 34 | Delete 35 | } 36 | -------------------------------------------------------------------------------- /src/CleanAspire.ClientApp/Services/JsInterop/OnlineStatusInterop.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for more information. 4 | 5 | 6 | namespace CleanAspire.ClientApp.Services.JsInterop; 7 | 8 | using Microsoft.JSInterop; 9 | using System; 10 | using System.Threading.Tasks; 11 | 12 | public class OnlineStatusInterop(IJSRuntime jsRuntime) : IAsyncDisposable 13 | { 14 | private DotNetObjectReference? _dotNetRef; 15 | 16 | public event Action? OnlineStatusChanged; 17 | 18 | public void Initialize() 19 | { 20 | _dotNetRef = DotNetObjectReference.Create(this); 21 | jsRuntime.InvokeVoidAsync("onlineStatusInterop.addOnlineStatusListener", _dotNetRef); 22 | } 23 | 24 | public async Task GetOnlineStatusAsync() 25 | { 26 | return await jsRuntime.InvokeAsync("onlineStatusInterop.getOnlineStatus"); 27 | } 28 | 29 | [JSInvokable] 30 | public void UpdateOnlineStatus(bool isOnline) 31 | { 32 | OnlineStatusChanged?.Invoke(isOnline); 33 | } 34 | 35 | public ValueTask DisposeAsync() 36 | { 37 | _dotNetRef?.Dispose(); 38 | return ValueTask.CompletedTask; 39 | } 40 | } 41 | 42 | -------------------------------------------------------------------------------- /src/CleanAspire.Api/NumberSchemaTransformer.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for more information. 4 | 5 | using Microsoft.OpenApi; 6 | using System.Text.Json.Serialization.Metadata; 7 | using Microsoft.AspNetCore.OpenApi; 8 | 9 | namespace CleanAspire.Api; 10 | 11 | public sealed class NumberSchemaTransformer : IOpenApiSchemaTransformer 12 | { 13 | public Task TransformAsync(OpenApiSchema schema, OpenApiSchemaTransformerContext context, CancellationToken cancellationToken) 14 | { 15 | var clrType = context.JsonTypeInfo?.Type; 16 | 17 | if (clrType == typeof(decimal) || clrType == typeof(decimal?)) 18 | { 19 | schema.Type = JsonSchemaType.Number; 20 | schema.Format = "decimal"; 21 | } 22 | else if (clrType == typeof(int) || clrType == typeof(int?)) 23 | { 24 | schema.Type = JsonSchemaType.Integer; 25 | schema.Format = "int32"; 26 | } 27 | else if (clrType == typeof(long) || clrType == typeof(long?)) 28 | { 29 | schema.Type = JsonSchemaType.Integer; 30 | schema.Format = "int64"; 31 | } 32 | 33 | return Task.CompletedTask; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/CleanAspire.WebApp/Components/Pages/Error.razor: -------------------------------------------------------------------------------- 1 | @page "/Error" 2 | @using System.Diagnostics 3 | 4 | Error 5 | 6 |

Error.

7 |

An error occurred while processing your request.

8 | 9 | @if (ShowRequestId) 10 | { 11 |

12 | Request ID: @RequestId 13 |

14 | } 15 | 16 |

Development Mode

17 |

18 | Swapping to Development environment will display more detailed information about the error that occurred. 19 |

20 |

21 | The Development environment shouldn't be enabled for deployed applications. 22 | It can result in displaying sensitive information from exceptions to end users. 23 | For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development 24 | and restarting the app. 25 |

26 | 27 | @code{ 28 | [CascadingParameter] 29 | private HttpContext? HttpContext { get; set; } 30 | 31 | private string? RequestId { get; set; } 32 | private bool ShowRequestId => !string.IsNullOrEmpty(RequestId); 33 | 34 | protected override void OnInitialized() => 35 | RequestId = Activity.Current?.Id ?? HttpContext?.TraceIdentifier; 36 | } 37 | -------------------------------------------------------------------------------- /src/CleanAspire.Infrastructure/Services/CurrentUserContextSetter.cs: -------------------------------------------------------------------------------- 1 | 2 | using System.Security.Claims; 3 | using CleanAspire.Application.Common.Services; 4 | 5 | namespace CleanAspire.Infrastructure.Services; 6 | 7 | /// 8 | /// Service for setting and clearing the current user context. 9 | /// 10 | public class CurrentUserContextSetter : ICurrentUserContextSetter 11 | { 12 | private readonly ICurrentUserContext _currentUserContext; 13 | 14 | /// 15 | /// Initializes a new instance of the class. 16 | /// 17 | /// The current user context. 18 | public CurrentUserContextSetter(ICurrentUserContext currentUserContext) 19 | { 20 | _currentUserContext = currentUserContext; 21 | } 22 | 23 | /// 24 | /// Sets the current user context with the provided session information. 25 | /// 26 | /// The session information of the current user. 27 | public void SetCurrentUser(ClaimsPrincipal user) 28 | { 29 | _currentUserContext.Set(user); 30 | } 31 | 32 | /// 33 | /// Clears the current user context. 34 | /// 35 | public void Clear() 36 | { 37 | _currentUserContext.Clear(); 38 | } 39 | } 40 | 41 | -------------------------------------------------------------------------------- /src/CleanAspire.ClientApp/wwwroot/js/fancybox.js: -------------------------------------------------------------------------------- 1 | import {Fancybox} from "https://cdn.jsdelivr.net/npm/@fancyapps/ui@5.0.36/dist/fancybox/fancybox.esm.js"; 2 | 3 | export function filepreview(url, gallery) { 4 | console.log(url); 5 | if (url == null) return; 6 | const fileName = getFileName(url); 7 | if (isImageUrl(url)) { 8 | let images = []; 9 | if (gallery != null && gallery.length > 0) { 10 | images = gallery.filter(l => isImageUrl(l)).map(x => ({src: x, caption: x.split("/").pop()})); 11 | } else { 12 | images = [{src: url, caption: url.split("/").pop()}]; 13 | } 14 | const fancybox = new Fancybox(images); 15 | } else if (isPDF(url)) { 16 | const fancybox = new Fancybox([{ src: url, type: 'pdf', caption: url }]); 17 | } else { 18 | const anchorElement = document.createElement('a'); 19 | anchorElement.href = url; 20 | anchorElement.download = fileName ?? ''; 21 | anchorElement.click(); 22 | anchorElement.remove(); 23 | } 24 | } 25 | 26 | function isImageUrl(url) { 27 | const imageExtensions = /\.(gif|jpe?g|tiff?|png|webp|bmp)$/i; 28 | return imageExtensions.test(url); 29 | } 30 | function isPDF(url) { 31 | return url.toLowerCase().endsWith('.pdf'); 32 | } 33 | function getFileName(url) { 34 | return url.split('/').pop(); 35 | } -------------------------------------------------------------------------------- /src/CleanAspire.WebApp/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "http": { 4 | "commandName": "Project", 5 | "launchBrowser": true, 6 | "environmentVariables": { 7 | "ASPNETCORE_ENVIRONMENT": "Development" 8 | }, 9 | "dotnetRunMessages": true, 10 | "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", 11 | "applicationUrl": "http://localhost:5252" 12 | }, 13 | "https": { 14 | "commandName": "Project", 15 | "launchBrowser": true, 16 | "environmentVariables": { 17 | "ASPNETCORE_ENVIRONMENT": "Development" 18 | }, 19 | "dotnetRunMessages": true, 20 | "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", 21 | "applicationUrl": "https://localhost:7114;http://localhost:5252" 22 | }, 23 | "Container (Dockerfile)": { 24 | "commandName": "Docker", 25 | "launchBrowser": true, 26 | "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}", 27 | "environmentVariables": { 28 | "ASPNETCORE_HTTPS_PORTS": "8081", 29 | "ASPNETCORE_HTTP_PORTS": "8080" 30 | }, 31 | "publishAllPorts": true, 32 | "useSSL": true 33 | } 34 | }, 35 | "$schema": "https://json.schemastore.org/launchsettings.json" 36 | } -------------------------------------------------------------------------------- /src/CleanAspire.Infrastructure/Persistence/Configurations/IdentityUserConfiguration.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | 5 | using CleanAspire.Domain.Identities; 6 | using Microsoft.EntityFrameworkCore; 7 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 8 | 9 | namespace CleanAspire.Infrastructure.Persistence.Configurations; 10 | 11 | public class ApplicationUserConfiguration : IEntityTypeConfiguration 12 | { 13 | public void Configure(EntityTypeBuilder builder) 14 | { 15 | // Each User can have many UserLogins 16 | builder.HasOne(x => x.Superior).WithMany().HasForeignKey(u => u.SuperiorId); 17 | builder.Property(x => x.Nickname).HasMaxLength(50); 18 | builder.Property(x => x.Provider).HasMaxLength(50); 19 | builder.Property(x => x.TenantId).HasMaxLength(50); 20 | builder.Property(x => x.AvatarUrl).HasMaxLength(255); 21 | builder.Property(x => x.RefreshToken).HasMaxLength(255); 22 | builder.Property(x => x.LanguageCode).HasMaxLength(255); 23 | builder.Property(x => x.TimeZoneId).HasMaxLength(255); 24 | builder.Property(x => x.CreatedBy).HasMaxLength(50); 25 | builder.Property(x => x.LastModifiedBy).HasMaxLength(50); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/CleanAspire.ClientApp/wwwroot/syncing.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/CleanAspire.Application/Features/Tenants/Queries/GetAllTenantsQuery.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for more information. 4 | 5 | using CleanAspire.Application.Features.Tenants.DTOs; 6 | namespace CleanAspire.Application.Features.Tenants.Queries; 7 | 8 | public record GetAllTenantsQuery() : IFusionCacheRequest> 9 | { 10 | public string CacheKey => "GetAllTenants"; 11 | public IEnumerable? Tags => new[] { "tenants" }; 12 | } 13 | 14 | public class GetAllTenantsQueryHandler : IRequestHandler> 15 | { 16 | private readonly IApplicationDbContext _dbContext; 17 | 18 | public GetAllTenantsQueryHandler(IApplicationDbContext dbContext) 19 | { 20 | _dbContext = dbContext; 21 | } 22 | 23 | public async ValueTask> Handle(GetAllTenantsQuery request, CancellationToken cancellationToken) 24 | { 25 | var tenants = await _dbContext.Tenants.OrderBy(x=>x.Name) 26 | .Select(t => new TenantDto 27 | { 28 | Id = t.Id, 29 | Name = t.Name, 30 | Description = t.Description 31 | }) 32 | .ToListAsync(cancellationToken); 33 | 34 | return tenants; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/CleanAspire.ClientApp/Components/Autocompletes/TimeZoneAutocomplete.cs: -------------------------------------------------------------------------------- 1 | using MudBlazor; 2 | 3 | namespace CleanAspire.ClientApp.Components.Autocompletes; 4 | 5 | public class TimeZoneAutocomplete : MudAutocomplete 6 | { 7 | public TimeZoneAutocomplete() 8 | { 9 | SearchFunc = SearchFunc_; 10 | Dense = true; 11 | ResetValueOnEmptyText = true; 12 | ToStringFunc = x => 13 | { 14 | var timeZone = TimeZones.FirstOrDefault(tz => tz.Id.Equals(x)); 15 | return timeZone != null ? timeZone.DisplayName : x; 16 | }; 17 | } 18 | 19 | private List TimeZones { get; set; } = TimeZoneInfo.GetSystemTimeZones().ToList(); 20 | 21 | private Task> SearchFunc_(string value, CancellationToken cancellation = default) 22 | { 23 | return string.IsNullOrEmpty(value) 24 | ? Task.FromResult(TimeZones.Select(tz => tz.Id).AsEnumerable()) 25 | : Task.FromResult(TimeZones 26 | .Where(tz => Contains(tz, value)) 27 | .Select(tz => tz.Id)); 28 | } 29 | 30 | private static bool Contains(TimeZoneInfo timeZone, string value) 31 | { 32 | return timeZone.DisplayName.Contains(value, StringComparison.InvariantCultureIgnoreCase) || 33 | timeZone.Id.Contains(value, StringComparison.InvariantCultureIgnoreCase); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/CleanAspire.ClientApp/Components/Thumbnail.razor: -------------------------------------------------------------------------------- 1 | 2 |
3 | @if (IsImage) 4 | { 5 | 6 | }else if (IsPdf) 7 | { 8 | 9 | } 10 | else 11 | { 12 | 13 | } 14 | @FileName() 15 |
16 | 17 | @code { 18 | [Parameter] 19 | public string? Path { get; set; } 20 | [Parameter] 21 | public string? FileUrl { get; set; } 22 | 23 | private string Url() 24 | { 25 | var url = FileUrl?.Replace("\\", "/")??string.Empty; 26 | return url; 27 | } 28 | private string FileName() 29 | { 30 | var str = Path?.Split('\\').Last() ?? string.Empty; 31 | return str; 32 | } 33 | private string GetFileExtension() 34 | { 35 | return Path?.Split('.').Last()??string.Empty; 36 | } 37 | private bool IsPdf => GetFileExtension().ToLower() == "pdf"; 38 | 39 | private bool IsImage => GetFileExtension().ToLower() == "jpg" || GetFileExtension().ToLower() == "jpeg" || GetFileExtension().ToLower() == "png" || GetFileExtension().ToLower() == "gif" || GetFileExtension().ToLower() == "svg"; 40 | 41 | 42 | 43 | } 44 | -------------------------------------------------------------------------------- /src/CleanAspire.ClientApp/Services/Interfaces/IStorageService.cs: -------------------------------------------------------------------------------- 1 | namespace CleanAspire.ClientApp.Services.Interfaces; 2 | 3 | 4 | public interface IStorageService 5 | { 6 | /// 7 | /// Retrieves an item from the storage asynchronously. 8 | /// 9 | /// The type of the item to retrieve. 10 | /// The key of the item to retrieve. 11 | /// A representing the asynchronous operation. The task result contains the retrieved item, or null if the item does not exist. 12 | ValueTask GetItemAsync(string key); 13 | 14 | /// 15 | /// Removes an item from the storage asynchronously. 16 | /// 17 | /// The key of the item to remove. 18 | /// A representing the asynchronous operation. 19 | ValueTask RemoveItemAsync(string key); 20 | 21 | /// 22 | /// Sets an item in the storage asynchronously. 23 | /// 24 | /// The type of the item to set. 25 | /// The key of the item to set. 26 | /// The value of the item to set. 27 | /// A representing the asynchronous operation. 28 | ValueTask SetItemAsync(string key, T value); 29 | } 30 | 31 | -------------------------------------------------------------------------------- /src/CleanAspire.ClientApp/Client/Models/ProductCategory.cs: -------------------------------------------------------------------------------- 1 | // 2 | using System.Runtime.Serialization; 3 | using System; 4 | namespace CleanAspire.Api.Client.Models 5 | { 6 | [global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")] 7 | #pragma warning disable CS1591 8 | public enum ProductCategory 9 | #pragma warning restore CS1591 10 | { 11 | [EnumMember(Value = "Electronics")] 12 | #pragma warning disable CS1591 13 | Electronics, 14 | #pragma warning restore CS1591 15 | [EnumMember(Value = "Furniture")] 16 | #pragma warning disable CS1591 17 | Furniture, 18 | #pragma warning restore CS1591 19 | [EnumMember(Value = "Clothing")] 20 | #pragma warning disable CS1591 21 | Clothing, 22 | #pragma warning restore CS1591 23 | [EnumMember(Value = "Food")] 24 | #pragma warning disable CS1591 25 | Food, 26 | #pragma warning restore CS1591 27 | [EnumMember(Value = "Beverages")] 28 | #pragma warning disable CS1591 29 | Beverages, 30 | #pragma warning restore CS1591 31 | [EnumMember(Value = "HealthCare")] 32 | #pragma warning disable CS1591 33 | HealthCare, 34 | #pragma warning restore CS1591 35 | [EnumMember(Value = "Sports")] 36 | #pragma warning disable CS1591 37 | Sports, 38 | #pragma warning restore CS1591 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/CleanAspire.ClientApp/Services/PushNotifications/WebpushrAuthHandler.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for more information. 4 | 5 | using CleanAspire.Api.Client; 6 | namespace CleanAspire.ClientApp.Services.PushNotifications; 7 | 8 | public class WebpushrAuthHandler : DelegatingHandler 9 | { 10 | private readonly ApiClient _apiClient; 11 | private readonly WebpushrOptionsCache _optionsCache; 12 | public WebpushrAuthHandler(ApiClient apiClient, WebpushrOptionsCache optionsCache) 13 | { 14 | _apiClient = apiClient; 15 | _optionsCache = optionsCache; 16 | } 17 | protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) 18 | { 19 | var webpushrOptions = await _optionsCache.GetOptionsAsync(() => _apiClient.Webpushr.Config.GetAsync()); 20 | if (webpushrOptions == null) 21 | throw new InvalidOperationException("Failed to retrieve Webpushr options."); 22 | 23 | request.Headers.Clear(); 24 | request.Headers.Add("webpushrKey", webpushrOptions.ApiKey); 25 | request.Headers.Add("webpushrAuthToken", webpushrOptions.Token); 26 | request.Headers.Add("Accept", "application/json"); 27 | return await base.SendAsync(request, cancellationToken); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/CleanAspire.Api/Identity/EmailSender.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for more information. 4 | 5 | 6 | using Microsoft.AspNetCore.Identity.UI.Services; 7 | using StrongGrid; 8 | 9 | 10 | namespace CleanAspire.Api.Identity; 11 | 12 | public class EmailSender : IEmailSender 13 | { 14 | 15 | private readonly string _sendGridApiKey; 16 | private readonly string _defaultFromEmail; 17 | public EmailSender(IConfiguration configuration) 18 | { 19 | _sendGridApiKey = configuration["SendGrid:ApiKey"] ?? ""; 20 | _defaultFromEmail = configuration["SendGrid:DefaultFromEmail"] ?? "noreply@blazorserver.com"; 21 | } 22 | public async Task SendEmailAsync(string email, string subject, string htmlMessage) 23 | { 24 | if (string.IsNullOrEmpty(_sendGridApiKey)) 25 | { 26 | throw new InvalidOperationException("SendGrid API Key is not configured."); 27 | } 28 | 29 | var client = new Client(_sendGridApiKey); 30 | var from = new StrongGrid.Models.MailAddress(_defaultFromEmail, "noreply"); 31 | var to = new StrongGrid.Models.MailAddress(email, email); 32 | var messageId = await client.Mail.SendToSingleRecipientAsync(to: to, from: from, subject: subject, htmlContent: htmlMessage, textContent: htmlMessage); 33 | } 34 | } 35 | 36 | 37 | -------------------------------------------------------------------------------- /src/CleanAspire.Application/Features/Tenants/Commands/DeleteTenantCommand.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for more information. 4 | 5 | namespace CleanAspire.Application.Features.Tenants.Commands; 6 | 7 | public record DeleteTenantCommand(params IEnumerable Ids) : IFusionCacheRefreshRequest 8 | { 9 | public IEnumerable? Tags => new[] { "tenants" }; 10 | } 11 | 12 | 13 | public class DeleteTenantCommandHandler : IRequestHandler 14 | { 15 | private readonly ILogger _logger; 16 | private readonly IApplicationDbContext _dbContext; 17 | 18 | public DeleteTenantCommandHandler(ILogger logger, IApplicationDbContext dbContext) 19 | { 20 | _logger = logger; 21 | _dbContext = dbContext; 22 | } 23 | 24 | public async ValueTask Handle(DeleteTenantCommand request, CancellationToken cancellationToken) 25 | { 26 | // Logic to delete tenants from the database 27 | _dbContext.Tenants.RemoveRange(_dbContext.Tenants.Where(t => request.Ids.Contains(t.Id))); 28 | await _dbContext.SaveChangesAsync(cancellationToken); 29 | _logger.LogInformation("Tenants with Ids {TenantIds} are deleted", string.Join(", ", request.Ids)); 30 | return Unit.Value; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/CleanAspire.Application/Common/Interfaces/IUploadService.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | using System.ComponentModel; 5 | using SixLabors.ImageSharp.Processing; 6 | 7 | namespace CleanAspire.Application.Common.Interfaces; 8 | 9 | public interface IUploadService 10 | { 11 | Task UploadAsync(UploadRequest request); 12 | Task RemoveAsync(string filename); 13 | } 14 | public class UploadRequest 15 | { 16 | public UploadRequest(string fileName, UploadType uploadType, byte[] data, bool overwrite = false, string? folder = null, ResizeOptions? resizeOptions = null) 17 | { 18 | FileName = fileName; 19 | UploadType = uploadType; 20 | Data = data; 21 | Overwrite = overwrite; 22 | Folder = folder; 23 | ResizeOptions = resizeOptions; 24 | } 25 | public string FileName { get; set; } 26 | public string? Extension { get; set; } 27 | public UploadType UploadType { get; set; } 28 | public bool Overwrite { get; set; } 29 | public byte[] Data { get; set; } 30 | public string? Folder { get; set; } 31 | public ResizeOptions? ResizeOptions { get; set; } 32 | } 33 | public enum UploadType : byte 34 | { 35 | [Description(@"Products")] Product, 36 | [Description(@"Images")] Images, 37 | [Description(@"ProfilePictures")] ProfilePicture, 38 | [Description(@"Documents")] Document 39 | } 40 | -------------------------------------------------------------------------------- /src/CleanAspire.ClientApp/Services/OfflineModeState.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for more information. 4 | 5 | using CleanAspire.ClientApp.Services.Interfaces; 6 | 7 | namespace CleanAspire.ClientApp.Services; 8 | 9 | public class OfflineModeState 10 | { 11 | private const string OfflineModeKey = "_offlineMode"; 12 | private readonly IStorageService _storageService; 13 | public bool Enabled { get; private set; } 14 | 15 | public event Func? OnChange; 16 | public OfflineModeState(IStorageService storageService) 17 | { 18 | _storageService = storageService; 19 | // Initialize the OfflineModeEnabled with a default value 20 | Enabled = true; 21 | } 22 | // Initialize the offline mode setting from localStorage 23 | public async Task InitializeAsync() 24 | { 25 | var storedValue = await _storageService.GetItemAsync(OfflineModeKey); 26 | Enabled = storedValue ?? true; 27 | } 28 | // Update the OfflineModeEnabled and persist it to localStorage 29 | public async Task SetOfflineModeAsync(bool isEnabled) 30 | { 31 | Enabled = isEnabled; 32 | await _storageService.SetItemAsync(OfflineModeKey, isEnabled); 33 | NotifyStateChanged(); 34 | } 35 | 36 | private void NotifyStateChanged() => OnChange?.Invoke(); 37 | } 38 | -------------------------------------------------------------------------------- /tests/CleanAspire.Tests/CleanAspire.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net10.0 5 | enable 6 | enable 7 | false 8 | true 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | all 18 | runtime; build; native; contentfiles; analyzers; buildtransitive 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /src/CleanAspire.Infrastructure/Persistence/Conversions/ValueConversionExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using Microsoft.EntityFrameworkCore.ChangeTracking; 3 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 4 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 5 | 6 | namespace CleanAspire.Infrastructure.Persistence.Conversions; 7 | #nullable disable warnings 8 | public static class ValueConversionExtensions 9 | { 10 | private readonly static JsonSerializerOptions options = new JsonSerializerOptions 11 | { 12 | PropertyNameCaseInsensitive = true, 13 | PropertyNamingPolicy = JsonNamingPolicy.CamelCase 14 | }; 15 | public static PropertyBuilder HasJsonConversion(this PropertyBuilder propertyBuilder) 16 | { 17 | var converter = new ValueConverter( 18 | v => JsonSerializer.Serialize(v, options), 19 | v => string.IsNullOrEmpty(v) ? default : JsonSerializer.Deserialize(v, options)); 20 | 21 | var comparer = new ValueComparer( 22 | (l, r) => JsonSerializer.Serialize(l, options) == JsonSerializer.Serialize(r, options), 23 | v => v == null ? 0 : JsonSerializer.Serialize(v, options).GetHashCode(), 24 | v => JsonSerializer.Deserialize(JsonSerializer.Serialize(v, options), options)); 25 | 26 | propertyBuilder.HasConversion(converter); 27 | propertyBuilder.Metadata.SetValueComparer(comparer); 28 | 29 | return propertyBuilder; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/CleanAspire.WebApp/Program.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for more information. 4 | 5 | using CleanAspire.ClientApp; 6 | 7 | 8 | var builder = WebApplication.CreateBuilder(args); 9 | builder.AddServiceDefaults(); 10 | 11 | // Add services to the container. 12 | builder.Services.AddRazorComponents() 13 | .AddInteractiveServerComponents() 14 | .AddInteractiveWebAssemblyComponents(); 15 | 16 | 17 | builder.Services.AddCoreServices(builder.Configuration); 18 | builder.Services.AddHttpClients(builder.Configuration); 19 | builder.Services.AddAuthenticationAndLocalization(builder.Configuration); 20 | 21 | var app = builder.Build(); 22 | 23 | // Configure the HTTP request pipeline. 24 | if (app.Environment.IsDevelopment()) 25 | { 26 | app.UseWebAssemblyDebugging(); 27 | } 28 | else 29 | { 30 | app.UseExceptionHandler("/Error", createScopeForErrors: true); 31 | // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. 32 | app.UseHsts(); 33 | } 34 | 35 | app.UseHttpsRedirection(); 36 | 37 | 38 | app.UseAntiforgery(); 39 | 40 | app.MapStaticAssets(); 41 | app.MapRazorComponents() 42 | .AddInteractiveServerRenderMode() 43 | .AddInteractiveWebAssemblyRenderMode() 44 | .AddAdditionalAssemblies(typeof(CleanAspire.ClientApp._Imports).Assembly); 45 | 46 | app.Run(); 47 | -------------------------------------------------------------------------------- /src/CleanAspire.Infrastructure/Persistence/Configurations/AuditTrailConfiguration.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | 5 | using CleanAspire.Domain.Entities; 6 | using CleanAspire.Infrastructure.Persistence.Conversions; 7 | using Microsoft.EntityFrameworkCore; 8 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 9 | 10 | namespace CleanAspire.Infrastructure.Persistence.Configurations; 11 | 12 | #nullable disable 13 | public class AuditTrailConfiguration : IEntityTypeConfiguration 14 | { 15 | public void Configure(EntityTypeBuilder builder) 16 | { 17 | builder.HasOne(x => x.Owner) 18 | .WithMany() 19 | .HasForeignKey(x => x.UserId) 20 | .OnDelete(DeleteBehavior.SetNull); 21 | builder.Navigation(e => e.Owner).AutoInclude(); 22 | builder.Property(t => t.AuditType) 23 | .HasConversion(); 24 | builder.Property(e => e.AffectedColumns).HasJsonConversion(); 25 | builder.Property(u => u.OldValues).HasJsonConversion(); 26 | builder.Property(u => u.NewValues).HasJsonConversion(); 27 | builder.Property(u => u.PrimaryKey).HasJsonConversion(); 28 | builder.Ignore(x => x.TemporaryProperties); 29 | builder.Ignore(x => x.HasTemporaryProperties); 30 | builder.Property(x => x.DebugView).HasMaxLength(int.MaxValue); 31 | builder.Property(x => x.ErrorMessage).HasMaxLength(int.MaxValue); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/CleanAspire.ClientApp/Components/Autocompletes/LanguageAutocomplete.cs: -------------------------------------------------------------------------------- 1 | using CleanAspire.ClientApp.Services; 2 | using MudBlazor; 3 | 4 | namespace CleanAspire.ClientApp.Components.Autocompletes; 5 | 6 | public class LanguageAutocomplete : MudAutocomplete 7 | { 8 | public LanguageAutocomplete() 9 | { 10 | SearchFunc = SearchFunc_; 11 | Dense = true; 12 | ResetValueOnEmptyText = true; 13 | ToStringFunc = x => 14 | { 15 | var language = Languages.FirstOrDefault(lang => lang.Code.Equals(x, StringComparison.OrdinalIgnoreCase)); 16 | return language != null ? $"{language.DisplayName}" : x; 17 | }; 18 | } 19 | 20 | private List Languages { get; set; } = SupportedLocalization.SupportedLanguages.ToList(); 21 | 22 | private Task> SearchFunc_(string value, CancellationToken cancellation = default) 23 | { 24 | // 如果输入为空,返回完整的语言列表;否则进行模糊搜索 25 | return string.IsNullOrEmpty(value) 26 | ? Task.FromResult(Languages.Select(lang => lang.Code).AsEnumerable()) 27 | : Task.FromResult(Languages 28 | .Where(lang => Contains(lang, value)) 29 | .Select(lang => lang.Code)); 30 | } 31 | 32 | private static bool Contains(LanguageCode language, string value) 33 | { 34 | return language.Code.Contains(value, StringComparison.InvariantCultureIgnoreCase) || 35 | language.DisplayName.Contains(value, StringComparison.InvariantCultureIgnoreCase); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/CleanAspire.ClientApp/Layout/Appbar.razor: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | @L[AppSettings.AppName] 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | @code { 28 | [Inject] 29 | public LayoutService LayoutService { get; set; } = default!; 30 | } 31 | -------------------------------------------------------------------------------- /src/CleanAspire.Application/Features/Tenants/Commands/CreateTenantCommand.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for more information. 4 | 5 | namespace CleanAspire.Application.Features.Tenants.Commands; 6 | 7 | public record CreateTenantCommand(string Name, string Description) : IFusionCacheRefreshRequest 8 | { 9 | public IEnumerable? Tags => new[] { "tenants" }; 10 | } 11 | 12 | public class CreateTenantCommandHandler : IRequestHandler 13 | { 14 | private readonly ILogger _logger; 15 | private readonly IApplicationDbContext _dbContext; 16 | 17 | public CreateTenantCommandHandler(ILogger logger, IApplicationDbContext dbContext) 18 | { 19 | _logger = logger; 20 | _dbContext = dbContext; 21 | } 22 | 23 | public async ValueTask Handle(CreateTenantCommand request, CancellationToken cancellationToken) 24 | { 25 | // Creating a new tenant instance with a unique Id 26 | var tenant = new Tenant 27 | { 28 | Name = request.Name, 29 | Description = request.Description 30 | }; 31 | // Logic to add tenant to the database 32 | _dbContext.Tenants.Add(tenant); 33 | await _dbContext.SaveChangesAsync(cancellationToken); 34 | _logger.LogInformation("Tenant {TenantId} is created", tenant.Id); 35 | return tenant.Id; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/CleanAspire.ClientApp/Services/SupportedLocalization.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | namespace CleanAspire.ClientApp.Services; 5 | 6 | public static class SupportedLocalization 7 | { 8 | public const string ResourcesPath = "Resources"; 9 | 10 | public static readonly LanguageCode[] SupportedLanguages = 11 | { 12 | new() 13 | { 14 | Code = "en-US", 15 | DisplayName = "English (United States)" 16 | }, 17 | new() 18 | { 19 | Code = "zh-CN", 20 | DisplayName = "中文(简体,中国)" 21 | }, 22 | new() 23 | { 24 | Code = "de-DE", 25 | DisplayName = "Deutsch (Deutschland)" 26 | }, 27 | new() 28 | { 29 | Code = "fr-FR", 30 | DisplayName = "français (France)" 31 | }, 32 | new() 33 | { 34 | Code = "ja-JP", 35 | DisplayName = "日本語 (日本)" 36 | }, 37 | new() 38 | { 39 | Code = "es-ES", 40 | DisplayName = "español (España)" 41 | }, 42 | new() 43 | { 44 | Code = "ko-KR", 45 | DisplayName = "한국어(대한민국)" 46 | }, 47 | new() 48 | { 49 | Code = "pt-BR", 50 | DisplayName = "português (Brasil)" 51 | } 52 | }; 53 | } 54 | 55 | public class LanguageCode 56 | { 57 | public string DisplayName { get; set; } = "en-US"; 58 | public string Code { get; set; } = "English"; 59 | } 60 | -------------------------------------------------------------------------------- /src/CleanAspire.ClientApp/_Imports.razor: -------------------------------------------------------------------------------- 1 | @using System.Net.Http 2 | @using System.Net.Http.Json 3 | @using System.Globalization 4 | @using CleanAspire.Api.Client 5 | @using CleanAspire.Api.Client.Models 6 | @using CleanAspire.ClientApp 7 | @using CleanAspire.ClientApp.Components 8 | @using CleanAspire.ClientApp.Configurations 9 | @using CleanAspire.ClientApp.Layout 10 | @using CleanAspire.ClientApp.Services 11 | @using CleanAspire.ClientApp.Services.Identity 12 | @using CleanAspire.ClientApp.Services.Interfaces 13 | @using CleanAspire.ClientApp.Services.JsInterop 14 | @using CleanAspire.ClientApp.Services.PushNotifications 15 | @using Microsoft.AspNetCore.Components.Authorization 16 | @using Microsoft.AspNetCore.Components.Forms 17 | @using Microsoft.AspNetCore.Components.Routing 18 | @using Microsoft.AspNetCore.Components.Web 19 | @using Microsoft.AspNetCore.Components.Web.Virtualization 20 | @using Microsoft.AspNetCore.Components.WebAssembly.Http 21 | @using Microsoft.Extensions.Localization 22 | @using Microsoft.JSInterop 23 | @using Microsoft.Kiota.Abstractions 24 | @using MudBlazor 25 | 26 | 27 | @inject IJSRuntime JS 28 | @inject NavigationManager Navigation 29 | @inject ClientAppSettings AppSettings 30 | @inject ISnackbar Snackbar 31 | @inject IDialogService DialogService 32 | @inject DialogServiceHelper DialogServiceHelper 33 | @inject IStringLocalizer L 34 | @inject ILogger Logger 35 | @inject ApiClient ApiClient 36 | @inject ApiClientServiceProxy ApiClientServiceProxy 37 | @inject UserProfileStore UserProfileStore 38 | @inject IWebpushrService WebpushrService 39 | @inject OnlineStatusInterop OnlineStatusInterop 40 | @inject OfflineModeState OfflineModeState -------------------------------------------------------------------------------- /src/CleanAspire.Application/Features/Products/EventHandlers/ProductCreatedEvent.cs: -------------------------------------------------------------------------------- 1 | namespace CleanAspire.Application.Features.Products.EventHandlers; 2 | /// 3 | /// Represents an event triggered when a product is created. 4 | /// Purpose: 5 | /// 1. To signal the creation of a product. 6 | /// 2. Used in the domain event notification mechanism to pass product details to subscribers. 7 | /// 8 | public class ProductCreatedEvent : DomainEvent 9 | { 10 | /// 11 | /// Constructor to initialize the event and pass the created product instance. 12 | /// 13 | /// The created product instance. 14 | public ProductCreatedEvent(Product item) 15 | { 16 | Item = item; // Assigns the provided product instance to the read-only property 17 | } 18 | 19 | /// 20 | /// Gets the product instance associated with the event. 21 | /// 22 | public Product Item { get; } 23 | } 24 | 25 | /* 26 | public class ProductCreatedEventHandler : INotificationHandler 27 | { 28 | private readonly ILogger _logger; 29 | 30 | public ProductCreatedEventHandler( 31 | ILogger logger 32 | ) 33 | { 34 | _logger = logger; 35 | } 36 | 37 | public async Task Handle(ProductCreatedEvent notification, CancellationToken cancellationToken) 38 | { 39 | _logger.LogInformation("Handled domain event '{EventType}' with notification: {@Notification} in {ElapsedMilliseconds} ms", notification.GetType().Name, notification, _timer.ElapsedMilliseconds); 40 | } 41 | } 42 | */ 43 | -------------------------------------------------------------------------------- /src/CleanAspire.ClientApp/wwwroot/green-point.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /src/CleanAspire.Application/Features/Products/EventHandlers/ProductDeletedEvent.cs: -------------------------------------------------------------------------------- 1 | namespace CleanAspire.Application.Features.Products.EventHandlers; 2 | /// 3 | /// Represents an event triggered when a product is deleted. 4 | /// Purpose: 5 | /// 1. To signal the deletion of a product. 6 | /// 2. Used in the domain event notification mechanism to inform subscribers about the deleted product. 7 | /// 8 | public class ProductDeletedEvent : DomainEvent 9 | { 10 | /// 11 | /// Constructor to initialize the event and pass the deleted product instance. 12 | /// 13 | /// The deleted product instance. 14 | public ProductDeletedEvent(Product item) 15 | { 16 | Item = item; // Assigns the provided product instance to the read-only property 17 | } 18 | 19 | /// 20 | /// Gets the product instance associated with the event. 21 | /// 22 | public Product Item { get; } 23 | } 24 | 25 | 26 | /* 27 | public class ProductDeletedEventHandler : INotificationHandler 28 | { 29 | private readonly ILogger _logger; 30 | 31 | public ProductDeletedEventHandler( 32 | ILogger logger 33 | ) 34 | { 35 | _logger = logger; 36 | } 37 | 38 | public async Task Handle(ProductDeletedEvent notification, CancellationToken cancellationToken) 39 | { 40 | _logger.LogInformation("Handled domain event '{EventType}' with notification: {@Notification} in {ElapsedMilliseconds} ms", notification.GetType().Name, notification, _timer.ElapsedMilliseconds); 41 | } 42 | } 43 | */ 44 | -------------------------------------------------------------------------------- /src/CleanAspire.Application/Features/Products/EventHandlers/ProductUpdatedEvent.cs: -------------------------------------------------------------------------------- 1 | namespace CleanAspire.Application.Features.Products.EventHandlers; 2 | /// 3 | /// Represents an event triggered when a product is updated. 4 | /// Purpose: 5 | /// 1. To signal that a product has been updated. 6 | /// 2. Used in the domain event notification mechanism to inform subscribers about the updated product details. 7 | /// 8 | public class ProductUpdatedEvent : DomainEvent 9 | { 10 | /// 11 | /// Constructor to initialize the event and pass the updated product instance. 12 | /// 13 | /// The updated product instance. 14 | public ProductUpdatedEvent(Product item) 15 | { 16 | Item = item; // Assigns the provided product instance to the read-only property 17 | } 18 | 19 | /// 20 | /// Gets the product instance associated with the event. 21 | /// 22 | public Product Item { get; } 23 | } 24 | /* 25 | public class ProductUpdatedEventHandler : INotificationHandler 26 | { 27 | private readonly ILogger _logger; 28 | 29 | public ProductUpdatedEventHandler( 30 | ILogger logger 31 | ) 32 | { 33 | _logger = logger; 34 | } 35 | 36 | public async Task Handle(ProductUpdatedEvent notification, CancellationToken cancellationToken) 37 | { 38 | _logger.LogInformation("Handled domain event '{EventType}' with notification: {@Notification} in {ElapsedMilliseconds} ms", notification.GetType().Name, notification, _timer.ElapsedMilliseconds); 39 | } 40 | } 41 | */ 42 | -------------------------------------------------------------------------------- /src/CleanAspire.Infrastructure/Configurations/DatabaseSettings.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace CleanAspire.Infrastructure.Configurations; 4 | 5 | /// 6 | /// Configuration wrapper for the database section 7 | /// 8 | public class DatabaseSettings : IValidatableObject 9 | { 10 | /// 11 | /// Database key constraint 12 | /// 13 | public const string Key = nameof(DatabaseSettings); 14 | 15 | /// 16 | /// Represents the database provider, which to connect to 17 | /// 18 | public string DBProvider { get; set; } = string.Empty; 19 | 20 | /// 21 | /// The connection string being used to connect with the given database provider 22 | /// 23 | public string ConnectionString { get; set; } = string.Empty; 24 | 25 | /// 26 | /// Validates the entered configuration 27 | /// 28 | /// Describes the context in which a validation check is performed. 29 | /// The result of the validation 30 | public IEnumerable Validate(ValidationContext validationContext) 31 | { 32 | if (string.IsNullOrEmpty(DBProvider)) 33 | yield return new ValidationResult( 34 | $"{nameof(DatabaseSettings)}.{nameof(DBProvider)} is not configured", 35 | new[] { nameof(DBProvider) }); 36 | 37 | if (string.IsNullOrEmpty(ConnectionString)) 38 | yield return new ValidationResult( 39 | $"{nameof(DatabaseSettings)}.{nameof(ConnectionString)} is not configured", 40 | new[] { nameof(ConnectionString) }); 41 | } 42 | } -------------------------------------------------------------------------------- /src/CleanAspire.ClientApp/Dockerfile: -------------------------------------------------------------------------------- 1 | # Stage 1: Build the Blazor Client Application 2 | FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build 3 | WORKDIR /src 4 | 5 | # Install Python for AOT compilation 6 | RUN apt-get update && apt-get install -y python3 python3-pip && ln -s /usr/bin/python3 /usr/bin/python 7 | 8 | # Copy the project files and restore dependencies 9 | COPY ["src/CleanAspire.ClientApp/CleanAspire.ClientApp.csproj", "src/CleanAspire.ClientApp/"] 10 | RUN dotnet restore "src/CleanAspire.ClientApp/CleanAspire.ClientApp.csproj" 11 | 12 | # Install wasm-tools for AOT 13 | RUN dotnet workload install wasm-tools --skip-manifest-update 14 | RUN dotnet workload update 15 | 16 | # Copy the entire source code and build the application in Release mode 17 | COPY . . 18 | RUN dotnet publish "src/CleanAspire.ClientApp/CleanAspire.ClientApp.csproj" -c Release -o /app/publish -p:DefineConstants=STANDALONE 19 | 20 | # Stage 2: Serve the Blazor Client Application using Nginx 21 | FROM nginx:alpine AS final 22 | WORKDIR /usr/share/nginx/html 23 | 24 | # Install OpenSSL to create a self-signed certificate 25 | RUN apk add --no-cache openssl && \ 26 | openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout /etc/ssl/private/nginx-selfsigned.key -out /etc/ssl/certs/nginx-selfsigned.crt -subj "/CN=localhost" 27 | 28 | # Clean the default nginx content 29 | RUN rm -rf ./* 30 | 31 | # Copy the build output from the previous stage 32 | COPY --from=build /app/publish/wwwroot . 33 | 34 | # Copy the generated self-signed certificate and configure Nginx for HTTPS 35 | COPY src/CleanAspire.ClientApp/nginx.conf /etc/nginx/nginx.conf 36 | 37 | # Expose port 80 for HTTP traffic and 443 for HTTPS traffic 38 | EXPOSE 80 39 | EXPOSE 443 40 | 41 | # Start Nginx 42 | CMD ["nginx", "-g", "daemon off;"] -------------------------------------------------------------------------------- /src/CleanAspire.Application/Features/Tenants/Commands/UpdateTenantCommand.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for more information. 4 | 5 | namespace CleanAspire.Application.Features.Tenants.Commands; 6 | 7 | public record UpdateTenantCommand(string Id, string Name, string Description) : IFusionCacheRefreshRequest 8 | { 9 | public IEnumerable? Tags => new[] { "tenants" }; 10 | } 11 | 12 | public class UpdateTenantCommandHandler : IRequestHandler 13 | { 14 | private readonly ILogger _logger; 15 | private readonly IApplicationDbContext _dbContext; 16 | 17 | public UpdateTenantCommandHandler(ILogger logger, IApplicationDbContext dbContext) 18 | { 19 | _logger = logger; 20 | _dbContext = dbContext; 21 | } 22 | 23 | public async ValueTask Handle(UpdateTenantCommand request, CancellationToken cancellationToken) 24 | { 25 | // Logic to update tenant in the database 26 | var tenant = await _dbContext.Tenants.FindAsync(new object[] { request.Id }, cancellationToken); 27 | if (tenant == null) 28 | { 29 | _logger.LogError($"Tenant with Id {request.Id} not found."); 30 | throw new Exception($"Tenant with Id {request.Id} not found."); 31 | } 32 | 33 | tenant.Name = request.Name; 34 | tenant.Description = request.Description; 35 | await _dbContext.SaveChangesAsync(cancellationToken); 36 | _logger.LogInformation($"Updated Tenant: {request.Id}, Name: {request.Name}"); 37 | return Unit.Value; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/CleanAspire.ClientApp/Components/PasswordInput.razor: -------------------------------------------------------------------------------- 1 | @using System.Linq.Expressions 2 | 3 | 16 | 17 | 18 | @code { 19 | [Parameter] public InputType InputType { get; set; } = InputType.Password; 20 | [Parameter] public string AdornmentIcon { get; set; } = Icons.Material.Filled.VisibilityOff; 21 | [Parameter] public string? Label { get; set; } 22 | [Parameter] public string? Placeholder { get; set; } 23 | [Parameter] public string? RequiredError { get; set; } 24 | [Parameter] public Expression>? Field { get; set; } 25 | [Parameter] public string Value { get; set; } = string.Empty; 26 | [Parameter] public EventCallback ValueChanged { get; set; } 27 | 28 | private bool isVisible; 29 | 30 | private async Task ToggleVisibility() 31 | { 32 | isVisible = !isVisible; 33 | AdornmentIcon = isVisible ? Icons.Material.Filled.Visibility : Icons.Material.Filled.VisibilityOff; 34 | InputType = isVisible ? InputType.Text : InputType.Password; 35 | if (ValueChanged.HasDelegate) 36 | { 37 | await ValueChanged.InvokeAsync(Value); 38 | } 39 | } 40 | 41 | private async Task OnValueChanged(string newValue) 42 | { 43 | Value = newValue; 44 | if (ValueChanged.HasDelegate) 45 | { 46 | await ValueChanged.InvokeAsync(newValue); 47 | } 48 | } 49 | } 50 | 51 | 52 | -------------------------------------------------------------------------------- /src/CleanAspire.ClientApp/wwwroot/red-point.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /src/CleanAspire.ClientApp/Components/LanguageSelector.razor: -------------------------------------------------------------------------------- 1 | @using System.Globalization 2 | @inject LanguageService LanguageService 3 | @inject IStorageService StorageService 4 | 5 | 6 | @foreach (var language in SupportedLocalization.SupportedLanguages) 7 | { 8 | if (language.Code == CurrentLanguage) 9 | { 10 | @language.DisplayName 11 | } 12 | else 13 | { 14 | @language.DisplayName 15 | } 16 | } 17 | 18 | 19 | 20 | @code 21 | { 22 | override protected void OnInitialized() 23 | { 24 | CurrentLanguage = CultureInfo.CurrentCulture.Name; 25 | LanguageService.OnLanguageChanged += UpdateLanguage; 26 | } 27 | public void Dispose() 28 | { 29 | LanguageService.OnLanguageChanged -= UpdateLanguage; 30 | } 31 | private void UpdateLanguage() 32 | { 33 | InvokeAsync(StateHasChanged); 34 | } 35 | public string? CurrentLanguage { get; set; } = "en-US"; 36 | private async Task ChangeLanguageAsync(string languageCode) 37 | { 38 | LanguageService.SetLanguage(languageCode); 39 | await StorageService.SetItemAsync("_Culture", languageCode); 40 | CurrentLanguage = languageCode; 41 | Navigation.NavigateTo(Navigation.BaseUri + "?culture=" + languageCode, true); 42 | } 43 | } -------------------------------------------------------------------------------- /src/CleanAspire.Application/Features/Stocks/DTOs/StockDto.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for more information. 4 | 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | using System.Text; 9 | using System.Threading.Tasks; 10 | using CleanAspire.Application.Features.Products.DTOs; 11 | using CleanAspire.Domain.Common; 12 | 13 | namespace CleanAspire.Application.Features.Stocks.DTOs; 14 | /// 15 | /// Data Transfer Object for Stock. 16 | /// 17 | public class StockDto 18 | { 19 | /// 20 | /// Gets or sets the unique identifier for the stock. 21 | /// 22 | public string Id { get; set; } = string.Empty; 23 | 24 | /// 25 | /// Gets or sets the unique identifier for the product. 26 | /// 27 | public string ProductId { get; set; } 28 | 29 | /// 30 | /// Gets or sets the product details. 31 | /// 32 | public ProductDto Product { get; set; } 33 | 34 | /// 35 | /// Gets or sets the quantity of the stock. 36 | /// 37 | public int Quantity { get; set; } = 0; 38 | 39 | /// 40 | /// Gets or sets the location of the stock. 41 | /// 42 | public string Location { get; set; } = string.Empty; 43 | 44 | /// 45 | /// Gets or sets the date and time when the stock was created. 46 | /// 47 | public DateTime? Created { get; set; } 48 | 49 | /// 50 | /// Gets or sets the date and time when the stock was last modified. 51 | /// 52 | public DateTime? LastModified { get; set; } 53 | } 54 | 55 | -------------------------------------------------------------------------------- /src/CleanAspire.Infrastructure/Persistence/Configurations/StockConfiguration.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | using System.Text.Json; 5 | using CleanAspire.Domain.Entities; 6 | using Microsoft.EntityFrameworkCore; 7 | using Microsoft.EntityFrameworkCore.ChangeTracking; 8 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 9 | 10 | namespace CleanAspire.Infrastructure.Persistence.Configurations; 11 | /// 12 | /// Configures the Stock entity. 13 | /// 14 | public class StockConfiguration : IEntityTypeConfiguration 15 | { 16 | /// 17 | /// Configures the properties and relationships of the Stock entity. 18 | /// 19 | /// The builder to be used to configure the Stock entity. 20 | public void Configure(EntityTypeBuilder builder) 21 | { 22 | /// 23 | /// Configures the ProductId property of the Stock entity. 24 | /// 25 | builder.Property(x => x.ProductId).HasMaxLength(50).IsRequired(); 26 | 27 | /// 28 | /// Configures the relationship between the Stock and Product entities. 29 | /// 30 | builder.HasOne(x => x.Product).WithMany().HasForeignKey(x => x.ProductId).OnDelete(DeleteBehavior.Cascade); 31 | 32 | /// 33 | /// Configures the Location property of the Stock entity. 34 | /// 35 | builder.Property(x => x.Location).HasMaxLength(12).IsRequired(); 36 | 37 | /// 38 | /// Ignores the DomainEvents property of the Stock entity. 39 | /// 40 | builder.Ignore(e => e.DomainEvents); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/CleanAspire.ClientApp/Pages/PrivacyPolicy.razor: -------------------------------------------------------------------------------- 1 | @page "/privacy-policy" 2 | 3 | @layout MainLayout 4 | 5 | Privacy Policy 6 | 7 | Effective Date: 2024 8 | 9 | 10 | Thank you for accessing our open-source project. This privacy policy applies to our demonstration application, which is intended solely for showcasing project functionality and demonstrating features. 11 | 12 | 13 | 14 | 1. Data Collection and Usage 15 | 16 | Our application does not collect or store any real user personal data. All data displayed (e.g., names, emails, transaction records) are dummy data generated solely for demonstration purposes. 17 | 18 | 19 | 2. Data Security 20 | 21 | As this application is for demonstration only, the displayed data is virtual and not stored or transmitted to any servers. 22 | 23 | 24 | 3. Open-Source Declaration 25 | 26 | This project is open-source, and its source code is publicly available. Anyone can review the code to confirm that no real data collection or privacy-infringing logic exists. 27 | 28 | 29 | 4. Contact 30 | 31 | If you have any questions or concerns, please contact us: neo.js.cn@gmail.com 32 | 33 | 34 | 35 | 36 | @code { 37 | 38 | } 39 | -------------------------------------------------------------------------------- /src/CleanAspire.Application/Features/Products/Commands/DeleteProductCommand.cs: -------------------------------------------------------------------------------- 1 | // Summary: 2 | // This file defines a command and its handler for deleting products from the database. 3 | // The DeleteProductCommand encapsulates the product IDs to be deleted, while the 4 | // DeleteProductCommandHandler processes the command, removes the corresponding products, 5 | // triggers domain events such as ProductDeletedEvent, and commits the changes. This ensures 6 | // a structured and efficient approach to handling product deletions. 7 | 8 | using CleanAspire.Application.Features.Products.EventHandlers; 9 | using CleanAspire.Application.Pipeline; 10 | 11 | namespace CleanAspire.Application.Features.Products.Commands; 12 | 13 | // Command object that encapsulates the IDs of products to be deleted. 14 | public record DeleteProductCommand(params IEnumerable Ids) 15 | : IFusionCacheRefreshRequest, 16 | IRequiresValidation 17 | { 18 | public IEnumerable? Tags => new[] { "products" }; 19 | } 20 | 21 | public class DeleteProductCommandHandler : IRequestHandler 22 | { 23 | private readonly IApplicationDbContext _dbContext; 24 | 25 | public DeleteProductCommandHandler(ILogger logger, IApplicationDbContext dbContext) 26 | { 27 | _dbContext = dbContext; 28 | } 29 | 30 | public async ValueTask Handle(DeleteProductCommand request, CancellationToken cancellationToken) 31 | { 32 | var products = _dbContext.Products.Where(p => request.Ids.Contains(p.Id)); 33 | 34 | foreach (var product in products) 35 | { 36 | product.AddDomainEvent(new ProductDeletedEvent(product)); 37 | _dbContext.Products.Remove(product); 38 | } 39 | 40 | await _dbContext.SaveChangesAsync(cancellationToken); 41 | 42 | return Unit.Value; 43 | } 44 | } 45 | 46 | -------------------------------------------------------------------------------- /src/CleanAspire.ClientApp/Components/Autocompletes/MultiTenantAutocomplete.cs: -------------------------------------------------------------------------------- 1 | 2 | 3 | using CleanAspire.Api.Client; 4 | using CleanAspire.Api.Client.Models; 5 | using CleanAspire.ClientApp.Services; 6 | using Microsoft.AspNetCore.Components; 7 | using MudBlazor; 8 | 9 | namespace CleanAspire.ClientApp.Components.Autocompletes; 10 | 11 | public class MultiTenantAutocomplete : MudAutocomplete 12 | { 13 | public MultiTenantAutocomplete() 14 | { 15 | SearchFunc = SearchKeyValues; 16 | ToStringFunc = dto => dto?.Name; 17 | Dense = true; 18 | ResetValueOnEmptyText = true; 19 | ShowProgressIndicator = true; 20 | } 21 | public List? Tenants { get; set; } = new(); 22 | [Inject] private ApiClient ApiClient { get; set; } = default!; 23 | [Inject] private ApiClientServiceProxy ApiClientServiceProxy { get; set; } = default!; 24 | 25 | protected override async Task OnAfterRenderAsync(bool firstRender) 26 | { 27 | if (firstRender) 28 | { 29 | Tenants = await ApiClientServiceProxy.QueryAsync("multitenant", () => ApiClient.Tenants.GetAsync(), tags: null, expiration: TimeSpan.FromMinutes(60)); 30 | StateHasChanged(); // Trigger a re-render after the tenants are loaded 31 | } 32 | } 33 | private async Task> SearchKeyValues(string? value, CancellationToken cancellation) 34 | { 35 | IEnumerable result; 36 | 37 | if (string.IsNullOrWhiteSpace(value)) 38 | result = Tenants ?? new List(); 39 | else 40 | result = Tenants? 41 | .Where(x => x.Name?.Contains(value, StringComparison.InvariantCultureIgnoreCase) == true || 42 | x.Description?.Contains(value, StringComparison.InvariantCultureIgnoreCase) == true) 43 | .ToList() ?? new List(); 44 | 45 | return await Task.FromResult(result); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /.github/workflows/docker.yml: -------------------------------------------------------------------------------- 1 | name: Build and Push Docker Image 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | paths-ignore: 7 | - "deploy/**" 8 | pull_request: 9 | branches: [ "main" ] 10 | paths-ignore: 11 | - "deploy/**" 12 | workflow_dispatch: 13 | 14 | jobs: 15 | build: 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - name: Checkout repository 20 | uses: actions/checkout@v4 21 | 22 | - name: Get next version 23 | uses: reecetech/version-increment@2024.10.1 24 | id: version 25 | with: 26 | scheme: semver 27 | increment: patch 28 | 29 | - run: git tag ${{ steps.version.outputs.version }} 30 | - run: git push origin ${{ steps.version.outputs.version }} 31 | 32 | - name: Log in to Docker Hub 33 | uses: docker/login-action@v3 34 | with: 35 | username: ${{ secrets.DOCKER_USERNAME }} 36 | password: ${{ secrets.DOCKER_PASSWORD }} 37 | 38 | - name: Build and push CleanAspire.Standalone image 39 | run: | 40 | docker build -t ${{ secrets.DOCKER_USERNAME }}/cleanaspire-standalone:${{ steps.version.outputs.version }} -f src/CleanAspire.ClientApp/Dockerfile . 41 | docker push ${{ secrets.DOCKER_USERNAME }}/cleanaspire-standalone:${{ steps.version.outputs.version }} 42 | - name: Build and push CleanAspire.WebApp image 43 | run: | 44 | docker build -t ${{ secrets.DOCKER_USERNAME }}/cleanaspire-webapp:${{ steps.version.outputs.version }} -f src/CleanAspire.WebApp/Dockerfile . 45 | docker push ${{ secrets.DOCKER_USERNAME }}/cleanaspire-webapp:${{ steps.version.outputs.version }} 46 | 47 | - name: Build and push CleanAspire.Api image 48 | run: | 49 | docker build -t ${{ secrets.DOCKER_USERNAME }}/cleanaspire-api:${{ steps.version.outputs.version }} -f src/CleanAspire.Api/Dockerfile . 50 | docker push ${{ secrets.DOCKER_USERNAME }}/cleanaspire-api:${{ steps.version.outputs.version }} 51 | 52 | -------------------------------------------------------------------------------- /src/CleanAspire.Infrastructure/Persistence/Configurations/ProductConfiguration.cs: -------------------------------------------------------------------------------- 1 | // This class configures the database schema for the Product entity. 2 | // It implements IEntityTypeConfiguration to define the entity's properties, constraints, and relationships at the database level. 3 | 4 | // Purpose: 5 | // 1. Map the Product entity to the database with specific configurations for its properties. 6 | // 2. Ensure the database enforces data integrity (e.g., unique constraints, required fields). 7 | // 3. Customize how certain properties are stored in the database (e.g., converting enums to strings). 8 | // 4. Exclude non-persistent properties (e.g., DomainEvents) from being mapped to the database. 9 | 10 | using CleanAspire.Domain.Entities; 11 | using Microsoft.EntityFrameworkCore; 12 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 13 | 14 | namespace CleanAspire.Infrastructure.Persistence.Configurations; 15 | /// 16 | /// Configures the Product entity. 17 | /// 18 | public class ProductConfiguration : IEntityTypeConfiguration 19 | { 20 | /// 21 | /// Configures the properties and relationships of the Product entity. 22 | /// 23 | /// The builder to be used to configure the Product entity. 24 | public void Configure(EntityTypeBuilder builder) 25 | { 26 | builder.Property(x => x.Id).HasMaxLength(50); 27 | // Configures the Category property to be stored as a string in the database. 28 | builder.Property(x => x.Category).HasConversion(); 29 | 30 | // Configures the Name property to have a unique index. 31 | builder.HasIndex(x => x.Name).IsUnique(); 32 | 33 | // Configures the Name property to have a maximum length of 80 characters and to be required. 34 | builder.Property(x => x.Name).HasMaxLength(80).IsRequired(); 35 | 36 | // Ignores the DomainEvents property so it is not mapped to the database. 37 | builder.Ignore(e => e.DomainEvents); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/CleanAspire.ClientApp/Services/LocalStorageService.cs: -------------------------------------------------------------------------------- 1 | using Blazored.LocalStorage; 2 | using CleanAspire.ClientApp.Services.Interfaces; 3 | 4 | namespace CleanAspire.ClientApp.Services; 5 | 6 | /// 7 | /// Service for interacting with local storage. 8 | /// 9 | public class LocalStorageService : IStorageService 10 | { 11 | private readonly ILocalStorageService _localStorageService; 12 | 13 | public LocalStorageService(ILocalStorageService localStorageService) 14 | { 15 | _localStorageService = localStorageService; 16 | } 17 | 18 | /// 19 | /// Retrieves an item from local storage asynchronously. 20 | /// 21 | /// The type of the item to retrieve. 22 | /// The key of the item to retrieve. 23 | /// A representing the asynchronous operation, containing the retrieved item. 24 | public ValueTask GetItemAsync(string key) 25 | { 26 | return _localStorageService.GetItemAsync(key); 27 | } 28 | 29 | /// 30 | /// Removes an item from local storage asynchronously. 31 | /// 32 | /// The key of the item to remove. 33 | /// A representing the asynchronous operation. 34 | public ValueTask RemoveItemAsync(string key) 35 | { 36 | return _localStorageService.RemoveItemAsync(key); 37 | } 38 | 39 | /// 40 | /// Sets an item in local storage asynchronously. 41 | /// 42 | /// The type of the item to set. 43 | /// The key of the item to set. 44 | /// The value of the item to set. 45 | /// A representing the asynchronous operation. 46 | public ValueTask SetItemAsync(string key, T value) 47 | { 48 | return _localStorageService.SetItemAsync(key, value); 49 | } 50 | } 51 | 52 | -------------------------------------------------------------------------------- /src/CleanAspire.ClientApp/wwwroot/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 18 | 19 | 20 | 22 | 23 | 24 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /src/CleanAspire.ClientApp/Pages/TermsOfService.razor: -------------------------------------------------------------------------------- 1 | @page "/terms-of-service" 2 | 3 | @layout MainLayout 4 | 5 | Terms of Service 6 | 7 | Effective Date: 2024 8 | 9 | 10 | Thank you for using the demonstration application of our open-source project. This application is for demonstration purposes only and does not provide actual services. 11 | 12 | 13 | 14 | 1. Service Description 15 | 16 | This application is designed to showcase project functionality. It uses dummy data (virtual data) for demonstration and testing purposes only. The application is not intended for production use. 17 | 18 | 19 | 2. Data Disclaimer 20 | 21 | All data (including user information, transaction records, etc.) within this application are dummy data and are purely for display and testing purposes. No real personal data is involved or stored. 22 | 23 | 24 | 3. Open-Source Code 25 | 26 | This project is open-source, and its source code is publicly available. The code is governed by the project's open-source license agreement. 27 | 28 | 29 | 4. Disclaimer 30 | 31 | This application is provided "as is" without any warranties, express or implied. Dummy data displayed is for demonstration purposes only and holds no real-world value. 32 | 33 | 34 | 5. Contact 35 | 36 | If you have any questions or feedback, please contact us: neo.js.cn@gmail.com 37 | 38 | 39 | 40 | @code { 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/CleanAspire.Api/CleanAspire.Api.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net10.0 5 | enable 6 | enable 7 | CleanAspire.Api 8 | CleanAspire.Api 9 | 10 | 11 | 12 | $(MSBuildProjectDirectory) 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | all 21 | runtime; build; native; contentfiles; analyzers; buildtransitive 22 | 23 | 24 | 25 | all 26 | runtime; build; native; contentfiles; analyzers; buildtransitive 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /src/CleanAspire.ClientApp/Client/Webpushr/WebpushrRequestBuilder.cs: -------------------------------------------------------------------------------- 1 | // 2 | #pragma warning disable CS0618 3 | using CleanAspire.Api.Client.Webpushr.Config; 4 | using Microsoft.Kiota.Abstractions.Extensions; 5 | using Microsoft.Kiota.Abstractions; 6 | using System.Collections.Generic; 7 | using System.IO; 8 | using System.Threading.Tasks; 9 | using System; 10 | namespace CleanAspire.Api.Client.Webpushr 11 | { 12 | /// 13 | /// Builds and executes requests for operations under \webpushr 14 | /// 15 | [global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")] 16 | public partial class WebpushrRequestBuilder : BaseRequestBuilder 17 | { 18 | /// The config property 19 | public global::CleanAspire.Api.Client.Webpushr.Config.ConfigRequestBuilder Config 20 | { 21 | get => new global::CleanAspire.Api.Client.Webpushr.Config.ConfigRequestBuilder(PathParameters, RequestAdapter); 22 | } 23 | /// 24 | /// Instantiates a new and sets the default values. 25 | /// 26 | /// Path parameters for the request 27 | /// The request adapter to use to execute the requests. 28 | public WebpushrRequestBuilder(Dictionary pathParameters, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/webpushr", pathParameters) 29 | { 30 | } 31 | /// 32 | /// Instantiates a new and sets the default values. 33 | /// 34 | /// The raw URL to use for the request builder. 35 | /// The request adapter to use to execute the requests. 36 | public WebpushrRequestBuilder(string rawUrl, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/webpushr", rawUrl) 37 | { 38 | } 39 | } 40 | } 41 | #pragma warning restore CS0618 42 | -------------------------------------------------------------------------------- /src/CleanAspire.ClientApp/Services/UserPreferences/UserPreferencesService.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) MudBlazor 2021 2 | // MudBlazor licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for more information. 4 | 5 | using CleanAspire.ClientApp.Services.Interfaces; 6 | 7 | namespace CleanAspire.ClientApp.Services.UserPreferences; 8 | /// 9 | /// Service for managing user preferences. 10 | /// 11 | public interface IUserPreferencesService 12 | { 13 | /// 14 | /// Saves UserPreferences in local storage. 15 | /// 16 | /// The userPreferences to save in the local storage. 17 | /// A task representing the asynchronous operation. 18 | public Task SaveUserPreferences(UserPreferences userPreferences); 19 | 20 | /// 21 | /// Loads UserPreferences from local storage. 22 | /// 23 | /// UserPreferences object. Null when no settings were found. 24 | public Task LoadUserPreferences(); 25 | } 26 | 27 | 28 | /// 29 | /// Implementation of the IUserPreferencesService interface. 30 | /// 31 | public class UserPreferencesService : IUserPreferencesService 32 | { 33 | private readonly IStorageService _localStorage; 34 | private const string Key = "_userPreferences"; 35 | 36 | /// 37 | /// Initializes a new instance of the class. 38 | /// 39 | /// The storage service for accessing local storage. 40 | public UserPreferencesService(IStorageService localStorage) 41 | { 42 | _localStorage = localStorage; 43 | } 44 | 45 | /// 46 | public async Task SaveUserPreferences(UserPreferences userPreferences) 47 | { 48 | await _localStorage.SetItemAsync(Key, userPreferences); 49 | } 50 | 51 | /// 52 | public async Task LoadUserPreferences() 53 | { 54 | return await _localStorage.GetItemAsync(Key) ?? new UserPreferences(); 55 | } 56 | } 57 | 58 | 59 | -------------------------------------------------------------------------------- /src/CleanAspire.Domain/Entities/Product.cs: -------------------------------------------------------------------------------- 1 | using CleanAspire.Domain.Common; 2 | 3 | namespace CleanAspire.Domain.Entities; 4 | 5 | /// 6 | /// Represents a product entity. 7 | /// 8 | public class Product : BaseAuditableEntity, IAuditTrial 9 | { 10 | /// 11 | /// Gets or sets the SKU of the product. 12 | /// 13 | public string SKU { get; set; } = string.Empty; 14 | 15 | /// 16 | /// Gets or sets the name of the product. 17 | /// 18 | public string Name { get; set; } = string.Empty; 19 | 20 | /// 21 | /// Gets or sets the category of the product. 22 | /// 23 | public ProductCategory Category { get; set; } = ProductCategory.Electronics; 24 | 25 | /// 26 | /// Gets or sets the description of the product. 27 | /// 28 | public string? Description { get; set; } 29 | 30 | /// 31 | /// Gets or sets the price of the product. 32 | /// 33 | public decimal Price { get; set; } 34 | 35 | /// 36 | /// Gets or sets the currency of the product price. 37 | /// 38 | public string? Currency { get; set; } 39 | 40 | /// 41 | /// Gets or sets the unit of measure of the product. 42 | /// 43 | public string? UOM { get; set; } 44 | } 45 | 46 | 47 | /// 48 | /// Represents the category of a product. 49 | /// 50 | public enum ProductCategory 51 | { 52 | /// 53 | /// Electronics category. 54 | /// 55 | Electronics, 56 | 57 | /// 58 | /// Furniture category. 59 | /// 60 | Furniture, 61 | 62 | /// 63 | /// Clothing category. 64 | /// 65 | Clothing, 66 | 67 | /// 68 | /// Food category. 69 | /// 70 | Food, 71 | 72 | /// 73 | /// Beverages category. 74 | /// 75 | Beverages, 76 | 77 | /// 78 | /// Health care category. 79 | /// 80 | HealthCare, 81 | 82 | /// 83 | /// Sports category. 84 | /// 85 | Sports, 86 | } 87 | -------------------------------------------------------------------------------- /src/CleanAspire.Application/Pipeline/FusionCacheBehaviour.cs: -------------------------------------------------------------------------------- 1 | namespace CleanAspire.Application.Pipeline; 2 | 3 | /// 4 | /// Pipeline behavior for handling requests with FusionCache. 5 | /// 6 | /// The type of the request. 7 | /// The type of the response. 8 | public class FusionCacheBehaviour : IPipelineBehavior 9 | where TRequest : IFusionCacheRequest 10 | { 11 | private readonly IFusionCache _fusionCache; 12 | private readonly ILogger> _logger; 13 | 14 | /// 15 | /// Initializes a new instance of the class. 16 | /// 17 | /// The FusionCache instance. 18 | /// The logger instance. 19 | public FusionCacheBehaviour( 20 | IFusionCache fusionCache, 21 | ILogger> logger 22 | ) 23 | { 24 | _fusionCache = fusionCache; 25 | _logger = logger; 26 | } 27 | 28 | /// 29 | /// Handles the request by attempting to retrieve the response from the cache, or invoking the next handler if not found. 30 | /// 31 | /// The request instance. 32 | /// The next handler delegate. 33 | /// The cancellation token. 34 | /// The response instance. 35 | public async ValueTask Handle(TRequest request, MessageHandlerDelegate next, 36 | CancellationToken cancellationToken) 37 | { 38 | _logger.LogInformation("Handling request of type {RequestType} with cache key {CacheKey}", nameof(request), request.CacheKey); 39 | var response = await _fusionCache.GetOrSetAsync( 40 | request.CacheKey, 41 | async (ctx, token) => await next(request, token), 42 | tags: request.Tags 43 | ).ConfigureAwait(false); 44 | 45 | return response; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/CleanAspire.ClientApp/Layout/MainLayout.razor: -------------------------------------------------------------------------------- 1 | 2 | @inherits LayoutComponentBase 3 | 4 | @L[AppSettings.AppName] 5 | 6 | 7 | 8 | 9 | 10 | 11 | @Body 12 | 13 | 14 | @code{ 15 | [Inject] 16 | public LayoutService LayoutService { get; set; } = default!; 17 | private MudThemeProvider _mudThemeProvider=default!; 18 | 19 | protected override async Task OnInitializedAsync() 20 | { 21 | if (LayoutService != null) 22 | { 23 | LayoutService.MajorUpdateOccurred += LayoutServiceOnMajorUpdateOccured; 24 | } 25 | OnlineStatusInterop.Initialize(); 26 | await OfflineModeState.InitializeAsync(); 27 | await ApplyUserPreferences(); 28 | if (_mudThemeProvider != null) 29 | { 30 | await _mudThemeProvider.WatchSystemPreference(OnSystemPreferenceChanged); 31 | } 32 | 33 | } 34 | 35 | 36 | 37 | private async Task ApplyUserPreferences() 38 | { 39 | if (_mudThemeProvider != null) 40 | { 41 | var defaultDarkMode = await _mudThemeProvider.GetSystemPreference(); 42 | if (LayoutService != null) 43 | { 44 | await LayoutService.ApplyUserPreferences(defaultDarkMode); 45 | } 46 | } 47 | } 48 | 49 | private async Task OnSystemPreferenceChanged(bool newValue) 50 | { 51 | if (LayoutService != null) 52 | { 53 | await LayoutService.OnSystemPreferenceChanged(newValue); 54 | } 55 | } 56 | 57 | public void Dispose() 58 | { 59 | if (LayoutService != null) 60 | { 61 | LayoutService.MajorUpdateOccurred -= LayoutServiceOnMajorUpdateOccured; 62 | } 63 | } 64 | 65 | private void LayoutServiceOnMajorUpdateOccured(object? sender, EventArgs e) => StateHasChanged(); 66 | } 67 | -------------------------------------------------------------------------------- /src/CleanAspire.ClientApp/Pages/Account/ForgetPassword.razor: -------------------------------------------------------------------------------- 1 | @page "/account/forgetpassword" 2 | @using System.ComponentModel.DataAnnotations 3 | 4 | @L["Forgot Password?"] 5 | 6 |
7 | 8 | @L["Forgot Password?"] 9 |
10 |
11 |
12 | @L["No worries! Just enter your email address below and we'll send you a link to reset your password."] 13 |
14 | 15 | 16 |
17 | 18 | @L["Send Email"] 19 |
20 |
21 |
22 | 23 |
24 | @code { 25 | private ForgetPasswordModel model = new(); 26 | 27 | private async Task OnValidSubmit(EditContext context) 28 | { 29 | var result = await ApiClientServiceProxy.ExecuteAsync(() => ApiClient.Account.ForgotPassword.PostAsync(new ForgotPasswordRequest() { Email = model.Email })); 30 | result.Switch( 31 | ok => 32 | { 33 | Navigation.NavigateTo("/account/forgetpasswordsuccessful"); 34 | }, 35 | invalid => 36 | { 37 | Snackbar.Add(L[invalid.Detail ?? "Failed validation"], Severity.Error); 38 | }, 39 | error => 40 | { 41 | Snackbar.Add(L["Password reset request failed. Please check the email address and try again."], Severity.Error); 42 | } 43 | ); 44 | } 45 | public class ForgetPasswordModel 46 | { 47 | [Required] 48 | [EmailAddress] 49 | public string Email { get; set; } = string.Empty; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/CleanAspire.Api/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "UseInMemoryDatabase": false, 3 | "Logging": { 4 | "LogLevel": { 5 | "Default": "Information", 6 | "Microsoft.AspNetCore": "Warning" 7 | } 8 | }, 9 | "AllowedCorsOrigins": "https://localhost:7341,https://localhost:7123,https://localhost:7114,https://cleanaspire.blazorserver.com/", 10 | "ClientBaseUrl": "https://localhost:7114", 11 | "DatabaseSettings": { 12 | "DBProvider": "sqlite", 13 | "ConnectionString": "Data Source=CleanAspireDb.db" 14 | //"DBProvider": "mssql", 15 | //"ConnectionString": "Server=(localdb)\\mssqllocaldb;Database=CleanAspireDb;Trusted_Connection=True;MultipleActiveResultSets=true;" 16 | //"DBProvider": "postgresql", 17 | //"ConnectionString": "Server=127.0.0.1;Database=CleanAspireDb;User Id=root;Password=root;Port=5432" 18 | }, 19 | "AllowedHosts": "*", 20 | "SendGrid": { 21 | "ApiKey": "your-sendgrid-api-key", 22 | "DefaultFromEmail": "noreply@example.com" 23 | }, 24 | "Webpushr": { 25 | "Token": "your-webpushr-token", 26 | "ApiKey": "your-webpushr-api-keys", 27 | "PublicKey": "your-webpushr-public-key" 28 | }, 29 | "Authentication": { 30 | "Google": { 31 | "ClientId": "your-client-id", 32 | "ClientSecret": "your-client-secret" 33 | }, 34 | "Microsoft": { 35 | "ClientId": "your-client-id", 36 | "ClientSecret": "your-client-secret", 37 | "TenantId": "your-tenant-id" 38 | } 39 | }, 40 | "Minio": { 41 | "Endpoint": "minio.blazorserver.com", 42 | "AccessKey": "your-access-key", 43 | "SecretKey": "your-secret-key", 44 | "BucketName": "aspire" 45 | }, 46 | "Serilog": { 47 | "MinimumLevel": { 48 | "Default": "Debug", 49 | "Override": { 50 | "Microsoft": "Warning", 51 | "Microsoft.Hosting.Lifetime": "Warning", 52 | "System": "Warning", 53 | "System.Net.Http.HttpClient": "Warning" 54 | } 55 | }, 56 | "Properties": { 57 | "Application": "BlazorPWA", 58 | "Environment": "Development", 59 | "TargetFramework": "net9" 60 | }, 61 | "WriteTo": [ 62 | { 63 | "Name": "Seq", 64 | "Args": { 65 | "serverUrl": "http://10.33.1.150:8082", 66 | "apiKey": "none", 67 | "restrictedToMinimumLevel": "Verbose" 68 | } 69 | } 70 | ] 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/CleanAspire.Application/Features/Products/Commands/CreateProductCommand.cs: -------------------------------------------------------------------------------- 1 | // Summary: 2 | // This file defines a command and its handler for creating a new product in the database. 3 | // The CreateProductCommand encapsulates the necessary data for a product, while the 4 | // CreateProductCommandHandler processes the command, creates a product entity, 5 | // triggers domain events such as ProductCreatedEvent, and commits the changes. This ensures 6 | // a structured and efficient approach to handling product creation. 7 | 8 | using CleanAspire.Application.Features.Products.DTOs; 9 | using CleanAspire.Application.Features.Products.EventHandlers; 10 | using CleanAspire.Application.Pipeline; 11 | 12 | namespace CleanAspire.Application.Features.Products.Commands; 13 | 14 | // Command object that encapsulates the data required for creating a new product. 15 | // Its fields directly map to the properties of ProductDto. 16 | public record CreateProductCommand( 17 | string SKU, 18 | string Name, 19 | ProductCategory Category, 20 | string? Description, 21 | decimal Price, 22 | string? Currency, 23 | string? UOM 24 | ) : IFusionCacheRefreshRequest, 25 | IRequiresValidation 26 | { 27 | public IEnumerable? Tags => new[] { "products" }; 28 | } 29 | 30 | public class CreateProductCommandHandler : IRequestHandler 31 | { 32 | private readonly IApplicationDbContext _context; 33 | 34 | public CreateProductCommandHandler(IApplicationDbContext context) 35 | { 36 | _context = context; 37 | } 38 | 39 | public async ValueTask Handle(CreateProductCommand request, CancellationToken cancellationToken) 40 | { 41 | var product = new Product 42 | { 43 | SKU = request.SKU, 44 | Name = request.Name, 45 | Category = request.Category, 46 | Description = request.Description, 47 | Price = request.Price, 48 | Currency = request.Currency, 49 | UOM = request.UOM 50 | }; 51 | 52 | product.AddDomainEvent(new ProductCreatedEvent(product)); 53 | _context.Products.Add(product); 54 | await _context.SaveChangesAsync(cancellationToken); 55 | 56 | return new ProductDto() { Id = product.Id, Name = product.Name, SKU = product.SKU }; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/CleanAspire.ClientApp/Layout/MudBlazorLogo.razor: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 20 | 21 | 22 | 24 | 25 | 26 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | @code { 48 | [Parameter] public string Style { get; set; } = default!; 49 | [Parameter] public string Class { get; set; } = default!; 50 | } -------------------------------------------------------------------------------- /src/CleanAspire.Application/Features/Products/Commands/ImportProductsCommand.cs: -------------------------------------------------------------------------------- 1 | // Summary: 2 | // This file defines a command and its handler for importing products from a CSV file. 3 | // The ImportProductsCommand encapsulates the input stream, while the ImportProductsCommandHandler 4 | // reads the CSV data, maps it to product entities, triggers domain events like ProductCreatedEvent, 5 | // and commits the changes to the database. 6 | 7 | using System.Globalization; 8 | using CleanAspire.Application.Features.Products.DTOs; 9 | using CleanAspire.Application.Features.Products.EventHandlers; 10 | using CleanAspire.Application.Pipeline; 11 | using CsvHelper; 12 | 13 | namespace CleanAspire.Application.Features.Products.Commands; 14 | 15 | // Command object that encapsulates the input stream containing CSV data. 16 | public record ImportProductsCommand(Stream Stream) 17 | : IFusionCacheRefreshRequest, 18 | IRequiresValidation 19 | { 20 | public IEnumerable? Tags => new[] { "products" }; 21 | } 22 | 23 | public class ImportProductsCommandHandler : IRequestHandler 24 | { 25 | private readonly IApplicationDbContext _context; 26 | 27 | public ImportProductsCommandHandler(IApplicationDbContext context) 28 | { 29 | _context = context; 30 | } 31 | 32 | public async ValueTask Handle(ImportProductsCommand request, CancellationToken cancellationToken) 33 | { 34 | request.Stream.Position = 0; 35 | 36 | using (var reader = new StreamReader(request.Stream)) 37 | using (var csv = new CsvReader(reader, CultureInfo.InvariantCulture)) 38 | { 39 | var records = csv.GetRecords(); 40 | 41 | foreach (var product in records.Select(x => new Product 42 | { 43 | SKU = x.SKU, 44 | Name = x.Name, 45 | Category = x.Category, 46 | Description = x.Description, 47 | Price = x.Price, 48 | Currency = x.Currency, 49 | UOM = x.UOM 50 | })) 51 | { 52 | product.AddDomainEvent(new ProductCreatedEvent(product)); 53 | _context.Products.Add(product); 54 | } 55 | 56 | await _context.SaveChangesAsync(cancellationToken); 57 | } 58 | 59 | return Unit.Value; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/CleanAspire.ClientApp/CleanAspire.ClientApp.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net10.0 5 | enable 6 | enable 7 | true 8 | Default 9 | true 10 | service-worker-assets.js 11 | CleanAspire.ClientApp 12 | CleanAspire.ClientApp 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /src/CleanAspire.Infrastructure/CleanAspire.Infrastructure.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net10.0 5 | enable 6 | enable 7 | CleanAspire.Infrastructure 8 | CleanAspire.Infrastructure 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /src/CleanAspire.Application/Pipeline/MessageValidatorBehaviour.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for more information. 4 | 5 | using FluentValidation.Results; 6 | 7 | namespace CleanAspire.Application.Pipeline; 8 | public sealed class MessageValidatorBehaviour : MessagePreProcessor 9 | where TMessage : IMessage, IRequiresValidation 10 | { 11 | private readonly IReadOnlyCollection> _validators; 12 | 13 | public MessageValidatorBehaviour(IEnumerable> validators) 14 | { 15 | _validators = validators.ToList() ?? throw new ArgumentNullException(nameof(validators)); 16 | } 17 | 18 | protected override async ValueTask Handle(TMessage message, 19 | CancellationToken cancellationToken 20 | ) 21 | { 22 | var context = new ValidationContext(message); 23 | var validationResult = await _validators.ValidateAsync(context, cancellationToken); 24 | if (validationResult.Any()) 25 | { 26 | throw new ValidationException(validationResult); 27 | } 28 | 29 | } 30 | } 31 | 32 | 33 | public static class ValidationExtensions 34 | { 35 | public static async Task> ValidateAsync( 36 | this IEnumerable> validators, ValidationContext validationContext, 37 | CancellationToken cancellationToken = default) 38 | { 39 | if (!validators.Any()) return new List(); 40 | 41 | var validationResults = await Task.WhenAll( 42 | validators.Select(v => v.ValidateAsync(validationContext, cancellationToken))); 43 | 44 | return validationResults 45 | .SelectMany(r => r.Errors) 46 | .Where(f => f != null) 47 | .ToList(); 48 | } 49 | 50 | public static Dictionary ToDictionary(this List? failures) 51 | { 52 | return failures != null && failures.Any() 53 | ? failures.GroupBy(e => e.PropertyName, e => e.ErrorMessage) 54 | .ToDictionary(g => g.Key, g => g.ToArray()) 55 | : new Dictionary(); 56 | } 57 | } 58 | public interface IRequiresValidation 59 | { 60 | } 61 | -------------------------------------------------------------------------------- /src/CleanAspire.Application/Features/Products/Commands/UpdateProductCommand.cs: -------------------------------------------------------------------------------- 1 | // Summary: 2 | // This file defines a command and its handler for updating product details in the database. 3 | // The UpdateProductCommand encapsulates the necessary data to update a product, while the 4 | // UpdateProductCommandHandler validates the product's existence, updates its details, 5 | // triggers a domain event such as ProductUpdatedEvent, and commits the changes. 6 | 7 | using CleanAspire.Application.Features.Products.DTOs; 8 | using CleanAspire.Application.Features.Products.EventHandlers; 9 | using CleanAspire.Application.Pipeline; 10 | 11 | namespace CleanAspire.Application.Features.Products.Commands; 12 | 13 | // Command object that encapsulates the data required to update a product. 14 | // Each field corresponds to a property in ProductDto. 15 | public record UpdateProductCommand( 16 | string Id, 17 | string SKU, 18 | string Name, 19 | ProductCategory Category, 20 | string? Description, 21 | decimal Price, 22 | string? Currency, 23 | string? UOM 24 | ) : IFusionCacheRefreshRequest, 25 | IRequiresValidation 26 | { 27 | public IEnumerable? Tags => new[] { "products" }; 28 | } 29 | 30 | public class UpdateProductCommandHandler : IRequestHandler 31 | { 32 | private readonly IApplicationDbContext _context; 33 | 34 | public UpdateProductCommandHandler(IApplicationDbContext context) 35 | { 36 | _context = context; 37 | } 38 | 39 | public async ValueTask Handle(UpdateProductCommand request, CancellationToken cancellationToken) 40 | { 41 | var product = await _context.Products.FindAsync(new object[] { request.Id }, cancellationToken); 42 | if (product == null) 43 | { 44 | throw new KeyNotFoundException($"Product with Id '{request.Id}' was not found."); 45 | } 46 | 47 | product.SKU = request.SKU; 48 | product.Name = request.Name; 49 | product.Category = request.Category; 50 | product.Description = request.Description; 51 | product.Price = request.Price; 52 | product.Currency = request.Currency; 53 | product.UOM = request.UOM; 54 | 55 | product.AddDomainEvent(new ProductUpdatedEvent(product)); 56 | _context.Products.Update(product); 57 | 58 | await _context.SaveChangesAsync(cancellationToken); 59 | 60 | return Unit.Value; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/CleanAspire.Api/Dockerfile: -------------------------------------------------------------------------------- 1 | # Stage 1: Build the API Application 2 | FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build 3 | WORKDIR /src 4 | 5 | COPY ["src/CleanAspire.Api/CleanAspire.Api.csproj", "src/CleanAspire.Api/"] 6 | COPY ["src/CleanAspire.Application/CleanAspire.Application.csproj", "src/CleanAspire.Application/"] 7 | COPY ["src/CleanAspire.Domain/CleanAspire.Domain.csproj", "src/CleanAspire.Domain/"] 8 | COPY ["src/CleanAspire.Infrastructure/CleanAspire.Infrastructure.csproj", "src/CleanAspire.Infrastructure/"] 9 | COPY ["src/CleanAspire.ServiceDefaults/CleanAspire.ServiceDefaults.csproj", "src/CleanAspire.ServiceDefaults/"] 10 | COPY ["src/Migrators/Migrators.MSSQL/Migrators.MSSQL.csproj", "src/Migrators/Migrators.MSSQL/"] 11 | COPY ["src/Migrators/Migrators.PostgreSQL/Migrators.PostgreSQL.csproj", "src/Migrators/Migrators.PostgreSQL/"] 12 | COPY ["src/Migrators/Migrators.SQLite/Migrators.SQLite.csproj", "src/Migrators/Migrators.SQLite/"] 13 | 14 | RUN dotnet restore "src/CleanAspire.Api/CleanAspire.Api.csproj" 15 | 16 | COPY . . 17 | WORKDIR /src/src/CleanAspire.Api 18 | RUN dotnet publish "./CleanAspire.Api.csproj" -c Release -o /app/publish 19 | 20 | # Stage 2: Create the runtime image 21 | FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS runtime 22 | WORKDIR /app 23 | 24 | # Copy the build output from the previous stage 25 | COPY --from=build /app/publish . 26 | 27 | # Install OpenSSL 28 | RUN apt-get update && apt-get install -y openssl 29 | 30 | # Generate a self-signed certificate 31 | RUN mkdir -p /app/https && \ 32 | openssl req -x509 -newkey rsa:4096 -sha256 -days 3650 -nodes \ 33 | -keyout /app/https/private.key -out /app/https/certificate.crt \ 34 | -subj "/C=US/ST=State/L=City/O=Organization/CN=localhost" && \ 35 | openssl pkcs12 -export -out /app/https/aspnetapp.pfx \ 36 | -inkey /app/https/private.key -in /app/https/certificate.crt \ 37 | -password pass:CREDENTIAL_PLACEHOLDER 38 | 39 | 40 | # Setup environment variables for the application to find the certificate 41 | ENV ASPNETCORE_URLS=http://+:80;https://+:443 42 | ENV ASPNETCORE_Kestrel__Certificates__Default__Password="CREDENTIAL_PLACEHOLDER" 43 | ENV ASPNETCORE_Kestrel__Certificates__Default__Path="/app/https/aspnetapp.pfx" 44 | 45 | 46 | # Expose ports 47 | EXPOSE 80 443 48 | 49 | # Set the environment variable for ASP.NET Core to use Production settings 50 | ENV ASPNETCORE_ENVIRONMENT=Development 51 | 52 | # Start the application 53 | ENTRYPOINT ["dotnet", "CleanAspire.Api.dll"] 54 | -------------------------------------------------------------------------------- /src/CleanAspire.ClientApp/Client/Manage/ManageRequestBuilder.cs: -------------------------------------------------------------------------------- 1 | // 2 | #pragma warning disable CS0618 3 | using CleanAspire.Api.Client.Manage.Info; 4 | using CleanAspire.Api.Client.Manage.Twofa; 5 | using Microsoft.Kiota.Abstractions.Extensions; 6 | using Microsoft.Kiota.Abstractions; 7 | using System.Collections.Generic; 8 | using System.IO; 9 | using System.Threading.Tasks; 10 | using System; 11 | namespace CleanAspire.Api.Client.Manage 12 | { 13 | /// 14 | /// Builds and executes requests for operations under \manage 15 | /// 16 | [global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")] 17 | public partial class ManageRequestBuilder : BaseRequestBuilder 18 | { 19 | /// The info property 20 | public global::CleanAspire.Api.Client.Manage.Info.InfoRequestBuilder Info 21 | { 22 | get => new global::CleanAspire.Api.Client.Manage.Info.InfoRequestBuilder(PathParameters, RequestAdapter); 23 | } 24 | /// The Twofa property 25 | public global::CleanAspire.Api.Client.Manage.Twofa.TwofaRequestBuilder Twofa 26 | { 27 | get => new global::CleanAspire.Api.Client.Manage.Twofa.TwofaRequestBuilder(PathParameters, RequestAdapter); 28 | } 29 | /// 30 | /// Instantiates a new and sets the default values. 31 | /// 32 | /// Path parameters for the request 33 | /// The request adapter to use to execute the requests. 34 | public ManageRequestBuilder(Dictionary pathParameters, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/manage", pathParameters) 35 | { 36 | } 37 | /// 38 | /// Instantiates a new and sets the default values. 39 | /// 40 | /// The raw URL to use for the request builder. 41 | /// The request adapter to use to execute the requests. 42 | public ManageRequestBuilder(string rawUrl, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/manage", rawUrl) 43 | { 44 | } 45 | } 46 | } 47 | #pragma warning restore CS0618 48 | -------------------------------------------------------------------------------- /src/CleanAspire.ClientApp/Pages/Account/SignupConfirmation.razor: -------------------------------------------------------------------------------- 1 | @page "/account/confirm-email" 2 | 3 | 4 | @L["Confirm Verification"] 5 | 6 |
7 | 8 | @L["Confirm Verification"] 9 |
10 |
11 | @if (success) 12 | { 13 | @message 14 | @L["Sign In"] 15 | } 16 | else 17 | { 18 | @message 19 | } 20 |
21 |
22 | @code { 23 | [SupplyParameterFromQuery(Name = "code")] 24 | public string? Code { get; set; } 25 | [SupplyParameterFromQuery(Name = "userId")] 26 | public string? UserId { get; set; } 27 | private bool success { get; set; } 28 | private string? message { get; set; } 29 | protected override async Task OnInitializedAsync() 30 | { 31 | if (string.IsNullOrEmpty(Code)) 32 | { 33 | message = L["Invalid or missing token. Please check your verification link."]; 34 | } 35 | else 36 | { 37 | 38 | var result = await ApiClientServiceProxy.ExecuteAsync(() => ApiClient.ConfirmEmail.GetAsync(requestConfiguration => 39 | { 40 | requestConfiguration.QueryParameters.Code = Code; 41 | requestConfiguration.QueryParameters.UserId = UserId; 42 | } 43 | )); 44 | 45 | result.Switch( 46 | ok => 47 | { 48 | success = true; 49 | message = L["Your email has been successfully verified. You can now sign in."]; 50 | }, 51 | invalid => 52 | { 53 | message = L[invalid.Detail ?? "Failed validation"]; 54 | }, 55 | error => 56 | { 57 | message = L["Verification failed. The token may be invalid or expired."]; 58 | }); 59 | success = true; 60 | message = L["Your email has been successfully verified. You can now sign in."]; 61 | 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/CleanAspire.Api/Endpoints/TenantEndpointRegistrar.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for more information. 4 | 5 | using CleanAspire.Application.Features.Tenants.Commands; 6 | using CleanAspire.Application.Features.Tenants.DTOs; 7 | using CleanAspire.Application.Features.Tenants.Queries; 8 | using Mediator; 9 | using Microsoft.AspNetCore.Mvc; 10 | 11 | namespace CleanAspire.Api.Endpoints; 12 | 13 | public class TenantEndpointRegistrar : IEndpointRegistrar 14 | { 15 | public void RegisterRoutes(IEndpointRouteBuilder routes) 16 | { 17 | var group = routes.MapGroup("/tenants").WithTags("tenants"); 18 | 19 | group.MapGet("/", async ([FromServices] IMediator mediator) => await mediator.Send(new GetAllTenantsQuery())) 20 | .Produces>() 21 | .AllowAnonymous() 22 | .WithSummary("Get all tenants") 23 | .WithDescription("Returns a list of all tenants in the system."); 24 | 25 | group.MapGet("/{id}", async (IMediator mediator, [FromRoute] string id) => await mediator.Send(new GetTenantByIdQuery(id))) 26 | .Produces() 27 | .AllowAnonymous() 28 | .WithSummary("Get tenant by ID") 29 | .WithDescription("Returns the details of a specific tenant by their unique ID."); 30 | 31 | group.MapPost("/", async ([FromServices] IMediator mediator, [FromBody] CreateTenantCommand command) => 32 | { 33 | var id = await mediator.Send(command); 34 | return TypedResults.Ok(id); 35 | }).RequireAuthorization() 36 | .WithSummary("Create a new tenant") 37 | .WithDescription("Creates a new tenant with the provided details."); 38 | 39 | group.MapPut("/", async ([FromServices] IMediator mediator, [FromBody] UpdateTenantCommand command) => await mediator.Send(command)) 40 | .RequireAuthorization() 41 | .WithSummary("Update an existing tenant") 42 | .WithDescription("Updates the details of an existing tenant."); 43 | 44 | group.MapDelete("/", async (IMediator mediator, [FromQuery] string[] ids) => await mediator.Send(new DeleteTenantCommand(ids))) 45 | .RequireAuthorization() 46 | .WithSummary("Delete tenants by IDs") 47 | .WithDescription("Deletes one or more tenants by their unique IDs."); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/CleanAspire.ClientApp/Components/Autocompletes/ProductAutocomplete.cs: -------------------------------------------------------------------------------- 1 | 2 | 3 | using CleanAspire.Api.Client; 4 | using CleanAspire.Api.Client.Models; 5 | using CleanAspire.ClientApp.Services; 6 | using Microsoft.AspNetCore.Components; 7 | using MudBlazor; 8 | 9 | namespace CleanAspire.ClientApp.Components.Autocompletes; 10 | 11 | public class ProductAutocomplete : MudAutocomplete 12 | { 13 | public ProductAutocomplete() 14 | { 15 | SearchFunc = SearchKeyValues; 16 | ToStringFunc = dto => dto?.Name; 17 | Dense = true; 18 | ResetValueOnEmptyText = true; 19 | ShowProgressIndicator = true; 20 | } 21 | [Parameter] public string? DefaultProductId { get; set; } 22 | public List? Products { get; set; } = new(); 23 | [Inject] private ApiClient ApiClient { get; set; } = default!; 24 | [Inject] private ApiClientServiceProxy ApiClientServiceProxy { get; set; } = default!; 25 | 26 | protected override async Task OnAfterRenderAsync(bool firstRender) 27 | { 28 | if (firstRender) 29 | { 30 | Products = await ApiClientServiceProxy.QueryAsync("_allproducts", () => ApiClient.Products.GetAsync(), tags: null, expiration: TimeSpan.FromMinutes(60)); 31 | if (!string.IsNullOrEmpty(DefaultProductId)) 32 | { 33 | var defaultProduct = Products?.FirstOrDefault(p => p.Id == DefaultProductId); 34 | if (defaultProduct != null) 35 | { 36 | Value = defaultProduct; 37 | await ValueChanged.InvokeAsync(Value); 38 | } 39 | } 40 | StateHasChanged(); // Trigger a re-render after the tenants are loaded 41 | } 42 | } 43 | private async Task> SearchKeyValues(string? value, CancellationToken cancellation) 44 | { 45 | IEnumerable result; 46 | 47 | if (string.IsNullOrWhiteSpace(value)) 48 | result = Products ?? new List(); 49 | else 50 | result = Products? 51 | .Where(x => x.Name?.Contains(value, StringComparison.InvariantCultureIgnoreCase) == true || 52 | x.Sku?.Contains(value, StringComparison.InvariantCultureIgnoreCase) == true || 53 | x.Description?.Contains(value, StringComparison.InvariantCultureIgnoreCase) == true) 54 | .ToList() ?? new List(); 55 | 56 | return await Task.FromResult(result); 57 | } 58 | } 59 | --------------------------------------------------------------------------------