├── src ├── BuildingBlocks │ ├── Blazor.UI │ │ ├── wwwroot │ │ │ └── fsh.css │ │ ├── GlobalUsings.cs │ │ ├── Components │ │ │ ├── Title │ │ │ │ └── FshPageTitle.razor │ │ │ ├── FshScripts.razor │ │ │ ├── Layouts │ │ │ │ ├── FshBaseLayout.razor.cs │ │ │ │ └── FshDrawerHeader.razor │ │ │ ├── FshHead.razor │ │ │ ├── Navigation │ │ │ │ └── FshBreadcrumbs.razor │ │ │ ├── Base │ │ │ │ └── FshComponentBase.cs │ │ │ ├── Tabs │ │ │ │ └── FshTabs.razor │ │ │ ├── Cards │ │ │ │ └── FshCard.razor │ │ │ ├── Display │ │ │ │ └── FshBadge.razor │ │ │ ├── Feedback │ │ │ │ ├── FshAlert.razor │ │ │ │ └── Snackbar │ │ │ │ │ └── FshSnackbar.razor.cs │ │ │ ├── Inputs │ │ │ │ ├── FshSwitch.razor │ │ │ │ └── FshCheckbox.razor │ │ │ ├── Avatars │ │ │ │ └── FshAvatar.razor │ │ │ ├── Button │ │ │ │ └── FshButton.razor │ │ │ ├── Chips │ │ │ │ └── FshChip.razor │ │ │ └── Data │ │ │ │ ├── Pagination │ │ │ │ └── FshPagination.razor │ │ │ │ └── FshTable.razor │ │ ├── _Imports.razor │ │ ├── Blazor.UI.csproj │ │ └── ServiceCollectionExtensions.cs │ ├── Web │ │ ├── IFshWeb.cs │ │ ├── Origin │ │ │ └── OriginOptions.cs │ │ ├── Modules │ │ │ ├── IModuleConstants.cs │ │ │ ├── IModule.cs │ │ │ └── FshModuleAttribute.cs │ │ ├── RateLimiting │ │ │ ├── FixedWindowPolicyOptions.cs │ │ │ └── RateLimitingOptions.cs │ │ ├── Cors │ │ │ └── CorsOptions.cs │ │ ├── Security │ │ │ ├── SecurityExtensions.cs │ │ │ └── SecurityHeadersOptions.cs │ │ ├── Observability │ │ │ └── Logging │ │ │ │ └── Serilog │ │ │ │ └── StaticLogger.cs │ │ ├── Mediator │ │ │ └── Extensions.cs │ │ ├── OpenApi │ │ │ └── OpenApiOptions.cs │ │ └── Versioning │ │ │ └── Extensions.cs │ ├── Core │ │ ├── IFshCore.cs │ │ ├── Domain │ │ │ ├── IEntity.cs │ │ │ ├── IHasTenant.cs │ │ │ ├── AggregateRoot.cs │ │ │ ├── IHasDomainEvents.cs │ │ │ ├── ISoftDeletable.cs │ │ │ ├── IDomainEvent.cs │ │ │ ├── IAuditableEntity.cs │ │ │ ├── DomainEvent.cs │ │ │ └── BaseEntity.cs │ │ ├── Common │ │ │ └── QueryStringKeys.cs │ │ ├── Context │ │ │ ├── ICurrentUserInitializer.cs │ │ │ └── ICurrentUser.cs │ │ ├── Abstractions │ │ │ └── IAppUser.cs │ │ ├── Core.csproj │ │ └── Exceptions │ │ │ ├── NotFoundException.cs │ │ │ ├── ForbiddenException.cs │ │ │ └── UnauthorizedException.cs │ ├── Shared │ │ ├── Multitenancy │ │ │ ├── IAppTenantInfo.cs │ │ │ └── MultitenancyConstants.cs │ │ ├── Identity │ │ │ ├── AuditingPermissionConstants.cs │ │ │ ├── CustomClaims.cs │ │ │ ├── ClaimConstants.cs │ │ │ ├── ResourceConstants.cs │ │ │ ├── Authorization │ │ │ │ └── EndpointExtensions.cs │ │ │ ├── ActionConstants.cs │ │ │ └── RoleConstants.cs │ │ ├── Persistence │ │ │ ├── DbProviders.cs │ │ │ ├── PagedResponse.cs │ │ │ └── IPagedQuery.cs │ │ ├── Shared.csproj │ │ └── Auditing │ │ │ └── AuditAttributes.cs │ ├── Mailing │ │ ├── Services │ │ │ └── IMailService.cs │ │ ├── MailOptions.cs │ │ ├── Mailing.csproj │ │ └── Extensions.cs │ ├── Persistence │ │ ├── IConnectionStringValidator.cs │ │ ├── IDbInitializer.cs │ │ ├── Specifications │ │ │ ├── OrderExpression.cs │ │ │ ├── ISpecificationOfTResult.cs │ │ │ └── SpecificationOfTResult.cs │ │ └── Persistence.csproj │ ├── Jobs │ │ ├── HangfireOptions.cs │ │ └── Jobs.csproj │ ├── Storage │ │ ├── DTOs │ │ │ └── FileUploadRequest.cs │ │ ├── S3 │ │ │ └── S3StorageOptions.cs │ │ ├── Services │ │ │ └── IStorageService.cs │ │ ├── Storage.csproj │ │ └── FileType.cs │ ├── Eventing │ │ ├── Abstractions │ │ │ ├── IEventSerializer.cs │ │ │ ├── IIntegrationEventHandler.cs │ │ │ ├── IEventBus.cs │ │ │ └── IIntegrationEvent.cs │ │ ├── Inbox │ │ │ └── IInboxStore.cs │ │ ├── Outbox │ │ │ └── IOutboxStore.cs │ │ ├── EventingOptions.cs │ │ └── Eventing.csproj │ └── Caching │ │ ├── ICacheService.cs │ │ ├── CachingOptions.cs │ │ └── Caching.csproj ├── Playground │ ├── FSH.Playground.AppHost │ │ ├── .aspire │ │ │ └── settings.json │ │ ├── appsettings.Development.json │ │ ├── appsettings.json │ │ └── FSH.Playground.AppHost.csproj │ ├── Playground.Blazor │ │ ├── wwwroot │ │ │ └── favicon.ico │ │ ├── Services │ │ │ ├── Api │ │ │ │ └── ApiClients.cs │ │ │ └── CookieAuthenticationStateProvider.cs │ │ ├── appsettings.Development.json │ │ ├── Components │ │ │ ├── Layout │ │ │ │ └── EmptyLayout.razor │ │ │ ├── Pages │ │ │ │ ├── Home.razor │ │ │ │ └── Counter.razor │ │ │ ├── Routes.razor │ │ │ ├── _Imports.razor │ │ │ └── App.razor │ │ ├── appsettings.json │ │ ├── appsettings.Production.json │ │ └── Properties │ │ │ └── launchSettings.json │ ├── Playground.Api │ │ ├── Playground.Api.http │ │ ├── appsettings.Development.json │ │ ├── Requests │ │ │ ├── Identity │ │ │ │ └── identity-token.http │ │ │ └── root-and-health.http │ │ └── Properties │ │ │ └── launchSettings.json │ └── Migrations.PostgreSQL │ │ └── Migrations.PostgreSQL.csproj ├── Modules │ ├── Auditing │ │ ├── Modules.Auditing │ │ │ ├── AssemblyInfo.cs │ │ │ ├── Infrastructure │ │ │ │ ├── Http │ │ │ │ │ ├── HttpContextRoutingExtensions.cs │ │ │ │ │ └── ContentTypeHelper.cs │ │ │ │ └── Serialization │ │ │ │ │ └── SystemTextJsonAuditSerializer.cs │ │ │ ├── Modules.Auditing.csproj │ │ │ ├── Features │ │ │ │ └── v1 │ │ │ │ │ ├── GetAuditSummary │ │ │ │ │ └── GetAuditSummaryQueryValidator.cs │ │ │ │ │ ├── GetSecurityAudits │ │ │ │ │ └── GetSecurityAuditsQueryValidator.cs │ │ │ │ │ ├── GetExceptionAudits │ │ │ │ │ └── GetExceptionAuditsQueryValidator.cs │ │ │ │ │ ├── GetAuditsByTrace │ │ │ │ │ └── GetAuditsByTraceQueryValidator.cs │ │ │ │ │ ├── GetAuditsByCorrelation │ │ │ │ │ └── GetAuditsByCorrelationQueryValidator.cs │ │ │ │ │ ├── GetAudits │ │ │ │ │ ├── GetAuditsQueryValidator.cs │ │ │ │ │ └── GetAuditsEndpoint.cs │ │ │ │ │ └── GetAuditById │ │ │ │ │ └── GetAuditByIdEndpoint.cs │ │ │ ├── Persistence │ │ │ │ ├── AuditRecord.cs │ │ │ │ ├── AuditDbInitializer.cs │ │ │ │ ├── AuditRecordConfiguration.cs │ │ │ │ └── AuditDbContext.cs │ │ │ └── Core │ │ │ │ └── AuditingConfigurator.cs │ │ └── Modules.Auditing.Contracts │ │ │ ├── AuditingContractsMarker.cs │ │ │ ├── v1 │ │ │ ├── GetAuditById │ │ │ │ └── GetAuditByIdQuery.cs │ │ │ ├── GetAuditSummary │ │ │ │ └── GetAuditSummaryQuery.cs │ │ │ ├── GetAuditsByTrace │ │ │ │ └── GetAuditsByTraceQuery.cs │ │ │ ├── GetAuditsByCorrelation │ │ │ │ └── GetAuditsByCorrelationQuery.cs │ │ │ ├── GetSecurityAudits │ │ │ │ └── GetSecurityAuditsQuery.cs │ │ │ ├── GetExceptionAudits │ │ │ │ └── GetExceptionAuditsQuery.cs │ │ │ └── GetAudits │ │ │ │ └── GetAuditsQuery.cs │ │ │ ├── IAuditMaskingService.cs │ │ │ ├── IAuditSerializer.cs │ │ │ ├── IAuditMutatingEnricher.cs │ │ │ ├── IAuditSink.cs │ │ │ ├── ExceptionEventPayload.cs │ │ │ ├── SecurityEventPayload.cs │ │ │ ├── IAuditEnricher.cs │ │ │ ├── PropertyChange.cs │ │ │ ├── ExceptionSeverityClassifier.cs │ │ │ ├── EntityChangeEventPayload.cs │ │ │ ├── ActivityEventPayload.cs │ │ │ ├── Modules.Auditing.Contracts.csproj │ │ │ ├── IAuditPublisher.cs │ │ │ ├── ISecurityAudit.cs │ │ │ ├── Dtos │ │ │ ├── AuditSummaryAggregateDto.cs │ │ │ ├── AuditSummaryDto.cs │ │ │ └── AuditDetailDto.cs │ │ │ └── AuditHttpOptions.cs │ ├── Identity │ │ ├── Modules.Identity │ │ │ ├── AssemblyInfo.cs │ │ │ ├── AuthenticationConstants.cs │ │ │ ├── IRequiredPermissionMetadata.cs │ │ │ ├── Authorization │ │ │ │ └── PermissionAuthorizationRequirement.cs │ │ │ ├── Features │ │ │ │ └── v1 │ │ │ │ │ ├── RoleClaims │ │ │ │ │ └── FshRoleClaim.cs │ │ │ │ │ ├── Roles │ │ │ │ │ ├── UpsertRole │ │ │ │ │ │ ├── UpsertRoleCommandValidator.cs │ │ │ │ │ │ ├── UpsertRoleCommandHandler.cs │ │ │ │ │ │ └── CreateOrUpdateRoleEndpoint.cs │ │ │ │ │ ├── FshRole.cs │ │ │ │ │ ├── UpdateRolePermissions │ │ │ │ │ │ ├── UpdatePermissionsCommandValidator.cs │ │ │ │ │ │ └── UpdatePermissionsCommandHandler.cs │ │ │ │ │ ├── GetRoles │ │ │ │ │ │ ├── GetRolesQueryHandler.cs │ │ │ │ │ │ └── GetRolesEndpoint.cs │ │ │ │ │ ├── GetRoleById │ │ │ │ │ │ ├── GetRoleByIdQueryHandler.cs │ │ │ │ │ │ └── GetRoleByIdEndpoint.cs │ │ │ │ │ ├── DeleteRole │ │ │ │ │ │ ├── DeleteRoleCommandHandler.cs │ │ │ │ │ │ └── DeleteRoleEndpoint.cs │ │ │ │ │ └── GetRoleWithPermissions │ │ │ │ │ │ ├── GetRoleWithPermissionsQueryHandler.cs │ │ │ │ │ │ └── GetRolePermissionsEndpoint.cs │ │ │ │ │ ├── Users │ │ │ │ │ ├── PasswordHistory │ │ │ │ │ │ └── PasswordHistory.cs │ │ │ │ │ ├── ForgotPassword │ │ │ │ │ │ └── ForgotPasswordCommandValidator.cs │ │ │ │ │ ├── ResetPassword │ │ │ │ │ │ ├── ResetPasswordCommandValidator.cs │ │ │ │ │ │ └── ResetPasswordCommandHandler.cs │ │ │ │ │ ├── AssignUserRoles │ │ │ │ │ │ └── AssignUserRolesCommandHandler.cs │ │ │ │ │ ├── GetUsers │ │ │ │ │ │ ├── GetUsersQueryHandler.cs │ │ │ │ │ │ └── GetUsersListEndpoint.cs │ │ │ │ │ ├── DeleteUser │ │ │ │ │ │ ├── DeleteUserCommandHandler.cs │ │ │ │ │ │ └── DeleteUserEndpoint.cs │ │ │ │ │ ├── GetUserById │ │ │ │ │ │ ├── GetUserByIdQueryHandler.cs │ │ │ │ │ │ └── GetUserByIdEndpoint.cs │ │ │ │ │ ├── SearchUsers │ │ │ │ │ │ └── SearchUsersQueryValidator.cs │ │ │ │ │ ├── ConfirmEmail │ │ │ │ │ │ ├── ConfirmEmailCommandHandler.cs │ │ │ │ │ │ └── ConfirmEmailEndpoint.cs │ │ │ │ │ ├── GetUserRoles │ │ │ │ │ │ ├── GetUserRolesQueryHandler.cs │ │ │ │ │ │ └── GetUserRolesEndpoint.cs │ │ │ │ │ ├── GetUserProfile │ │ │ │ │ │ └── GetCurrentUserProfileQueryHandler.cs │ │ │ │ │ ├── GetUserPermissions │ │ │ │ │ │ └── GetCurrentUserPermissionsQueryHandler.cs │ │ │ │ │ ├── UserImageValidator.cs │ │ │ │ │ ├── FshUser.cs │ │ │ │ │ ├── ChangePassword │ │ │ │ │ │ └── ChangePasswordEndpoint.cs │ │ │ │ │ └── UpdateUser │ │ │ │ │ │ └── UpdateUserCommandHandler.cs │ │ │ │ │ ├── Tokens │ │ │ │ │ ├── RefreshToken │ │ │ │ │ │ └── RefreshTokenCommandValidator.cs │ │ │ │ │ └── TokenGeneration │ │ │ │ │ │ └── GenerateTokenCommandValidator.cs │ │ │ │ │ └── Sessions │ │ │ │ │ ├── GetUserSessions │ │ │ │ │ └── GetUserSessionsQueryHandler.cs │ │ │ │ │ └── GetMySessions │ │ │ │ │ ├── GetMySessionsEndpoint.cs │ │ │ │ │ └── GetMySessionsQueryHandler.cs │ │ │ ├── IdentityModuleConstants.cs │ │ │ ├── Data │ │ │ │ └── PasswordPolicyOptions.cs │ │ │ └── IdentityMetrics.cs │ │ └── Modules.Identity.Contracts │ │ │ ├── DTOs │ │ │ ├── TokenDto.cs │ │ │ ├── TokenResponse.cs │ │ │ ├── UserRoleDto.cs │ │ │ ├── RoleDto.cs │ │ │ ├── UserDto.cs │ │ │ └── UserSessionDto.cs │ │ │ ├── v1 │ │ │ ├── Users │ │ │ │ ├── RegisterUser │ │ │ │ │ ├── RegisterUserResponse.cs │ │ │ │ │ └── RegisterUserCommand.cs │ │ │ │ ├── AssignUserRoles │ │ │ │ │ ├── AssignUserRolesCommandResponse.cs │ │ │ │ │ └── AssignUserRolesCommand.cs │ │ │ │ ├── DeleteUser │ │ │ │ │ └── DeleteUserCommand.cs │ │ │ │ ├── GetUser │ │ │ │ │ └── GetUserQuery.cs │ │ │ │ ├── GetUsers │ │ │ │ │ └── GetUsersQuery.cs │ │ │ │ ├── ConfirmEmail │ │ │ │ │ └── ConfirmEmailCommand.cs │ │ │ │ ├── GetUserPermissions │ │ │ │ │ └── GetCurrentUserPermissionsQuery.cs │ │ │ │ ├── ForgotPassword │ │ │ │ │ └── ForgotPasswordCommand.cs │ │ │ │ ├── GetUserRoles │ │ │ │ │ └── GetUserRolesQuery.cs │ │ │ │ ├── GetUserProfile │ │ │ │ │ └── GetCurrentUserProfileQuery.cs │ │ │ │ ├── ToggleUserStatus │ │ │ │ │ └── ToggleUserStatusCommand.cs │ │ │ │ ├── ResetPassword │ │ │ │ │ └── ResetPasswordCommand.cs │ │ │ │ ├── UpdateUser │ │ │ │ │ └── UpdateUserCommand.cs │ │ │ │ ├── ChangePassword │ │ │ │ │ └── ChangePasswordCommand.cs │ │ │ │ └── SearchUsers │ │ │ │ │ └── SearchUsersQuery.cs │ │ │ ├── Roles │ │ │ │ ├── DeleteRole │ │ │ │ │ └── DeleteRoleCommand.cs │ │ │ │ ├── GetRole │ │ │ │ │ └── GetRoleQuery.cs │ │ │ │ ├── GetRoles │ │ │ │ │ └── GetRolesQuery.cs │ │ │ │ ├── GetRoleWithPermissions │ │ │ │ │ └── GetRoleWithPermissionsQuery.cs │ │ │ │ ├── UpsertRole │ │ │ │ │ └── UpsertRoleCommand.cs │ │ │ │ └── UpdatePermissions │ │ │ │ │ └── UpdatePermissionsCommand.cs │ │ │ ├── Sessions │ │ │ │ ├── RevokeSession │ │ │ │ │ └── RevokeSessionCommand.cs │ │ │ │ ├── RevokeAllSessions │ │ │ │ │ └── RevokeAllSessionsCommand.cs │ │ │ │ ├── GetMySessions │ │ │ │ │ └── GetMySessionsQuery.cs │ │ │ │ ├── AdminRevokeAllSessions │ │ │ │ │ └── AdminRevokeAllSessionsCommand.cs │ │ │ │ ├── AdminRevokeSession │ │ │ │ │ └── AdminRevokeSessionCommand.cs │ │ │ │ └── GetUserSessions │ │ │ │ │ └── GetUserSessionsQuery.cs │ │ │ └── Tokens │ │ │ │ ├── RefreshToken │ │ │ │ ├── RefreshTokenCommand.cs │ │ │ │ └── RefreshTokenCommandResponse.cs │ │ │ │ └── TokenGeneration │ │ │ │ └── GenerateTokenCommand.cs │ │ │ ├── IdentityContractsMarker.cs │ │ │ ├── Services │ │ │ ├── ITokenService.cs │ │ │ └── IRoleService.cs │ │ │ ├── Events │ │ │ ├── UserRegisteredIntegrationEvent.cs │ │ │ └── TokenGeneratedIntegrationEvent.cs │ │ │ └── Modules.Identity.Contracts.csproj │ └── Multitenancy │ │ ├── Modules.Multitenancy │ │ ├── AssemblyInfo.cs │ │ ├── Provisioning │ │ │ ├── TenantProvisioningStatus.cs │ │ │ └── TenantProvisioningStepName.cs │ │ ├── Features │ │ │ └── v1 │ │ │ │ ├── ChangeTenantActivation │ │ │ │ └── ChangeTenantActivationCommandValidator.cs │ │ │ │ ├── UpgradeTenant │ │ │ │ ├── UpgradeTenantCommandValidator.cs │ │ │ │ └── UpgradeTenantCommandHandler.cs │ │ │ │ ├── GetTenantTheme │ │ │ │ └── GetTenantThemeQueryHandler.cs │ │ │ │ ├── GetTenantStatus │ │ │ │ └── GetTenantStatusQueryHandler.cs │ │ │ │ ├── GetTenants │ │ │ │ └── GetTenantsQueryHandler.cs │ │ │ │ ├── TenantProvisioning │ │ │ │ └── GetTenantProvisioningStatus │ │ │ │ │ └── GetTenantProvisioningStatusQueryHandler.cs │ │ │ │ ├── ResetTenantTheme │ │ │ │ └── ResetTenantThemeCommandHandler.cs │ │ │ │ └── UpdateTenantTheme │ │ │ │ └── UpdateTenantThemeCommandHandler.cs │ │ ├── Data │ │ │ └── Configurations │ │ │ │ ├── AppTenantInfoConfiguration.cs │ │ │ │ ├── TenantProvisioningStepConfiguration.cs │ │ │ │ └── TenantProvisioningConfiguration.cs │ │ ├── Extensions.cs │ │ └── MultitenancyOptions.cs │ │ └── Modules.Multitenancy.Contracts │ │ ├── v1 │ │ ├── ResetTenantTheme │ │ │ └── ResetTenantThemeCommand.cs │ │ ├── UpgradeTenant │ │ │ ├── UpgradeTenantCommandResponse.cs │ │ │ └── UpgradeTenantCommand.cs │ │ ├── CreateTenant │ │ │ ├── CreateTenantCommandResponse.cs │ │ │ └── CreateTenantCommand.cs │ │ ├── GetTenantTheme │ │ │ └── GetTenantThemeQuery.cs │ │ ├── GetTenantStatus │ │ │ └── GetTenantStatusQuery.cs │ │ ├── UpdateTenantTheme │ │ │ └── UpdateTenantThemeCommand.cs │ │ ├── GetTenantMigrations │ │ │ └── GetTenantMigrationsQuery.cs │ │ ├── TenantProvisioning │ │ │ ├── RetryTenantProvisioningCommand.cs │ │ │ └── GetTenantProvisioningStatusQuery.cs │ │ ├── ChangeTenantActivation │ │ │ └── ChangeTenantActivationCommand.cs │ │ └── GetTenants │ │ │ └── GetTenantsQuery.cs │ │ ├── MultitenancyContractsMarker.cs │ │ ├── Dtos │ │ ├── TenantLifecycleResultDto.cs │ │ ├── TenantDto.cs │ │ ├── TenantStatusDto.cs │ │ ├── TenantProvisioningStatusDto.cs │ │ └── TenantMigrationStatusDto.cs │ │ └── Modules.Multitenancy.Contracts.csproj ├── Tools │ └── CLI │ │ └── Program.cs └── Tests │ └── Multitenacy.Tests │ └── Multitenacy.Tests.csproj ├── .template.config ├── icon.png └── ide.host.json ├── terraform ├── modules │ ├── alb │ │ ├── versions.tf │ │ └── outputs.tf │ ├── network │ │ └── versions.tf │ ├── s3_bucket │ │ └── versions.tf │ ├── ecs_cluster │ │ ├── versions.tf │ │ └── outputs.tf │ ├── ecs_service │ │ └── versions.tf │ ├── elasticache_redis │ │ └── versions.tf │ └── rds_postgres │ │ └── versions.tf └── apps │ ├── playground │ ├── app_stack │ │ └── versions.tf │ ├── envs │ │ ├── dev │ │ │ └── us-east-1 │ │ │ │ └── backend.tf │ │ ├── prod │ │ │ └── us-east-1 │ │ │ │ ├── backend.tf │ │ │ │ └── prod.us-east-1.tfvars │ │ └── staging │ │ │ └── us-east-1 │ │ │ ├── backend.tf │ │ │ └── staging.us-east-1.tfvars │ └── README.md │ └── restaurantpos │ └── README.md ├── .config └── dotnet-tools.json └── scripts └── openapi ├── check-openapi-drift.ps1 └── generate-api-clients.ps1 /src/BuildingBlocks/Blazor.UI/wwwroot/fsh.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/BuildingBlocks/Blazor.UI/GlobalUsings.cs: -------------------------------------------------------------------------------- 1 | global using MudBlazor; 2 | global using MudBlazor.Services; -------------------------------------------------------------------------------- /src/BuildingBlocks/Web/IFshWeb.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.Framework.Web; 2 | public interface IFshWeb 3 | { 4 | } 5 | -------------------------------------------------------------------------------- /src/BuildingBlocks/Core/IFshCore.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.Framework.Core; 2 | public interface IFshCore 3 | { 4 | } 5 | -------------------------------------------------------------------------------- /.template.config/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fullstackhero/dotnet-starter-kit/HEAD/.template.config/icon.png -------------------------------------------------------------------------------- /src/BuildingBlocks/Blazor.UI/Components/Title/FshPageTitle.razor: -------------------------------------------------------------------------------- 1 |

FshPageTitle

2 | 3 | @code { 4 | 5 | } 6 | -------------------------------------------------------------------------------- /src/Playground/FSH.Playground.AppHost/.aspire/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "appHostPath": "../FSH.Playground.AppHost.csproj" 3 | } -------------------------------------------------------------------------------- /src/BuildingBlocks/Blazor.UI/Components/FshScripts.razor: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.template.config/ide.host.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/vs-2017.3.host", 3 | "order": 0, 4 | "icon": "icon.png" 5 | } -------------------------------------------------------------------------------- /src/BuildingBlocks/Core/Domain/IEntity.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.Framework.Core.Domain; 2 | public interface IEntity 3 | { 4 | TId Id { get; } 5 | } -------------------------------------------------------------------------------- /src/BuildingBlocks/Core/Domain/IHasTenant.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.Framework.Core.Domain; 2 | public interface IHasTenant 3 | { 4 | string TenantId { get; } 5 | } -------------------------------------------------------------------------------- /src/BuildingBlocks/Web/Origin/OriginOptions.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.Framework.Web.Origin; 2 | 3 | public class OriginOptions 4 | { 5 | public Uri? OriginUrl { get; set; } 6 | } -------------------------------------------------------------------------------- /src/Modules/Auditing/Modules.Auditing/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using FSH.Framework.Web.Modules; 2 | 3 | [assembly: FshModule(typeof(FSH.Modules.Auditing.AuditingModule), 300)] 4 | -------------------------------------------------------------------------------- /src/Modules/Identity/Modules.Identity/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using FSH.Framework.Web.Modules; 2 | 3 | [assembly: FshModule(typeof(FSH.Modules.Identity.IdentityModule), 100)] 4 | -------------------------------------------------------------------------------- /src/Playground/Playground.Blazor/wwwroot/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fullstackhero/dotnet-starter-kit/HEAD/src/Playground/Playground.Blazor/wwwroot/favicon.ico -------------------------------------------------------------------------------- /src/BuildingBlocks/Blazor.UI/Components/Layouts/FshBaseLayout.razor.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.Framework.Blazor.UI.Components.Layouts; 2 | 3 | public partial class FshBaseLayout 4 | { 5 | } 6 | -------------------------------------------------------------------------------- /src/Modules/Multitenancy/Modules.Multitenancy/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using FSH.Framework.Web.Modules; 2 | 3 | [assembly: FshModule(typeof(FSH.Modules.Multitenancy.MultitenancyModule), 200)] 4 | -------------------------------------------------------------------------------- /src/BuildingBlocks/Shared/Multitenancy/IAppTenantInfo.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.Framework.Shared.Multitenancy; 2 | 3 | public interface IAppTenantInfo 4 | { 5 | string? ConnectionString { get; set; } 6 | } -------------------------------------------------------------------------------- /src/Playground/Playground.Api/Playground.Api.http: -------------------------------------------------------------------------------- 1 | @Playground.Api_HostAddress = http://localhost:5014 2 | 3 | GET {{Playground.Api_HostAddress}}/weatherforecast/ 4 | Accept: application/json 5 | 6 | ### 7 | -------------------------------------------------------------------------------- /src/Playground/Playground.Blazor/Services/Api/ApiClients.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.Playground.Blazor.Services.Api; 2 | 3 | internal static class ApiClients 4 | { 5 | public const string FSH = "fshapi"; 6 | } 7 | -------------------------------------------------------------------------------- /src/BuildingBlocks/Mailing/Services/IMailService.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.Framework.Mailing.Services; 2 | 3 | public interface IMailService 4 | { 5 | Task SendAsync(MailRequest request, CancellationToken ct); 6 | } -------------------------------------------------------------------------------- /src/Modules/Identity/Modules.Identity.Contracts/DTOs/TokenDto.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.Modules.Identity.Contracts.DTOs; 2 | 3 | public record TokenDto(string Token, string RefreshToken, DateTime RefreshTokenExpiryTime); -------------------------------------------------------------------------------- /src/Playground/Playground.Blazor/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/Modules/Identity/Modules.Identity.Contracts/v1/Users/RegisterUser/RegisterUserResponse.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.Modules.Identity.Contracts.v1.Users.RegisterUser; 2 | 3 | public record RegisterUserResponse(string UserId); -------------------------------------------------------------------------------- /src/Playground/FSH.Playground.AppHost/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/Modules/Identity/Modules.Identity/AuthenticationConstants.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.Modules.Identity; 2 | 3 | public static class AuthenticationConstants 4 | { 5 | public const string AuthenticationScheme = "Bearer"; 6 | } -------------------------------------------------------------------------------- /src/Modules/Identity/Modules.Identity/IRequiredPermissionMetadata.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.Modules.Identity; 2 | 3 | public interface IRequiredPermissionMetadata 4 | { 5 | HashSet RequiredPermissions { get; } 6 | } 7 | -------------------------------------------------------------------------------- /src/BuildingBlocks/Core/Common/QueryStringKeys.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.Framework.Core.Common; 2 | public static class QueryStringKeys 3 | { 4 | public const string Code = "code"; 5 | public const string UserId = "userId"; 6 | } -------------------------------------------------------------------------------- /src/BuildingBlocks/Core/Domain/AggregateRoot.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.Framework.Core.Domain; 2 | public abstract class AggregateRoot : BaseEntity 3 | { 4 | // Put aggregate-wide behaviors/helpers here if needed 5 | } 6 | -------------------------------------------------------------------------------- /src/Playground/Playground.Blazor/Components/Layout/EmptyLayout.razor: -------------------------------------------------------------------------------- 1 | @inherits LayoutComponentBase 2 | 3 | 4 | 5 | 6 | 7 | 8 | @Body 9 | -------------------------------------------------------------------------------- /src/BuildingBlocks/Core/Domain/IHasDomainEvents.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.Framework.Core.Domain; 2 | public interface IHasDomainEvents 3 | { 4 | IReadOnlyCollection DomainEvents { get; } 5 | void ClearDomainEvents(); 6 | } -------------------------------------------------------------------------------- /terraform/modules/alb/versions.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 1.14.0" 3 | 4 | required_providers { 5 | aws = { 6 | source = "hashicorp/aws" 7 | version = ">= 5.80.0" 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/BuildingBlocks/Persistence/IConnectionStringValidator.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.Framework.Persistence; 2 | 3 | public interface IConnectionStringValidator 4 | { 5 | bool TryValidate(string connectionString, string? dbProvider = null); 6 | } -------------------------------------------------------------------------------- /src/BuildingBlocks/Web/Modules/IModuleConstants.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.Framework.Web.Modules; 2 | 3 | public interface IModuleConstants 4 | { 5 | string ModuleId { get; } 6 | string ModuleName { get; } 7 | string ApiPrefix { get; } 8 | } -------------------------------------------------------------------------------- /terraform/modules/network/versions.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 1.14.0" 3 | 4 | required_providers { 5 | aws = { 6 | source = "hashicorp/aws" 7 | version = ">= 5.80.0" 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /terraform/modules/s3_bucket/versions.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 1.14.0" 3 | 4 | required_providers { 5 | aws = { 6 | source = "hashicorp/aws" 7 | version = ">= 5.80.0" 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/Playground/Playground.Blazor/Components/Pages/Home.razor: -------------------------------------------------------------------------------- 1 | @page "/welcome" 2 | @inherits ComponentBase 3 | 4 | 6 | -------------------------------------------------------------------------------- /terraform/modules/ecs_cluster/versions.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 1.14.0" 3 | 4 | required_providers { 5 | aws = { 6 | source = "hashicorp/aws" 7 | version = ">= 5.80.0" 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /terraform/modules/ecs_service/versions.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 1.14.0" 3 | 4 | required_providers { 5 | aws = { 6 | source = "hashicorp/aws" 7 | version = ">= 5.80.0" 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/BuildingBlocks/Core/Domain/ISoftDeletable.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.Framework.Core.Domain; 2 | public interface ISoftDeletable 3 | { 4 | bool IsDeleted { get; } 5 | DateTimeOffset? DeletedOnUtc { get; } 6 | string? DeletedBy { get; } 7 | } 8 | -------------------------------------------------------------------------------- /src/BuildingBlocks/Shared/Identity/AuditingPermissionConstants.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.Framework.Shared.Identity; 2 | 3 | public static class AuditingPermissionConstants 4 | { 5 | public const string View = "Permissions.AuditTrails.View"; 6 | } 7 | 8 | -------------------------------------------------------------------------------- /src/Modules/Auditing/Modules.Auditing.Contracts/AuditingContractsMarker.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.Modules.Auditing.Contracts; 2 | 3 | // Marker type for contract assembly scanning (Mediator, etc.) 4 | public sealed class AuditingContractsMarker 5 | { 6 | } 7 | -------------------------------------------------------------------------------- /src/Modules/Identity/Modules.Identity.Contracts/IdentityContractsMarker.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.Modules.Identity.Contracts; 2 | 3 | // Marker type for contract assembly scanning (Mediator, etc.) 4 | public sealed class IdentityContractsMarker 5 | { 6 | } 7 | -------------------------------------------------------------------------------- /src/Modules/Identity/Modules.Identity.Contracts/v1/Users/AssignUserRoles/AssignUserRolesCommandResponse.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.Modules.Identity.Contracts.v1.Users.AssignUserRoles; 2 | 3 | public sealed record AssignUserRolesCommandResponse(string Result); -------------------------------------------------------------------------------- /terraform/apps/playground/app_stack/versions.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 1.14.0" 3 | 4 | required_providers { 5 | aws = { 6 | source = "hashicorp/aws" 7 | version = ">= 5.80.0" 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /terraform/modules/elasticache_redis/versions.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 1.14.0" 3 | 4 | required_providers { 5 | aws = { 6 | source = "hashicorp/aws" 7 | version = ">= 5.80.0" 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/Playground/FSH.Playground.AppHost/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning", 6 | "Aspire.Hosting.Dcp": "Warning" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/BuildingBlocks/Persistence/IDbInitializer.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.Framework.Persistence; 2 | 3 | public interface IDbInitializer 4 | { 5 | Task MigrateAsync(CancellationToken cancellationToken); 6 | Task SeedAsync(CancellationToken cancellationToken); 7 | } -------------------------------------------------------------------------------- /src/Modules/Identity/Modules.Identity.Contracts/v1/Roles/DeleteRole/DeleteRoleCommand.cs: -------------------------------------------------------------------------------- 1 | using Mediator; 2 | 3 | namespace FSH.Modules.Identity.Contracts.v1.Roles.DeleteRole; 4 | 5 | public sealed record DeleteRoleCommand(string Id) : ICommand; 6 | 7 | -------------------------------------------------------------------------------- /src/Modules/Identity/Modules.Identity.Contracts/v1/Users/DeleteUser/DeleteUserCommand.cs: -------------------------------------------------------------------------------- 1 | using Mediator; 2 | 3 | namespace FSH.Modules.Identity.Contracts.v1.Users.DeleteUser; 4 | 5 | public sealed record DeleteUserCommand(string Id) : ICommand; 6 | 7 | -------------------------------------------------------------------------------- /src/Modules/Multitenancy/Modules.Multitenancy.Contracts/v1/ResetTenantTheme/ResetTenantThemeCommand.cs: -------------------------------------------------------------------------------- 1 | using Mediator; 2 | 3 | namespace FSH.Modules.Multitenancy.Contracts.v1.ResetTenantTheme; 4 | 5 | public sealed record ResetTenantThemeCommand : ICommand; 6 | -------------------------------------------------------------------------------- /src/Modules/Multitenancy/Modules.Multitenancy.Contracts/v1/UpgradeTenant/UpgradeTenantCommandResponse.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.Modules.Multitenancy.Contracts.v1.UpgradeTenant; 2 | 3 | public sealed record UpgradeTenantCommandResponse(DateTime NewValidity, string Tenant); -------------------------------------------------------------------------------- /.config/dotnet-tools.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "isRoot": true, 4 | "tools": { 5 | "nswag.consolecore": { 6 | "version": "14.6.3", 7 | "commands": [ 8 | "nswag" 9 | ], 10 | "rollForward": false 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /src/Modules/Identity/Modules.Identity/Authorization/PermissionAuthorizationRequirement.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authorization; 2 | 3 | namespace FSH.Modules.Identity.Authorization; 4 | 5 | public class PermissionAuthorizationRequirement : IAuthorizationRequirement; -------------------------------------------------------------------------------- /src/Modules/Multitenancy/Modules.Multitenancy.Contracts/MultitenancyContractsMarker.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.Modules.Multitenancy.Contracts; 2 | 3 | // Marker type for contract assembly scanning (Mediator, etc.) 4 | public sealed class MultitenancyContractsMarker 5 | { 6 | } 7 | -------------------------------------------------------------------------------- /src/BuildingBlocks/Core/Domain/IDomainEvent.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.Framework.Core.Domain; 2 | public interface IDomainEvent 3 | { 4 | Guid EventId { get; } 5 | DateTimeOffset OccurredOnUtc { get; } 6 | string? CorrelationId { get; } 7 | string? TenantId { get; } 8 | } -------------------------------------------------------------------------------- /src/Modules/Identity/Modules.Identity.Contracts/v1/Sessions/RevokeSession/RevokeSessionCommand.cs: -------------------------------------------------------------------------------- 1 | using Mediator; 2 | 3 | namespace FSH.Modules.Identity.Contracts.v1.Sessions.RevokeSession; 4 | 5 | public sealed record RevokeSessionCommand(Guid SessionId) : ICommand; 6 | -------------------------------------------------------------------------------- /src/BuildingBlocks/Core/Context/ICurrentUserInitializer.cs: -------------------------------------------------------------------------------- 1 | using System.Security.Claims; 2 | 3 | namespace FSH.Framework.Core.Context; 4 | public interface ICurrentUserInitializer 5 | { 6 | void SetCurrentUser(ClaimsPrincipal user); 7 | 8 | void SetCurrentUserId(string userId); 9 | } -------------------------------------------------------------------------------- /src/Modules/Identity/Modules.Identity.Contracts/v1/Users/GetUser/GetUserQuery.cs: -------------------------------------------------------------------------------- 1 | using FSH.Modules.Identity.Contracts.DTOs; 2 | using Mediator; 3 | 4 | namespace FSH.Modules.Identity.Contracts.v1.Users.GetUser; 5 | 6 | public sealed record GetUserQuery(string Id) : IQuery; 7 | 8 | -------------------------------------------------------------------------------- /src/Modules/Identity/Modules.Identity.Contracts/v1/Users/GetUsers/GetUsersQuery.cs: -------------------------------------------------------------------------------- 1 | using FSH.Modules.Identity.Contracts.DTOs; 2 | using Mediator; 3 | 4 | namespace FSH.Modules.Identity.Contracts.v1.Users.GetUsers; 5 | 6 | public sealed record GetUsersQuery : IQuery>; 7 | 8 | -------------------------------------------------------------------------------- /src/Modules/Multitenancy/Modules.Multitenancy/Provisioning/TenantProvisioningStatus.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.Modules.Multitenancy.Provisioning; 2 | 3 | public enum TenantProvisioningStatus 4 | { 5 | Pending = 0, 6 | Running = 1, 7 | Completed = 2, 8 | Failed = 3 9 | } 10 | -------------------------------------------------------------------------------- /src/Modules/Identity/Modules.Identity.Contracts/DTOs/TokenResponse.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.Modules.Identity.Contracts.DTOs; 2 | 3 | public sealed record TokenResponse( 4 | string AccessToken, 5 | string RefreshToken, 6 | DateTime RefreshTokenExpiresAt, 7 | DateTime AccessTokenExpiresAt); -------------------------------------------------------------------------------- /src/Modules/Identity/Modules.Identity.Contracts/v1/Roles/GetRole/GetRoleQuery.cs: -------------------------------------------------------------------------------- 1 | using FSH.Modules.Identity.Contracts.DTOs; 2 | using Mediator; 3 | 4 | namespace FSH.Modules.Identity.Contracts.v1.Roles.GetRole; 5 | 6 | public sealed record GetRoleQuery(string Id) : IQuery; 7 | 8 | -------------------------------------------------------------------------------- /src/BuildingBlocks/Jobs/HangfireOptions.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.Framework.Jobs; 2 | 3 | public class HangfireOptions 4 | { 5 | public string UserName { get; set; } = "admin"; 6 | public string Password { get; set; } = "Secure1234!Me"; 7 | public string Route { get; set; } = "/jobs"; 8 | } 9 | -------------------------------------------------------------------------------- /src/Modules/Identity/Modules.Identity.Contracts/v1/Roles/GetRoles/GetRolesQuery.cs: -------------------------------------------------------------------------------- 1 | using FSH.Modules.Identity.Contracts.DTOs; 2 | using Mediator; 3 | 4 | namespace FSH.Modules.Identity.Contracts.v1.Roles.GetRoles; 5 | 6 | public sealed record GetRolesQuery : IQuery>; 7 | 8 | -------------------------------------------------------------------------------- /src/Modules/Identity/Modules.Identity.Contracts/v1/Users/ConfirmEmail/ConfirmEmailCommand.cs: -------------------------------------------------------------------------------- 1 | using Mediator; 2 | 3 | namespace FSH.Modules.Identity.Contracts.v1.Users.ConfirmEmail; 4 | 5 | public sealed record ConfirmEmailCommand(string UserId, string Code, string Tenant) : ICommand; 6 | 7 | -------------------------------------------------------------------------------- /src/Modules/Multitenancy/Modules.Multitenancy/Provisioning/TenantProvisioningStepName.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.Modules.Multitenancy.Provisioning; 2 | 3 | public enum TenantProvisioningStepName 4 | { 5 | Database = 1, 6 | Migrations = 2, 7 | Seeding = 3, 8 | CacheWarm = 4 9 | } 10 | -------------------------------------------------------------------------------- /src/BuildingBlocks/Core/Domain/IAuditableEntity.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.Framework.Core.Domain; 2 | public interface IAuditableEntity 3 | { 4 | DateTimeOffset CreatedOnUtc { get; } 5 | string? CreatedBy { get; } 6 | DateTimeOffset? LastModifiedOnUtc { get; } 7 | string? LastModifiedBy { get; } 8 | } -------------------------------------------------------------------------------- /src/Modules/Auditing/Modules.Auditing.Contracts/v1/GetAuditById/GetAuditByIdQuery.cs: -------------------------------------------------------------------------------- 1 | using FSH.Modules.Auditing.Contracts.Dtos; 2 | using Mediator; 3 | 4 | namespace FSH.Modules.Auditing.Contracts.v1.GetAuditById; 5 | 6 | public sealed record GetAuditByIdQuery(Guid Id) : IQuery; 7 | 8 | -------------------------------------------------------------------------------- /src/Modules/Identity/Modules.Identity.Contracts/v1/Sessions/RevokeAllSessions/RevokeAllSessionsCommand.cs: -------------------------------------------------------------------------------- 1 | using Mediator; 2 | 3 | namespace FSH.Modules.Identity.Contracts.v1.Sessions.RevokeAllSessions; 4 | 5 | public sealed record RevokeAllSessionsCommand(Guid? ExceptSessionId = null) : ICommand; 6 | -------------------------------------------------------------------------------- /src/Playground/Playground.Blazor/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | }, 8 | "AllowedHosts": "*", 9 | "Api": { 10 | "BaseUrl": "https://localhost:7030" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/BuildingBlocks/Storage/DTOs/FileUploadRequest.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.Framework.Storage.DTOs; 2 | 3 | public class FileUploadRequest 4 | { 5 | public string FileName { get; set; } = default!; 6 | public string ContentType { get; set; } = default!; 7 | public List Data { get; set; } = []; 8 | } -------------------------------------------------------------------------------- /src/Modules/Identity/Modules.Identity.Contracts/v1/Tokens/RefreshToken/RefreshTokenCommand.cs: -------------------------------------------------------------------------------- 1 | using Mediator; 2 | 3 | namespace FSH.Modules.Identity.Contracts.v1.Tokens.RefreshToken; 4 | 5 | public record RefreshTokenCommand(string Token, string RefreshToken) 6 | : ICommand; -------------------------------------------------------------------------------- /src/Modules/Multitenancy/Modules.Multitenancy.Contracts/v1/CreateTenant/CreateTenantCommandResponse.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.Modules.Multitenancy.Contracts.v1.CreateTenant; 2 | 3 | public sealed record CreateTenantCommandResponse( 4 | string Id, 5 | string ProvisioningCorrelationId, 6 | string Status); 7 | -------------------------------------------------------------------------------- /src/Playground/Playground.Blazor/appsettings.Production.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | }, 8 | "AllowedHosts": "ui.example.com", 9 | "Api": { 10 | "BaseUrl": "" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/Modules/Identity/Modules.Identity.Contracts/v1/Tokens/RefreshToken/RefreshTokenCommandResponse.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.Modules.Identity.Contracts.v1.Tokens.RefreshToken; 2 | 3 | public sealed record RefreshTokenCommandResponse( 4 | string Token, 5 | string RefreshToken, 6 | DateTime RefreshTokenExpiryTime); -------------------------------------------------------------------------------- /src/Modules/Identity/Modules.Identity.Contracts/v1/Users/GetUserPermissions/GetCurrentUserPermissionsQuery.cs: -------------------------------------------------------------------------------- 1 | using Mediator; 2 | 3 | namespace FSH.Modules.Identity.Contracts.v1.Users.GetUserPermissions; 4 | 5 | public sealed record GetCurrentUserPermissionsQuery(string UserId) : IQuery?>; 6 | 7 | -------------------------------------------------------------------------------- /src/Modules/Multitenancy/Modules.Multitenancy.Contracts/v1/GetTenantTheme/GetTenantThemeQuery.cs: -------------------------------------------------------------------------------- 1 | using FSH.Modules.Multitenancy.Contracts.Dtos; 2 | using Mediator; 3 | 4 | namespace FSH.Modules.Multitenancy.Contracts.v1.GetTenantTheme; 5 | 6 | public sealed record GetTenantThemeQuery : IQuery; 7 | -------------------------------------------------------------------------------- /src/Modules/Identity/Modules.Identity.Contracts/v1/Sessions/GetMySessions/GetMySessionsQuery.cs: -------------------------------------------------------------------------------- 1 | using FSH.Modules.Identity.Contracts.DTOs; 2 | using Mediator; 3 | 4 | namespace FSH.Modules.Identity.Contracts.v1.Sessions.GetMySessions; 5 | 6 | public sealed record GetMySessionsQuery : IQuery>; 7 | -------------------------------------------------------------------------------- /src/Modules/Identity/Modules.Identity.Contracts/v1/Users/ForgotPassword/ForgotPasswordCommand.cs: -------------------------------------------------------------------------------- 1 | using Mediator; 2 | 3 | namespace FSH.Modules.Identity.Contracts.v1.Users.ForgotPassword; 4 | 5 | public class ForgotPasswordCommand : ICommand 6 | { 7 | public string Email { get; set; } = default!; 8 | } 9 | -------------------------------------------------------------------------------- /src/Modules/Identity/Modules.Identity.Contracts/v1/Users/GetUserRoles/GetUserRolesQuery.cs: -------------------------------------------------------------------------------- 1 | using FSH.Modules.Identity.Contracts.DTOs; 2 | using Mediator; 3 | 4 | namespace FSH.Modules.Identity.Contracts.v1.Users.GetUserRoles; 5 | 6 | public sealed record GetUserRolesQuery(string UserId) : IQuery>; 7 | 8 | -------------------------------------------------------------------------------- /src/Modules/Multitenancy/Modules.Multitenancy.Contracts/v1/UpgradeTenant/UpgradeTenantCommand.cs: -------------------------------------------------------------------------------- 1 | using Mediator; 2 | 3 | namespace FSH.Modules.Multitenancy.Contracts.v1.UpgradeTenant; 4 | 5 | public sealed record UpgradeTenantCommand(string Tenant, DateTime ExtendedExpiryDate) 6 | : ICommand; -------------------------------------------------------------------------------- /src/Modules/Identity/Modules.Identity.Contracts/v1/Sessions/AdminRevokeAllSessions/AdminRevokeAllSessionsCommand.cs: -------------------------------------------------------------------------------- 1 | using Mediator; 2 | 3 | namespace FSH.Modules.Identity.Contracts.v1.Sessions.AdminRevokeAllSessions; 4 | 5 | public sealed record AdminRevokeAllSessionsCommand(Guid UserId, string? Reason = null) : ICommand; 6 | -------------------------------------------------------------------------------- /src/Modules/Identity/Modules.Identity.Contracts/v1/Sessions/AdminRevokeSession/AdminRevokeSessionCommand.cs: -------------------------------------------------------------------------------- 1 | using Mediator; 2 | 3 | namespace FSH.Modules.Identity.Contracts.v1.Sessions.AdminRevokeSession; 4 | 5 | public sealed record AdminRevokeSessionCommand(Guid UserId, Guid SessionId, string? Reason = null) : ICommand; 6 | -------------------------------------------------------------------------------- /src/Modules/Identity/Modules.Identity.Contracts/DTOs/UserRoleDto.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.Modules.Identity.Contracts.DTOs; 2 | 3 | public class UserRoleDto 4 | { 5 | public string? RoleId { get; set; } 6 | public string? RoleName { get; set; } 7 | public string? Description { get; set; } 8 | public bool Enabled { get; set; } 9 | } -------------------------------------------------------------------------------- /src/Modules/Identity/Modules.Identity.Contracts/v1/Sessions/GetUserSessions/GetUserSessionsQuery.cs: -------------------------------------------------------------------------------- 1 | using FSH.Modules.Identity.Contracts.DTOs; 2 | using Mediator; 3 | 4 | namespace FSH.Modules.Identity.Contracts.v1.Sessions.GetUserSessions; 5 | 6 | public sealed record GetUserSessionsQuery(Guid UserId) : IQuery>; 7 | -------------------------------------------------------------------------------- /src/Modules/Identity/Modules.Identity.Contracts/v1/Users/GetUserProfile/GetCurrentUserProfileQuery.cs: -------------------------------------------------------------------------------- 1 | using FSH.Modules.Identity.Contracts.DTOs; 2 | using Mediator; 3 | 4 | namespace FSH.Modules.Identity.Contracts.v1.Users.GetUserProfile; 5 | 6 | public sealed record GetCurrentUserProfileQuery(string UserId) : IQuery; 7 | 8 | -------------------------------------------------------------------------------- /src/Modules/Multitenancy/Modules.Multitenancy.Contracts/v1/GetTenantStatus/GetTenantStatusQuery.cs: -------------------------------------------------------------------------------- 1 | using FSH.Modules.Multitenancy.Contracts.Dtos; 2 | using Mediator; 3 | 4 | namespace FSH.Modules.Multitenancy.Contracts.v1.GetTenantStatus; 5 | 6 | public sealed record GetTenantStatusQuery(string TenantId) : IQuery; 7 | 8 | -------------------------------------------------------------------------------- /src/Modules/Multitenancy/Modules.Multitenancy.Contracts/v1/UpdateTenantTheme/UpdateTenantThemeCommand.cs: -------------------------------------------------------------------------------- 1 | using FSH.Modules.Multitenancy.Contracts.Dtos; 2 | using Mediator; 3 | 4 | namespace FSH.Modules.Multitenancy.Contracts.v1.UpdateTenantTheme; 5 | 6 | public sealed record UpdateTenantThemeCommand(TenantThemeDto Theme) : ICommand; 7 | -------------------------------------------------------------------------------- /src/Playground/Playground.Api/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | }, 8 | "CorsOptions": { 9 | "AllowAll": true 10 | }, 11 | "OpenApiOptions": { 12 | "Enabled": true 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/BuildingBlocks/Web/RateLimiting/FixedWindowPolicyOptions.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.Framework.Web.RateLimiting; 2 | 3 | public sealed class FixedWindowPolicyOptions 4 | { 5 | public int PermitLimit { get; set; } = 100; 6 | public int WindowSeconds { get; set; } = 60; 7 | public int QueueLimit { get; set; } = 0; 8 | } 9 | 10 | -------------------------------------------------------------------------------- /src/Modules/Auditing/Modules.Auditing.Contracts/IAuditMaskingService.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.Modules.Auditing.Contracts; 2 | 3 | /// 4 | /// Masks or hashes sensitive fields before persistence or externalization. 5 | /// 6 | public interface IAuditMaskingService 7 | { 8 | object ApplyMasking(object payload); 9 | } 10 | -------------------------------------------------------------------------------- /src/Modules/Identity/Modules.Identity.Contracts/v1/Roles/GetRoleWithPermissions/GetRoleWithPermissionsQuery.cs: -------------------------------------------------------------------------------- 1 | using FSH.Modules.Identity.Contracts.DTOs; 2 | using Mediator; 3 | 4 | namespace FSH.Modules.Identity.Contracts.v1.Roles.GetRoleWithPermissions; 5 | 6 | public sealed record GetRoleWithPermissionsQuery(string Id) : IQuery; 7 | 8 | -------------------------------------------------------------------------------- /terraform/modules/rds_postgres/versions.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 1.14.0" 3 | 4 | required_providers { 5 | aws = { 6 | source = "hashicorp/aws" 7 | version = ">= 5.80.0" 8 | } 9 | random = { 10 | source = "hashicorp/random" 11 | version = ">= 3.6.0" 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Modules/Identity/Modules.Identity/Features/v1/RoleClaims/FshRoleClaim.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Identity; 2 | 3 | namespace FSH.Modules.Identity.Features.v1.RoleClaims; 4 | 5 | public class FshRoleClaim : IdentityRoleClaim 6 | { 7 | public string? CreatedBy { get; init; } 8 | public DateTimeOffset CreatedOn { get; init; } 9 | } -------------------------------------------------------------------------------- /src/Playground/Playground.Api/Requests/Identity/identity-token.http: -------------------------------------------------------------------------------- 1 | @baseUrl = https://localhost:7030 2 | @tenant = root 3 | 4 | ### Issue JWT token 5 | POST {{baseUrl}}/api/v1/identity/token 6 | Content-Type: application/json 7 | tenant: {{tenant}} 8 | 9 | { 10 | "email": "admin@root.com", 11 | "password": "123Pa$$word!" 12 | } 13 | 14 | -------------------------------------------------------------------------------- /src/BuildingBlocks/Core/Abstractions/IAppUser.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.Framework.Core.Abstractions; 2 | public interface IAppUser 3 | { 4 | string? FirstName { get; } 5 | string? LastName { get; } 6 | Uri? ImageUrl { get; } 7 | bool IsActive { get; } 8 | string? RefreshToken { get; } 9 | DateTime RefreshTokenExpiryTime { get; } 10 | } -------------------------------------------------------------------------------- /src/Modules/Auditing/Modules.Auditing.Contracts/IAuditSerializer.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.Modules.Auditing.Contracts; 2 | 3 | /// 4 | /// Deterministic JSON serialization for payloads (camelCase, enum-as-string, stable output). 5 | /// 6 | public interface IAuditSerializer 7 | { 8 | string SerializePayload(object payload); 9 | } 10 | -------------------------------------------------------------------------------- /src/Modules/Identity/Modules.Identity.Contracts/v1/Tokens/TokenGeneration/GenerateTokenCommand.cs: -------------------------------------------------------------------------------- 1 | using FSH.Modules.Identity.Contracts.DTOs; 2 | using Mediator; 3 | 4 | namespace FSH.Modules.Identity.Contracts.v1.Tokens.TokenGeneration; 5 | 6 | public record GenerateTokenCommand( 7 | string Email, 8 | string Password) 9 | : ICommand; -------------------------------------------------------------------------------- /src/BuildingBlocks/Web/Cors/CorsOptions.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.Framework.Web.Cors; 2 | 3 | public sealed class CorsOptions 4 | { 5 | public bool AllowAll { get; init; } = true; 6 | public string[] AllowedOrigins { get; init; } = []; 7 | public string[] AllowedHeaders { get; init; } = ["*"]; 8 | public string[] AllowedMethods { get; init; } = ["*"]; 9 | } -------------------------------------------------------------------------------- /src/BuildingBlocks/Blazor.UI/Components/Layouts/FshDrawerHeader.razor: -------------------------------------------------------------------------------- 1 | @inherits FshComponentBase 2 | 3 | 4 | 5 | @Title 6 | 7 | 8 | 9 | @code { 10 | [Parameter] 11 | public string Title { get; set; } = "Fsh Playground"; 12 | } 13 | -------------------------------------------------------------------------------- /src/Modules/Identity/Modules.Identity.Contracts/v1/Users/ToggleUserStatus/ToggleUserStatusCommand.cs: -------------------------------------------------------------------------------- 1 | using Mediator; 2 | 3 | namespace FSH.Modules.Identity.Contracts.v1.Users.ToggleUserStatus; 4 | 5 | public class ToggleUserStatusCommand : ICommand 6 | { 7 | public bool ActivateUser { get; set; } 8 | public string? UserId { get; set; } 9 | } 10 | -------------------------------------------------------------------------------- /src/Modules/Multitenancy/Modules.Multitenancy.Contracts/v1/GetTenantMigrations/GetTenantMigrationsQuery.cs: -------------------------------------------------------------------------------- 1 | using FSH.Modules.Multitenancy.Contracts.Dtos; 2 | using Mediator; 3 | 4 | namespace FSH.Modules.Multitenancy.Contracts.v1.GetTenantMigrations; 5 | 6 | public sealed record GetTenantMigrationsQuery : IQuery>; 7 | 8 | -------------------------------------------------------------------------------- /src/BuildingBlocks/Blazor.UI/Components/FshHead.razor: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/Modules/Auditing/Modules.Auditing.Contracts/IAuditMutatingEnricher.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.Modules.Auditing.Contracts; 2 | 3 | /// 4 | /// Enricher that can return a modified event (e.g., fill missing fields, mask payload). 5 | /// 6 | public interface IAuditMutatingEnricher 7 | { 8 | AuditEnvelope Enrich(AuditEnvelope envelope); 9 | } 10 | -------------------------------------------------------------------------------- /src/Modules/Identity/Modules.Identity.Contracts/DTOs/RoleDto.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.Modules.Identity.Contracts.DTOs; 2 | 3 | public class RoleDto 4 | { 5 | public string Id { get; set; } = default!; 6 | public string Name { get; set; } = default!; 7 | public string? Description { get; set; } 8 | public IReadOnlyCollection? Permissions { get; set; } 9 | } -------------------------------------------------------------------------------- /src/Modules/Multitenancy/Modules.Multitenancy.Contracts/v1/TenantProvisioning/RetryTenantProvisioningCommand.cs: -------------------------------------------------------------------------------- 1 | using FSH.Modules.Multitenancy.Contracts.Dtos; 2 | using Mediator; 3 | 4 | namespace FSH.Modules.Multitenancy.Contracts.v1.TenantProvisioning; 5 | 6 | public sealed record RetryTenantProvisioningCommand(string TenantId) : ICommand; 7 | -------------------------------------------------------------------------------- /src/Modules/Multitenancy/Modules.Multitenancy.Contracts/v1/TenantProvisioning/GetTenantProvisioningStatusQuery.cs: -------------------------------------------------------------------------------- 1 | using FSH.Modules.Multitenancy.Contracts.Dtos; 2 | using Mediator; 3 | 4 | namespace FSH.Modules.Multitenancy.Contracts.v1.TenantProvisioning; 5 | 6 | public sealed record GetTenantProvisioningStatusQuery(string TenantId) : IQuery; 7 | -------------------------------------------------------------------------------- /src/Playground/Playground.Api/Requests/root-and-health.http: -------------------------------------------------------------------------------- 1 | @baseUrl = https://localhost:7030 2 | 3 | ### Root hello 4 | GET {{baseUrl}}/ 5 | Accept: application/json 6 | 7 | ### Liveness probe 8 | GET {{baseUrl}}/health/live 9 | Accept: application/json 10 | 11 | ### Readiness probe (includes DB checks) 12 | GET {{baseUrl}}/health/ready 13 | Accept: application/json 14 | 15 | -------------------------------------------------------------------------------- /src/BuildingBlocks/Storage/S3/S3StorageOptions.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.Framework.Storage.S3; 2 | 3 | public sealed class S3StorageOptions 4 | { 5 | public string? Bucket { get; set; } 6 | public string? Region { get; set; } 7 | public string? Prefix { get; set; } 8 | public bool PublicRead { get; set; } = true; 9 | public string? PublicBaseUrl { get; set; } 10 | } 11 | -------------------------------------------------------------------------------- /src/BuildingBlocks/Blazor.UI/_Imports.razor: -------------------------------------------------------------------------------- 1 | @using Microsoft.AspNetCore.Components.Web 2 | @using Microsoft.AspNetCore.Components.Forms 3 | @using MudBlazor 4 | @using FSH.Framework.Blazor.UI.Components.Base 5 | @using FSH.Framework.Blazor.UI.Theme 6 | @using FSH.Framework.Blazor.UI.Components.Theme 7 | @using FSH.Framework.Blazor.UI.Components.Page 8 | @using FSH.Framework.Blazor.UI.Components.User -------------------------------------------------------------------------------- /src/Modules/Auditing/Modules.Auditing.Contracts/IAuditSink.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.Modules.Auditing.Contracts; 2 | 3 | /// 4 | /// Destination for audit events (e.g., SQL, file, OTLP). Implementations must be efficient and batch-friendly. 5 | /// 6 | public interface IAuditSink 7 | { 8 | Task WriteAsync(IReadOnlyList batch, CancellationToken ct); 9 | } 10 | -------------------------------------------------------------------------------- /src/Modules/Multitenancy/Modules.Multitenancy.Contracts/v1/ChangeTenantActivation/ChangeTenantActivationCommand.cs: -------------------------------------------------------------------------------- 1 | using FSH.Modules.Multitenancy.Contracts.Dtos; 2 | using Mediator; 3 | 4 | namespace FSH.Modules.Multitenancy.Contracts.v1.ChangeTenantActivation; 5 | 6 | public sealed record ChangeTenantActivationCommand(string TenantId, bool IsActive) 7 | : ICommand; -------------------------------------------------------------------------------- /src/Modules/Multitenancy/Modules.Multitenancy.Contracts/v1/CreateTenant/CreateTenantCommand.cs: -------------------------------------------------------------------------------- 1 | using Mediator; 2 | 3 | namespace FSH.Modules.Multitenancy.Contracts.v1.CreateTenant; 4 | 5 | public sealed record CreateTenantCommand( 6 | string Id, 7 | string Name, 8 | string? ConnectionString, 9 | string AdminEmail, 10 | string? Issuer) : ICommand; -------------------------------------------------------------------------------- /src/BuildingBlocks/Mailing/MailOptions.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.Framework.Mailing; 2 | 3 | public class MailOptions 4 | { 5 | public string? From { get; set; } 6 | 7 | public string? Host { get; set; } 8 | 9 | public int Port { get; set; } 10 | 11 | public string? UserName { get; set; } 12 | 13 | public string? Password { get; set; } 14 | 15 | public string? DisplayName { get; set; } 16 | } -------------------------------------------------------------------------------- /terraform/modules/ecs_cluster/outputs.tf: -------------------------------------------------------------------------------- 1 | output "id" { 2 | description = "The ID of the ECS cluster" 3 | value = aws_ecs_cluster.this.id 4 | } 5 | 6 | output "arn" { 7 | description = "The ARN of the ECS cluster" 8 | value = aws_ecs_cluster.this.arn 9 | } 10 | 11 | output "name" { 12 | description = "The name of the ECS cluster" 13 | value = aws_ecs_cluster.this.name 14 | } 15 | -------------------------------------------------------------------------------- /src/BuildingBlocks/Web/RateLimiting/RateLimitingOptions.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.Framework.Web.RateLimiting; 2 | 3 | public sealed class RateLimitingOptions 4 | { 5 | public bool Enabled { get; set; } = true; 6 | public FixedWindowPolicyOptions Global { get; set; } = new(); 7 | public FixedWindowPolicyOptions Auth { get; set; } = new() { PermitLimit = 10, WindowSeconds = 60, QueueLimit = 0 }; 8 | } 9 | 10 | -------------------------------------------------------------------------------- /terraform/apps/playground/envs/dev/us-east-1/backend.tf: -------------------------------------------------------------------------------- 1 | # Terraform 1.10+ uses S3 native locking via use_lockfile. 2 | # DynamoDB is no longer required for state locking. 3 | terraform { 4 | backend "s3" { 5 | bucket = "fsh-state-bucket" 6 | key = "dev/us-east-1/terraform.tfstate" 7 | region = "us-east-1" 8 | encrypt = true 9 | use_lockfile = true 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/BuildingBlocks/Blazor.UI/Components/Navigation/FshBreadcrumbs.razor: -------------------------------------------------------------------------------- 1 | 2 | 3 | @code { 4 | [Parameter] public IEnumerable Items { get; set; } = Array.Empty(); 5 | [Parameter] public int MaxItems { get; set; } = 4; 6 | 7 | private IReadOnlyList GetItemsList() => Items?.ToList() ?? []; 8 | } 9 | -------------------------------------------------------------------------------- /src/Modules/Auditing/Modules.Auditing.Contracts/ExceptionEventPayload.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.Modules.Auditing.Contracts; 2 | 3 | public sealed record ExceptionEventPayload( 4 | ExceptionArea Area, 5 | string ExceptionType, 6 | string Message, 7 | IReadOnlyList StackTop, // capped frames 8 | IReadOnlyDictionary? Data, 9 | string? RouteOrLocation 10 | ); 11 | -------------------------------------------------------------------------------- /terraform/apps/playground/README.md: -------------------------------------------------------------------------------- 1 | # Playground App Stack 2 | Terraform stack for the Playground API/Blazor app. Uses shared modules from `../../modules`. 3 | 4 | - Env/region stacks live under `envs///` (backend.tf + *.tfvars + main.tf). 5 | - App composition lives under `app_stack/` (wiring ECS services, ALB, RDS, Redis, S3). 6 | - Images are built from GitHub Actions, pushed to ECR, and referenced in tfvars. 7 | -------------------------------------------------------------------------------- /terraform/apps/playground/envs/prod/us-east-1/backend.tf: -------------------------------------------------------------------------------- 1 | # Terraform 1.10+ uses S3 native locking via use_lockfile. 2 | # DynamoDB is no longer required for state locking. 3 | terraform { 4 | backend "s3" { 5 | bucket = "fsh-state-bucket" 6 | key = "prod/us-east-1/terraform.tfstate" 7 | region = "us-east-1" 8 | encrypt = true 9 | use_lockfile = true 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /terraform/apps/playground/envs/staging/us-east-1/backend.tf: -------------------------------------------------------------------------------- 1 | # Terraform 1.10+ uses S3 native locking via use_lockfile. 2 | # DynamoDB is no longer required for state locking. 3 | terraform { 4 | backend "s3" { 5 | bucket = "fsh-state-bucket" 6 | key = "staging/us-east-1/terraform.tfstate" 7 | region = "us-east-1" 8 | encrypt = true 9 | use_lockfile = true 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/BuildingBlocks/Core/Context/ICurrentUser.cs: -------------------------------------------------------------------------------- 1 | using System.Security.Claims; 2 | 3 | namespace FSH.Framework.Core.Context; 4 | public interface ICurrentUser 5 | { 6 | string? Name { get; } 7 | 8 | Guid GetUserId(); 9 | 10 | string? GetUserEmail(); 11 | 12 | string? GetTenant(); 13 | 14 | bool IsAuthenticated(); 15 | 16 | bool IsInRole(string role); 17 | 18 | IEnumerable? GetUserClaims(); 19 | } -------------------------------------------------------------------------------- /src/BuildingBlocks/Eventing/Abstractions/IEventSerializer.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.Framework.Eventing.Abstractions; 2 | 3 | /// 4 | /// Serializes and deserializes integration events for transport and storage (outbox). 5 | /// 6 | public interface IEventSerializer 7 | { 8 | string Serialize(IIntegrationEvent @event); 9 | 10 | IIntegrationEvent? Deserialize(string payload, string eventTypeName); 11 | } 12 | 13 | -------------------------------------------------------------------------------- /src/Modules/Multitenancy/Modules.Multitenancy.Contracts/Dtos/TenantLifecycleResultDto.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.Modules.Multitenancy.Contracts.Dtos; 2 | 3 | public sealed class TenantLifecycleResultDto 4 | { 5 | public string TenantId { get; set; } = default!; 6 | 7 | public bool IsActive { get; set; } 8 | 9 | public DateTime? ValidUpto { get; set; } 10 | 11 | public string Message { get; set; } = string.Empty; 12 | } 13 | -------------------------------------------------------------------------------- /terraform/apps/restaurantpos/README.md: -------------------------------------------------------------------------------- 1 | # RestaurantPOS App Stack (skeleton) 2 | Placeholder for a future app using shared modules from `../../modules`. 3 | 4 | - Env/region stacks will live under `envs///` (backend.tf + *.tfvars + main.tf). 5 | - App composition will live under `app_stack/` (ECS services, ALB, DB/cache/S3 as needed). 6 | - Images will come from the RestaurantPOS app Dockerfiles and be referenced in tfvars. 7 | -------------------------------------------------------------------------------- /src/BuildingBlocks/Web/Security/SecurityExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Builder; 2 | 3 | namespace FSH.Framework.Web.Security; 4 | 5 | public static class SecurityExtensions 6 | { 7 | public static IApplicationBuilder UseHeroSecurityHeaders(this IApplicationBuilder app) 8 | { 9 | ArgumentNullException.ThrowIfNull(app); 10 | return app.UseMiddleware(); 11 | } 12 | } 13 | 14 | -------------------------------------------------------------------------------- /src/Modules/Auditing/Modules.Auditing.Contracts/SecurityEventPayload.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.Modules.Auditing.Contracts; 2 | 3 | public sealed record SecurityEventPayload( 4 | SecurityAction Action, 5 | string? SubjectId, 6 | string? ClientId, 7 | string? AuthMethod, // Password, OIDC, etc. 8 | string? ReasonCode, // InvalidPassword, LockedOut, etc. 9 | IReadOnlyDictionary? ClaimsSnapshot 10 | ); 11 | -------------------------------------------------------------------------------- /src/Modules/Identity/Modules.Identity.Contracts/v1/Users/AssignUserRoles/AssignUserRolesCommand.cs: -------------------------------------------------------------------------------- 1 | using FSH.Modules.Identity.Contracts.DTOs; 2 | using Mediator; 3 | 4 | namespace FSH.Modules.Identity.Contracts.v1.Users.AssignUserRoles; 5 | 6 | public sealed class AssignUserRolesCommand : ICommand 7 | { 8 | public required string UserId { get; init; } 9 | public List UserRoles { get; init; } = new(); 10 | } -------------------------------------------------------------------------------- /src/Modules/Identity/Modules.Identity.Contracts/v1/Users/ResetPassword/ResetPasswordCommand.cs: -------------------------------------------------------------------------------- 1 | using Mediator; 2 | 3 | namespace FSH.Modules.Identity.Contracts.v1.Users.ResetPassword; 4 | 5 | public class ResetPasswordCommand : ICommand 6 | { 7 | public string Email { get; set; } = default!; 8 | 9 | public string Password { get; set; } = default!; 10 | 11 | public string Token { get; set; } = default!; 12 | } 13 | -------------------------------------------------------------------------------- /src/BuildingBlocks/Shared/Identity/CustomClaims.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.Framework.Shared.Constants; 2 | 3 | public static class CustomClaims 4 | { 5 | public const string Tenant = "tenant"; 6 | public const string Fullname = "fullName"; 7 | public const string Permission = "permission"; 8 | public const string ImageUrl = "image_url"; 9 | public const string IpAddress = "ipAddress"; 10 | public const string Expiration = "exp"; 11 | } -------------------------------------------------------------------------------- /src/Modules/Identity/Modules.Identity.Contracts/v1/Roles/UpsertRole/UpsertRoleCommand.cs: -------------------------------------------------------------------------------- 1 | using FSH.Modules.Identity.Contracts.DTOs; 2 | using Mediator; 3 | 4 | namespace FSH.Modules.Identity.Contracts.v1.Roles.UpsertRole; 5 | 6 | public class UpsertRoleCommand : ICommand 7 | { 8 | public string Id { get; set; } = default!; 9 | public string Name { get; set; } = default!; 10 | public string? Description { get; set; } 11 | } 12 | -------------------------------------------------------------------------------- /src/BuildingBlocks/Web/Modules/IModule.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Routing; 2 | using Microsoft.Extensions.Hosting; 3 | 4 | namespace FSH.Framework.Web.Modules; 5 | 6 | public interface IModule 7 | { 8 | // DI/Options/Health/etc. — don’t depend on ASP.NET types here 9 | void ConfigureServices(IHostApplicationBuilder builder); 10 | 11 | // HTTP wiring — Minimal APIs only 12 | void MapEndpoints(IEndpointRouteBuilder endpoints); 13 | } -------------------------------------------------------------------------------- /src/BuildingBlocks/Shared/Identity/ClaimConstants.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.Framework.Shared.Constants; 2 | 3 | public static class ClaimConstants 4 | { 5 | public const string Tenant = "tenant"; 6 | public const string Fullname = "fullName"; 7 | public const string Permission = "permission"; 8 | public const string ImageUrl = "image_url"; 9 | public const string IpAddress = "ipAddress"; 10 | public const string Expiration = "exp"; 11 | } 12 | -------------------------------------------------------------------------------- /src/Modules/Auditing/Modules.Auditing.Contracts/IAuditEnricher.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.Modules.Auditing.Contracts; 2 | 3 | /// 4 | /// Hook to augment events before they are published (e.g., add tenant/user/trace, normalize fields, enforce caps). 5 | /// 6 | public interface IAuditEnricher 7 | { 8 | /// Mutate/augment the event instance prior to serialization/publish. 9 | void Enrich(IAuditEvent auditEvent); 10 | } 11 | -------------------------------------------------------------------------------- /src/BuildingBlocks/Storage/Services/IStorageService.cs: -------------------------------------------------------------------------------- 1 | using FSH.Framework.Storage.DTOs; 2 | 3 | namespace FSH.Framework.Storage.Services; 4 | 5 | public interface IStorageService 6 | { 7 | Task UploadAsync( 8 | FileUploadRequest request, 9 | FileType fileType, 10 | CancellationToken cancellationToken = default) where T : class; 11 | 12 | Task RemoveAsync(string path, CancellationToken cancellationToken = default); 13 | } -------------------------------------------------------------------------------- /src/Modules/Auditing/Modules.Auditing.Contracts/PropertyChange.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.Modules.Auditing.Contracts; 2 | 3 | /// 4 | /// A single property delta for entity-change auditing. 5 | /// 6 | public sealed record PropertyChange( 7 | string Name, 8 | string? DataType, // e.g., "string", "int", "datetime" 9 | object? OldValue, 10 | object? NewValue, 11 | bool IsSensitive // true => value already masked/hashed 12 | ); 13 | -------------------------------------------------------------------------------- /src/BuildingBlocks/Eventing/Abstractions/IIntegrationEventHandler.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.Framework.Eventing.Abstractions; 2 | 3 | /// 4 | /// Handles a single integration event type. 5 | /// 6 | /// The integration event type. 7 | public interface IIntegrationEventHandler 8 | where TEvent : IIntegrationEvent 9 | { 10 | Task HandleAsync(TEvent @event, CancellationToken ct = default); 11 | } 12 | 13 | -------------------------------------------------------------------------------- /src/BuildingBlocks/Blazor.UI/Components/Base/FshComponentBase.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Components; 2 | 3 | namespace FSH.Framework.Blazor.UI.Components.Base; 4 | 5 | public abstract class FshComponentBase : ComponentBase 6 | { 7 | [Inject] protected ISnackbar Snackbar { get; set; } = default!; 8 | [Inject] protected IDialogService DialogService { get; set; } = default!; 9 | [Inject] protected NavigationManager Navigation { get; set; } = default!; 10 | } 11 | -------------------------------------------------------------------------------- /src/BuildingBlocks/Persistence/Specifications/OrderExpression.cs: -------------------------------------------------------------------------------- 1 | using System.Linq.Expressions; 2 | 3 | namespace FSH.Framework.Persistence; 4 | 5 | /// 6 | /// Normalized representation of an ordering expression for specifications. 7 | /// 8 | /// The root entity type. 9 | public sealed record OrderExpression( 10 | Expression> KeySelector, 11 | bool Descending) 12 | where T : class; 13 | 14 | -------------------------------------------------------------------------------- /src/Modules/Identity/Modules.Identity/IdentityModuleConstants.cs: -------------------------------------------------------------------------------- 1 | using FSH.Framework.Web.Modules; 2 | 3 | namespace FSH.Modules.Identity; 4 | 5 | public sealed class IdentityModuleConstants : IModuleConstants 6 | { 7 | public string ModuleId => "Identity"; 8 | 9 | public string ModuleName => "Identity"; 10 | 11 | public string ApiPrefix => "identity"; 12 | public const string SchemaName = "identity"; 13 | public const int PasswordLength = 10; 14 | } 15 | -------------------------------------------------------------------------------- /src/BuildingBlocks/Eventing/Inbox/IInboxStore.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.Framework.Eventing.Inbox; 2 | 3 | /// 4 | /// Abstraction for idempotent consumer tracking. 5 | /// 6 | public interface IInboxStore 7 | { 8 | Task HasProcessedAsync(Guid eventId, string handlerName, CancellationToken ct = default); 9 | 10 | Task MarkProcessedAsync(Guid eventId, string handlerName, string? tenantId, string eventType, CancellationToken ct = default); 11 | } 12 | 13 | -------------------------------------------------------------------------------- /src/Modules/Auditing/Modules.Auditing.Contracts/v1/GetAuditSummary/GetAuditSummaryQuery.cs: -------------------------------------------------------------------------------- 1 | using FSH.Modules.Auditing.Contracts.Dtos; 2 | using Mediator; 3 | 4 | namespace FSH.Modules.Auditing.Contracts.v1.GetAuditSummary; 5 | 6 | public sealed class GetAuditSummaryQuery : IQuery 7 | { 8 | public DateTime? FromUtc { get; init; } 9 | 10 | public DateTime? ToUtc { get; init; } 11 | 12 | public string? TenantId { get; init; } 13 | } 14 | 15 | -------------------------------------------------------------------------------- /src/Modules/Auditing/Modules.Auditing.Contracts/ExceptionSeverityClassifier.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.Modules.Auditing.Contracts; 2 | 3 | public static class ExceptionSeverityClassifier 4 | { 5 | public static AuditSeverity Classify(Exception ex) => 6 | ex switch 7 | { 8 | OperationCanceledException => AuditSeverity.Information, 9 | UnauthorizedAccessException => AuditSeverity.Warning, 10 | _ => AuditSeverity.Error 11 | }; 12 | } 13 | -------------------------------------------------------------------------------- /src/BuildingBlocks/Blazor.UI/Components/Tabs/FshTabs.razor: -------------------------------------------------------------------------------- 1 | 6 | @ChildContent 7 | 8 | 9 | @code { 10 | [Parameter] public RenderFragment? ChildContent { get; set; } 11 | [Parameter(CaptureUnmatchedValues = true)] public Dictionary? AdditionalAttributes { get; set; } 12 | } 13 | -------------------------------------------------------------------------------- /src/Modules/Identity/Modules.Identity/Features/v1/Roles/UpsertRole/UpsertRoleCommandValidator.cs: -------------------------------------------------------------------------------- 1 | using FluentValidation; 2 | using FSH.Modules.Identity.Contracts.v1.Roles.UpsertRole; 3 | 4 | namespace FSH.Modules.Identity.Features.v1.Roles.UpsertRole; 5 | 6 | public class UpsertRoleCommandValidator : AbstractValidator 7 | { 8 | public UpsertRoleCommandValidator() 9 | { 10 | RuleFor(x => x.Name).NotEmpty().WithMessage("Role name is required."); 11 | } 12 | } -------------------------------------------------------------------------------- /src/Modules/Auditing/Modules.Auditing.Contracts/EntityChangeEventPayload.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.Modules.Auditing.Contracts; 2 | 3 | public sealed record EntityChangeEventPayload( 4 | string DbContext, 5 | string? Schema, 6 | string Table, 7 | string EntityName, 8 | string Key, // unified string key (e.g., "Id:42" or "TenantId:1|UserId:42") 9 | EntityOperation Operation, 10 | IReadOnlyList Changes, 11 | string? TransactionId 12 | ); 13 | -------------------------------------------------------------------------------- /src/Modules/Auditing/Modules.Auditing.Contracts/v1/GetAuditsByTrace/GetAuditsByTraceQuery.cs: -------------------------------------------------------------------------------- 1 | using FSH.Modules.Auditing.Contracts.Dtos; 2 | using Mediator; 3 | 4 | namespace FSH.Modules.Auditing.Contracts.v1.GetAuditsByTrace; 5 | 6 | public sealed class GetAuditsByTraceQuery : IQuery> 7 | { 8 | public string TraceId { get; init; } = default!; 9 | 10 | public DateTime? FromUtc { get; init; } 11 | 12 | public DateTime? ToUtc { get; init; } 13 | } 14 | 15 | -------------------------------------------------------------------------------- /src/BuildingBlocks/Core/Core.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | FSH.Framework.Core 5 | FSH.Framework.Core 6 | FullStackHero.Framework.Core 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/Modules/Auditing/Modules.Auditing/Infrastructure/Http/HttpContextRoutingExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Http; 2 | using Microsoft.AspNetCore.Routing; 3 | 4 | namespace FSH.Modules.Auditing; 5 | 6 | internal static class HttpContextRoutingExtensions 7 | { 8 | public static string? GetRoutePattern(this HttpContext ctx) 9 | => ctx.GetEndpoint() switch 10 | { 11 | RouteEndpoint re => re.RoutePattern.RawText, 12 | _ => null 13 | }; 14 | } 15 | 16 | -------------------------------------------------------------------------------- /src/Modules/Identity/Modules.Identity/Features/v1/Users/PasswordHistory/PasswordHistory.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.Modules.Identity.Features.v1.Users.PasswordHistory; 2 | 3 | public class PasswordHistory 4 | { 5 | public int Id { get; set; } 6 | public string UserId { get; set; } = default!; 7 | public string PasswordHash { get; set; } = default!; 8 | public DateTime CreatedAt { get; set; } = DateTime.UtcNow; 9 | 10 | // Navigation property 11 | public virtual FshUser? User { get; set; } 12 | } 13 | -------------------------------------------------------------------------------- /src/Modules/Multitenancy/Modules.Multitenancy.Contracts/v1/GetTenants/GetTenantsQuery.cs: -------------------------------------------------------------------------------- 1 | using FSH.Framework.Shared.Persistence; 2 | using FSH.Modules.Multitenancy.Contracts.Dtos; 3 | using Mediator; 4 | 5 | namespace FSH.Modules.Multitenancy.Contracts.v1.GetTenants; 6 | 7 | public sealed class GetTenantsQuery : IPagedQuery, IQuery> 8 | { 9 | public int? PageNumber { get; set; } 10 | 11 | public int? PageSize { get; set; } 12 | 13 | public string? Sort { get; set; } 14 | } 15 | -------------------------------------------------------------------------------- /src/Modules/Multitenancy/Modules.Multitenancy.Contracts/Dtos/TenantDto.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.Modules.Multitenancy.Contracts.Dtos; 2 | 3 | public sealed class TenantDto 4 | { 5 | public string Id { get; set; } = default!; 6 | public string Name { get; set; } = default!; 7 | public string? ConnectionString { get; set; } 8 | public string AdminEmail { get; set; } = default!; 9 | public bool IsActive { get; set; } 10 | public DateTime ValidUpto { get; set; } 11 | public string? Issuer { get; set; } 12 | } -------------------------------------------------------------------------------- /src/Modules/Auditing/Modules.Auditing.Contracts/v1/GetAuditsByCorrelation/GetAuditsByCorrelationQuery.cs: -------------------------------------------------------------------------------- 1 | using FSH.Modules.Auditing.Contracts.Dtos; 2 | using Mediator; 3 | 4 | namespace FSH.Modules.Auditing.Contracts.v1.GetAuditsByCorrelation; 5 | 6 | public sealed class GetAuditsByCorrelationQuery : IQuery> 7 | { 8 | public string CorrelationId { get; init; } = default!; 9 | 10 | public DateTime? FromUtc { get; init; } 11 | 12 | public DateTime? ToUtc { get; init; } 13 | } 14 | 15 | -------------------------------------------------------------------------------- /src/Modules/Identity/Modules.Identity/Features/v1/Roles/FshRole.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Identity; 2 | 3 | namespace FSH.Modules.Identity.Features.v1.Roles; 4 | 5 | public class FshRole : IdentityRole 6 | { 7 | public string? Description { get; set; } 8 | 9 | public FshRole(string name, string? description = null) 10 | : base(name) 11 | { 12 | ArgumentNullException.ThrowIfNull(name); 13 | 14 | Description = description; 15 | NormalizedName = name.ToUpperInvariant(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/BuildingBlocks/Shared/Persistence/DbProviders.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.Framework.Shared.Persistence; 2 | 3 | /// 4 | /// Supported database providers for the starter kit. 5 | /// 6 | public static class DbProviders 7 | { 8 | /// 9 | /// PostgreSQL database provider. 10 | /// 11 | public const string PostgreSQL = "POSTGRESQL"; 12 | 13 | /// 14 | /// Microsoft SQL Server (MSSQL) database provider. 15 | /// 16 | public const string MSSQL = "MSSQL"; 17 | } 18 | -------------------------------------------------------------------------------- /src/BuildingBlocks/Web/Observability/Logging/Serilog/StaticLogger.cs: -------------------------------------------------------------------------------- 1 | using Serilog; 2 | using Serilog.Core; 3 | 4 | namespace FSH.Framework.Web.Observability.Logging.Serilog; 5 | 6 | public static class StaticLogger 7 | { 8 | public static void EnsureInitialized() 9 | { 10 | if (Log.Logger is not Logger) 11 | { 12 | Log.Logger = new LoggerConfiguration() 13 | .Enrich.FromLogContext() 14 | .WriteTo.Console() 15 | .CreateLogger(); 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /src/BuildingBlocks/Eventing/Abstractions/IEventBus.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.Framework.Eventing.Abstractions; 2 | 3 | /// 4 | /// Abstraction over an event bus. The initial provider is in-memory; additional providers 5 | /// can be added without changing modules that publish or handle events. 6 | /// 7 | public interface IEventBus 8 | { 9 | Task PublishAsync(IIntegrationEvent @event, CancellationToken ct = default); 10 | 11 | Task PublishAsync(IEnumerable events, CancellationToken ct = default); 12 | } 13 | 14 | -------------------------------------------------------------------------------- /src/Modules/Multitenancy/Modules.Multitenancy/Features/v1/ChangeTenantActivation/ChangeTenantActivationCommandValidator.cs: -------------------------------------------------------------------------------- 1 | using FluentValidation; 2 | using FSH.Modules.Multitenancy.Contracts.v1.ChangeTenantActivation; 3 | 4 | namespace FSH.Modules.Multitenancy.Features.v1.ChangeTenantActivation; 5 | 6 | internal sealed class ChangeTenantActivationCommandValidator : AbstractValidator 7 | { 8 | public ChangeTenantActivationCommandValidator() => 9 | RuleFor(t => t.TenantId) 10 | .NotEmpty(); 11 | } -------------------------------------------------------------------------------- /src/Playground/Playground.Blazor/Components/Pages/Counter.razor: -------------------------------------------------------------------------------- 1 | @page "/counter" 2 | 3 | Counter 4 | 5 | Counter 6 | 7 | Current count: @currentCount 8 | 9 | Click me 10 | 11 | @code { 12 | private int currentCount = 0; 13 | 14 | private void IncrementCount() 15 | { 16 | currentCount++; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/BuildingBlocks/Blazor.UI/Components/Cards/FshCard.razor: -------------------------------------------------------------------------------- 1 | 2 | @ChildContent 3 | 4 | 5 | @code { 6 | [Parameter] public string? Class { get; set; } 7 | [Parameter] public RenderFragment? ChildContent { get; set; } 8 | [Parameter(CaptureUnmatchedValues = true)] public Dictionary? AdditionalAttributes { get; set; } 9 | 10 | private string CssClass => string.IsNullOrWhiteSpace(Class) ? "fsh-card" : $"fsh-card {Class}"; 11 | } 12 | -------------------------------------------------------------------------------- /src/Modules/Multitenancy/Modules.Multitenancy.Contracts/Dtos/TenantStatusDto.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.Modules.Multitenancy.Contracts.Dtos; 2 | 3 | public sealed class TenantStatusDto 4 | { 5 | public string Id { get; init; } = default!; 6 | public string Name { get; init; } = default!; 7 | public bool IsActive { get; init; } 8 | public DateTime ValidUpto { get; init; } 9 | public bool HasConnectionString { get; init; } 10 | public string AdminEmail { get; init; } = default!; 11 | public string? Issuer { get; init; } 12 | } 13 | 14 | -------------------------------------------------------------------------------- /src/Modules/Identity/Modules.Identity/Features/v1/Users/ForgotPassword/ForgotPasswordCommandValidator.cs: -------------------------------------------------------------------------------- 1 | using FluentValidation; 2 | using FSH.Modules.Identity.Contracts.v1.Users.ForgotPassword; 3 | 4 | namespace FSH.Modules.Identity.Features.v1.Users.ForgotPassword; 5 | 6 | public class ForgotPasswordCommandValidator : AbstractValidator 7 | { 8 | public ForgotPasswordCommandValidator() 9 | { 10 | RuleFor(p => p.Email).Cascade(CascadeMode.Stop) 11 | .NotEmpty() 12 | .EmailAddress(); 13 | } 14 | } -------------------------------------------------------------------------------- /src/Modules/Multitenancy/Modules.Multitenancy/Features/v1/UpgradeTenant/UpgradeTenantCommandValidator.cs: -------------------------------------------------------------------------------- 1 | using FluentValidation; 2 | using FSH.Modules.Multitenancy.Contracts.v1.UpgradeTenant; 3 | 4 | namespace FSH.Modules.Multitenancy.Features.v1.UpgradeTenant; 5 | 6 | public sealed class UpgradeTenantCommandValidator : AbstractValidator 7 | { 8 | public UpgradeTenantCommandValidator() 9 | { 10 | RuleFor(t => t.Tenant).NotEmpty(); 11 | RuleFor(t => t.ExtendedExpiryDate).GreaterThan(DateTime.UtcNow); 12 | } 13 | } -------------------------------------------------------------------------------- /src/BuildingBlocks/Shared/Persistence/PagedResponse.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.Framework.Shared.Persistence; 2 | 3 | public sealed class PagedResponse 4 | { 5 | public IReadOnlyCollection Items { get; init; } = Array.Empty(); 6 | 7 | public int PageNumber { get; init; } 8 | 9 | public int PageSize { get; init; } 10 | 11 | public long TotalCount { get; init; } 12 | 13 | public int TotalPages { get; init; } 14 | 15 | public bool HasNext => PageNumber < TotalPages; 16 | 17 | public bool HasPrevious => PageNumber > 1; 18 | } 19 | 20 | -------------------------------------------------------------------------------- /src/Modules/Auditing/Modules.Auditing.Contracts/ActivityEventPayload.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.Modules.Auditing.Contracts; 2 | 3 | public sealed record ActivityEventPayload( 4 | ActivityKind Kind, 5 | string Name, // route template, command/query name, job id 6 | int? StatusCode, 7 | int DurationMs, 8 | BodyCapture Captured, // Request/Response/Both/None 9 | int RequestSize, 10 | int ResponseSize, 11 | object? RequestPreview, // truncated/filtered snapshot (JSON-friendly) 12 | object? ResponsePreview 13 | ); 14 | -------------------------------------------------------------------------------- /src/BuildingBlocks/Blazor.UI/Components/Display/FshBadge.razor: -------------------------------------------------------------------------------- 1 | 2 | @ChildContent 3 | 4 | 5 | @code { 6 | [Parameter] public Color Color { get; set; } = Color.Primary; 7 | [Parameter] public Variant Variant { get; set; } = Variant.Filled; 8 | [Parameter] public RenderFragment? ChildContent { get; set; } 9 | [Parameter(CaptureUnmatchedValues = true)] public Dictionary? AdditionalAttributes { get; set; } 10 | } 11 | -------------------------------------------------------------------------------- /src/Modules/Identity/Modules.Identity.Contracts/Services/ITokenService.cs: -------------------------------------------------------------------------------- 1 | using FSH.Modules.Identity.Contracts.DTOs; 2 | using System.Security.Claims; 3 | 4 | namespace FSH.Modules.Identity.Contracts.Services; 5 | 6 | public interface ITokenService 7 | { 8 | /// 9 | /// Issues a new access and refresh token for the specified subject. 10 | /// 11 | Task IssueAsync( 12 | string subject, 13 | IEnumerable claims, 14 | string? tenant = null, 15 | CancellationToken ct = default); 16 | } -------------------------------------------------------------------------------- /src/Modules/Multitenancy/Modules.Multitenancy/Data/Configurations/AppTenantInfoConfiguration.cs: -------------------------------------------------------------------------------- 1 | using FSH.Framework.Shared.Multitenancy; 2 | using Microsoft.EntityFrameworkCore; 3 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 4 | 5 | namespace FSH.Modules.Multitenancy.Data.Configurations; 6 | 7 | public class AppTenantInfoConfiguration : IEntityTypeConfiguration 8 | { 9 | public void Configure(EntityTypeBuilder builder) 10 | { 11 | builder.ToTable("Tenants", MultitenancyConstants.Schema); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Playground/Playground.Blazor/Components/Routes.razor: -------------------------------------------------------------------------------- 1 | @using FSH.Playground.Blazor.Components.Layout 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |

Sorry, there's nothing at this address.

11 |
12 |
13 |
14 | -------------------------------------------------------------------------------- /src/Modules/Identity/Modules.Identity.Contracts/v1/Roles/UpdatePermissions/UpdatePermissionsCommand.cs: -------------------------------------------------------------------------------- 1 | using Mediator; 2 | 3 | namespace FSH.Modules.Identity.Contracts.v1.Roles.UpdatePermissions; 4 | 5 | public class UpdatePermissionsCommand : ICommand 6 | { 7 | /// 8 | /// The ID of the role to update. 9 | /// 10 | public string RoleId { get; init; } = default!; 11 | 12 | /// 13 | /// The list of permissions to assign to the role. 14 | /// 15 | public List Permissions { get; init; } = []; 16 | } 17 | -------------------------------------------------------------------------------- /src/Modules/Identity/Modules.Identity/Features/v1/Users/ResetPassword/ResetPasswordCommandValidator.cs: -------------------------------------------------------------------------------- 1 | using FluentValidation; 2 | using FSH.Modules.Identity.Contracts.v1.Users.ResetPassword; 3 | 4 | namespace FSH.Modules.Identity.Features.v1.Users.ResetPassword; 5 | 6 | public class ResetPasswordCommandValidator : AbstractValidator 7 | { 8 | public ResetPasswordCommandValidator() 9 | { 10 | RuleFor(x => x.Email).NotEmpty().EmailAddress(); 11 | RuleFor(x => x.Password).NotEmpty(); 12 | RuleFor(x => x.Token).NotEmpty(); 13 | } 14 | } -------------------------------------------------------------------------------- /src/Modules/Identity/Modules.Identity/Features/v1/Roles/UpdateRolePermissions/UpdatePermissionsCommandValidator.cs: -------------------------------------------------------------------------------- 1 | using FluentValidation; 2 | using FSH.Modules.Identity.Contracts.v1.Roles.UpdatePermissions; 3 | 4 | namespace FSH.Modules.Identity.Features.v1.Roles.UpdateRolePermissions; 5 | 6 | public class UpdatePermissionsCommandValidator : AbstractValidator 7 | { 8 | public UpdatePermissionsCommandValidator() 9 | { 10 | RuleFor(r => r.RoleId) 11 | .NotEmpty(); 12 | RuleFor(r => r.Permissions) 13 | .NotNull(); 14 | } 15 | } -------------------------------------------------------------------------------- /src/BuildingBlocks/Shared/Identity/ResourceConstants.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.Framework.Shared.Constants; 2 | public static class ResourceConstants 3 | { 4 | public const string Tenants = nameof(Tenants); 5 | public const string Dashboard = nameof(Dashboard); 6 | public const string Hangfire = nameof(Hangfire); 7 | public const string Users = nameof(Users); 8 | public const string UserRoles = nameof(UserRoles); 9 | public const string Roles = nameof(Roles); 10 | public const string RoleClaims = nameof(RoleClaims); 11 | public const string AuditTrails = nameof(AuditTrails); 12 | } 13 | -------------------------------------------------------------------------------- /src/Modules/Identity/Modules.Identity.Contracts/DTOs/UserDto.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.Modules.Identity.Contracts.DTOs; 2 | public class UserDto 3 | { 4 | public string? Id { get; set; } 5 | 6 | public string? UserName { get; set; } 7 | 8 | public string? FirstName { get; set; } 9 | 10 | public string? LastName { get; set; } 11 | 12 | public string? Email { get; set; } 13 | 14 | public bool IsActive { get; set; } = true; 15 | 16 | public bool EmailConfirmed { get; set; } 17 | 18 | public string? PhoneNumber { get; set; } 19 | 20 | public string? ImageUrl { get; set; } 21 | } -------------------------------------------------------------------------------- /src/BuildingBlocks/Core/Domain/DomainEvent.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.Framework.Core.Domain; 2 | /// Base domain event with correlation and tenant context. 3 | public abstract record DomainEvent( 4 | Guid EventId, 5 | DateTimeOffset OccurredOnUtc, 6 | string? CorrelationId = null, 7 | string? TenantId = null 8 | ) : IDomainEvent 9 | { 10 | public static T Create(Func factory) 11 | where T : DomainEvent 12 | { 13 | ArgumentNullException.ThrowIfNull(factory); 14 | return factory(Guid.NewGuid(), DateTimeOffset.UtcNow); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Modules/Identity/Modules.Identity.Contracts/Events/UserRegisteredIntegrationEvent.cs: -------------------------------------------------------------------------------- 1 | using FSH.Framework.Eventing.Abstractions; 2 | 3 | namespace FSH.Modules.Identity.Contracts.Events; 4 | 5 | /// 6 | /// Integration event raised when a new user is registered. 7 | /// 8 | public sealed record UserRegisteredIntegrationEvent( 9 | Guid Id, 10 | DateTime OccurredOnUtc, 11 | string? TenantId, 12 | string CorrelationId, 13 | string Source, 14 | string UserId, 15 | string Email, 16 | string FirstName, 17 | string LastName) 18 | : IIntegrationEvent; 19 | 20 | -------------------------------------------------------------------------------- /src/BuildingBlocks/Shared/Identity/Authorization/EndpointExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Builder; 2 | 3 | namespace FSH.Framework.Shared.Identity.Authorization; 4 | 5 | public static class EndpointExtensions 6 | { 7 | public static TBuilder RequirePermission( 8 | this TBuilder endpointConventionBuilder, string requiredPermission, params string[] additionalRequiredPermissions) 9 | where TBuilder : IEndpointConventionBuilder 10 | { 11 | return endpointConventionBuilder.WithMetadata(new RequiredPermissionAttribute(requiredPermission, additionalRequiredPermissions)); 12 | } 13 | } -------------------------------------------------------------------------------- /src/Modules/Auditing/Modules.Auditing.Contracts/Modules.Auditing.Contracts.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | FSH.Modules.Auditing.Contracts 5 | FSH.Modules.Auditing.Contracts 6 | FullStackHero.Modules.Auditing.Contracts 7 | $(NoWarn);S2094 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/Modules/Identity/Modules.Identity.Contracts/v1/Users/UpdateUser/UpdateUserCommand.cs: -------------------------------------------------------------------------------- 1 | using FSH.Framework.Storage.DTOs; 2 | using Mediator; 3 | 4 | namespace FSH.Modules.Identity.Contracts.v1.Users.UpdateUser; 5 | 6 | public class UpdateUserCommand : ICommand 7 | { 8 | public string Id { get; set; } = default!; 9 | public string? FirstName { get; set; } 10 | public string? LastName { get; set; } 11 | public string? PhoneNumber { get; set; } 12 | public string? Email { get; set; } 13 | public FileUploadRequest? Image { get; set; } 14 | public bool DeleteCurrentImage { get; set; } 15 | } 16 | -------------------------------------------------------------------------------- /src/Modules/Auditing/Modules.Auditing/Modules.Auditing.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | FSH.Modules.Auditing 5 | FSH.Modules.Auditing 6 | FullStackHero.Modules.Auditing 7 | $(NoWarn);CA1031;CA1812;CA1859;S3267 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/BuildingBlocks/Caching/ICacheService.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.Framework.Caching; 2 | public interface ICacheService 3 | { 4 | Task GetItemAsync(string key, CancellationToken ct = default); 5 | Task SetItemAsync(string key, T value, TimeSpan? sliding = default, CancellationToken ct = default); 6 | Task RemoveItemAsync(string key, CancellationToken ct = default); 7 | Task RefreshItemAsync(string key, CancellationToken ct = default); 8 | T? GetItem(string key); 9 | void SetItem(string key, T value, TimeSpan? sliding = default); 10 | void RemoveItem(string key); 11 | void RefreshItem(string key); 12 | } -------------------------------------------------------------------------------- /src/Modules/Auditing/Modules.Auditing/Features/v1/GetAuditSummary/GetAuditSummaryQueryValidator.cs: -------------------------------------------------------------------------------- 1 | using FluentValidation; 2 | using FSH.Modules.Auditing.Contracts.v1.GetAuditSummary; 3 | 4 | namespace FSH.Modules.Auditing.Features.v1.GetAuditSummary; 5 | 6 | public sealed class GetAuditSummaryQueryValidator : AbstractValidator 7 | { 8 | public GetAuditSummaryQueryValidator() 9 | { 10 | RuleFor(q => q) 11 | .Must(q => !q.FromUtc.HasValue || !q.ToUtc.HasValue || q.FromUtc <= q.ToUtc) 12 | .WithMessage("FromUtc must be less than or equal to ToUtc."); 13 | } 14 | } 15 | 16 | -------------------------------------------------------------------------------- /src/BuildingBlocks/Core/Domain/BaseEntity.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.Framework.Core.Domain; 2 | public abstract class BaseEntity : IEntity, IHasDomainEvents 3 | { 4 | private readonly List _domainEvents = []; 5 | 6 | public TId Id { get; protected set; } = default!; 7 | 8 | public IReadOnlyCollection DomainEvents => _domainEvents; 9 | 10 | /// Raise and record a domain event for later dispatch. 11 | protected void AddDomainEvent(IDomainEvent @event) 12 | => _domainEvents.Add(@event); 13 | 14 | public void ClearDomainEvents() => _domainEvents.Clear(); 15 | } 16 | -------------------------------------------------------------------------------- /src/Modules/Auditing/Modules.Auditing.Contracts/IAuditPublisher.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.Modules.Auditing.Contracts; 2 | 3 | /// 4 | /// Low-latency, non-blocking publisher. Implement with a bounded channel + background worker. 5 | /// 6 | public interface IAuditPublisher 7 | { 8 | /// Publish an audit event. Implementations should avoid blocking the request path. 9 | ValueTask PublishAsync(IAuditEvent auditEvent, CancellationToken ct = default); 10 | 11 | /// Ambient scope for the current operation (usually request-scoped). 12 | IAuditScope CurrentScope { get; } 13 | } 14 | -------------------------------------------------------------------------------- /src/Modules/Identity/Modules.Identity.Contracts/Services/IRoleService.cs: -------------------------------------------------------------------------------- 1 | using FSH.Modules.Identity.Contracts.DTOs; 2 | 3 | namespace FSH.Modules.Identity.Contracts.Services; 4 | 5 | public interface IRoleService 6 | { 7 | Task> GetRolesAsync(); 8 | Task GetRoleAsync(string id); 9 | Task CreateOrUpdateRoleAsync(string roleId, string name, string description); 10 | Task DeleteRoleAsync(string id); 11 | Task GetWithPermissionsAsync(string id, CancellationToken cancellationToken); 12 | 13 | Task UpdatePermissionsAsync(string roleId, List permissions); 14 | } -------------------------------------------------------------------------------- /src/BuildingBlocks/Shared/Identity/ActionConstants.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.Framework.Shared.Constants; 2 | public static class ActionConstants 3 | { 4 | public const string View = nameof(View); 5 | public const string Search = nameof(Search); 6 | public const string Create = nameof(Create); 7 | public const string Update = nameof(Update); 8 | public const string Delete = nameof(Delete); 9 | public const string Export = nameof(Export); 10 | public const string Generate = nameof(Generate); 11 | public const string Clean = nameof(Clean); 12 | public const string UpgradeSubscription = nameof(UpgradeSubscription); 13 | } 14 | -------------------------------------------------------------------------------- /src/Modules/Auditing/Modules.Auditing.Contracts/v1/GetSecurityAudits/GetSecurityAuditsQuery.cs: -------------------------------------------------------------------------------- 1 | using FSH.Modules.Auditing.Contracts; 2 | using FSH.Modules.Auditing.Contracts.Dtos; 3 | using Mediator; 4 | 5 | namespace FSH.Modules.Auditing.Contracts.v1.GetSecurityAudits; 6 | 7 | public sealed class GetSecurityAuditsQuery : IQuery> 8 | { 9 | public SecurityAction? Action { get; init; } 10 | 11 | public string? UserId { get; init; } 12 | 13 | public string? TenantId { get; init; } 14 | 15 | public DateTime? FromUtc { get; init; } 16 | 17 | public DateTime? ToUtc { get; init; } 18 | } 19 | 20 | -------------------------------------------------------------------------------- /src/Modules/Multitenancy/Modules.Multitenancy/Extensions.cs: -------------------------------------------------------------------------------- 1 | using Finbuckle.MultiTenant; 2 | using Finbuckle.MultiTenant.Abstractions; 3 | using Finbuckle.MultiTenant.AspNetCore.Extensions; 4 | using FSH.Modules.Multitenancy.Data; 5 | using Microsoft.AspNetCore.Builder; 6 | using Microsoft.Extensions.DependencyInjection; 7 | 8 | namespace FSH.Modules.Multitenancy; 9 | 10 | public static class Extensions 11 | { 12 | public static WebApplication UseHeroMultiTenantDatabases(this WebApplication app) 13 | { 14 | ArgumentNullException.ThrowIfNull(app); 15 | app.UseMultiTenant(); 16 | 17 | return app; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Modules/Auditing/Modules.Auditing/Features/v1/GetSecurityAudits/GetSecurityAuditsQueryValidator.cs: -------------------------------------------------------------------------------- 1 | using FluentValidation; 2 | using FSH.Modules.Auditing.Contracts.v1.GetSecurityAudits; 3 | 4 | namespace FSH.Modules.Auditing.Features.v1.GetSecurityAudits; 5 | 6 | public sealed class GetSecurityAuditsQueryValidator : AbstractValidator 7 | { 8 | public GetSecurityAuditsQueryValidator() 9 | { 10 | RuleFor(q => q) 11 | .Must(q => !q.FromUtc.HasValue || !q.ToUtc.HasValue || q.FromUtc <= q.ToUtc) 12 | .WithMessage("FromUtc must be less than or equal to ToUtc."); 13 | } 14 | } 15 | 16 | -------------------------------------------------------------------------------- /src/Modules/Identity/Modules.Identity.Contracts/v1/Users/ChangePassword/ChangePasswordCommand.cs: -------------------------------------------------------------------------------- 1 | using Mediator; 2 | 3 | namespace FSH.Modules.Identity.Contracts.v1.Users.ChangePassword; 4 | 5 | public class ChangePasswordCommand : ICommand 6 | { 7 | /// The user's current password. 8 | public string Password { get; init; } = default!; 9 | 10 | /// The new password the user wants to set. 11 | public string NewPassword { get; init; } = default!; 12 | 13 | /// Confirmation of the new password. 14 | public string ConfirmNewPassword { get; init; } = default!; 15 | } -------------------------------------------------------------------------------- /src/Modules/Auditing/Modules.Auditing/Features/v1/GetExceptionAudits/GetExceptionAuditsQueryValidator.cs: -------------------------------------------------------------------------------- 1 | using FluentValidation; 2 | using FSH.Modules.Auditing.Contracts.v1.GetExceptionAudits; 3 | 4 | namespace FSH.Modules.Auditing.Features.v1.GetExceptionAudits; 5 | 6 | public sealed class GetExceptionAuditsQueryValidator : AbstractValidator 7 | { 8 | public GetExceptionAuditsQueryValidator() 9 | { 10 | RuleFor(q => q) 11 | .Must(q => !q.FromUtc.HasValue || !q.ToUtc.HasValue || q.FromUtc <= q.ToUtc) 12 | .WithMessage("FromUtc must be less than or equal to ToUtc."); 13 | } 14 | } 15 | 16 | -------------------------------------------------------------------------------- /src/Modules/Identity/Modules.Identity/Features/v1/Tokens/RefreshToken/RefreshTokenCommandValidator.cs: -------------------------------------------------------------------------------- 1 | using FluentValidation; 2 | using FSH.Modules.Identity.Contracts.v1.Tokens.RefreshToken; 3 | 4 | namespace FSH.Modules.Identity.Features.v1.Tokens.RefreshToken; 5 | 6 | public class RefreshTokenCommandValidator : AbstractValidator 7 | { 8 | public RefreshTokenCommandValidator() 9 | { 10 | RuleFor(p => p.Token) 11 | .Cascade(CascadeMode.Stop) 12 | .NotEmpty(); 13 | 14 | RuleFor(p => p.RefreshToken) 15 | .Cascade(CascadeMode.Stop) 16 | .NotEmpty(); 17 | } 18 | } 19 | 20 | -------------------------------------------------------------------------------- /src/Modules/Multitenancy/Modules.Multitenancy/Data/Configurations/TenantProvisioningStepConfiguration.cs: -------------------------------------------------------------------------------- 1 | using FSH.Framework.Shared.Multitenancy; 2 | using FSH.Modules.Multitenancy.Provisioning; 3 | using Microsoft.EntityFrameworkCore; 4 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 5 | 6 | namespace FSH.Modules.Multitenancy.Data.Configurations; 7 | 8 | public class TenantProvisioningStepConfiguration : IEntityTypeConfiguration 9 | { 10 | public void Configure(EntityTypeBuilder builder) 11 | { 12 | builder.ToTable("TenantProvisioningSteps", MultitenancyConstants.Schema); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Playground/Playground.Blazor/Components/_Imports.razor: -------------------------------------------------------------------------------- 1 | @using System.Net.Http 2 | @using System.Net.Http.Json 3 | @using FSH.Playground.Blazor.Components.Layout 4 | @using Microsoft.AspNetCore.Authorization 5 | @using Microsoft.AspNetCore.Components.Forms 6 | @using Microsoft.AspNetCore.Components.Routing 7 | @using Microsoft.AspNetCore.Components.Web 8 | @using static Microsoft.AspNetCore.Components.Web.RenderMode 9 | @using Microsoft.AspNetCore.Components.Web.Virtualization 10 | @using Microsoft.JSInterop 11 | @using MudBlazor 12 | @using MudBlazor.Services 13 | @using FSH.Framework.Blazor.UI.Components.Page 14 | @using FSH.Framework.Blazor.UI.Components.User 15 | -------------------------------------------------------------------------------- /src/BuildingBlocks/Web/Mediator/Extensions.cs: -------------------------------------------------------------------------------- 1 | using FSH.Framework.Web.Mediator.Behaviors; 2 | using Mediator; 3 | using Microsoft.Extensions.DependencyInjection; 4 | using System.Reflection; 5 | 6 | namespace FSH.Framework.Web.Mediator; 7 | 8 | public static class Extensions 9 | { 10 | public static IServiceCollection 11 | EnableMediator(this IServiceCollection services, params Assembly[] featureAssemblies) 12 | { 13 | ArgumentNullException.ThrowIfNull(services); 14 | 15 | // Behaviors 16 | services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>)); 17 | 18 | return services; 19 | } 20 | 21 | } -------------------------------------------------------------------------------- /src/Modules/Multitenancy/Modules.Multitenancy.Contracts/Dtos/TenantProvisioningStatusDto.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.Modules.Multitenancy.Contracts.Dtos; 2 | 3 | public sealed record TenantProvisioningStepDto( 4 | string Step, 5 | string Status, 6 | DateTime? StartedUtc, 7 | DateTime? CompletedUtc, 8 | string? Error); 9 | 10 | public sealed record TenantProvisioningStatusDto( 11 | string TenantId, 12 | string Status, 13 | string CorrelationId, 14 | string? CurrentStep, 15 | string? Error, 16 | DateTime CreatedUtc, 17 | DateTime? StartedUtc, 18 | DateTime? CompletedUtc, 19 | IReadOnlyCollection Steps); 20 | -------------------------------------------------------------------------------- /scripts/openapi/check-openapi-drift.ps1: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env pwsh 2 | param( 3 | [Parameter(Mandatory = $false)] 4 | [string] $SpecUrl = "https://localhost:7030/openapi/v1.json" 5 | ) 6 | 7 | Set-StrictMode -Version Latest 8 | $ErrorActionPreference = "Stop" 9 | 10 | $repoRoot = Resolve-Path "$PSScriptRoot/../.." 11 | Set-Location $repoRoot 12 | 13 | Write-Host "Running NSwag generation against $SpecUrl..." 14 | ./scripts/openapi/generate-api-clients.ps1 -SpecUrl $SpecUrl 15 | 16 | $targetFile = "src/Playground/Playground.Blazor/ApiClient/Generated.cs" 17 | 18 | Write-Host "Checking for drift in $targetFile..." 19 | git diff --exit-code -- $targetFile 20 | 21 | Write-Host "No drift detected." 22 | -------------------------------------------------------------------------------- /src/BuildingBlocks/Blazor.UI/Components/Feedback/FshAlert.razor: -------------------------------------------------------------------------------- 1 | @inherits FshComponentBase 2 | 8 | @ChildContent 9 | 10 | 11 | @code { 12 | [Parameter] public Severity Severity { get; set; } = Severity.Info; 13 | [Parameter] public Variant Variant { get; set; } = Variant.Filled; 14 | [Parameter] public RenderFragment? ChildContent { get; set; } 15 | [Parameter(CaptureUnmatchedValues = true)] public Dictionary? AdditionalAttributes { get; set; } 16 | } 17 | -------------------------------------------------------------------------------- /src/Modules/Identity/Modules.Identity/Features/v1/Tokens/TokenGeneration/GenerateTokenCommandValidator.cs: -------------------------------------------------------------------------------- 1 | using FluentValidation; 2 | using FSH.Modules.Identity.Contracts.v1.Tokens.TokenGeneration; 3 | 4 | namespace FSH.Modules.Identity.Features.v1.Tokens.TokenGeneration; 5 | 6 | public class TokenGenerationCommandValidator : AbstractValidator 7 | { 8 | public TokenGenerationCommandValidator() 9 | { 10 | RuleFor(p => p.Email) 11 | .Cascade(CascadeMode.Stop) 12 | .NotEmpty() 13 | .EmailAddress(); 14 | 15 | RuleFor(p => p.Password) 16 | .Cascade(CascadeMode.Stop) 17 | .NotEmpty(); 18 | } 19 | } -------------------------------------------------------------------------------- /src/BuildingBlocks/Blazor.UI/Components/Inputs/FshSwitch.razor: -------------------------------------------------------------------------------- 1 | @inherits FshComponentBase 2 | 8 | 9 | @code { 10 | [Parameter] public bool Checked { get; set; } 11 | [Parameter] public EventCallback CheckedChanged { get; set; } 12 | [Parameter] public bool Disabled { get; set; } 13 | [Parameter] public Color Color { get; set; } = Color.Primary; 14 | [Parameter(CaptureUnmatchedValues = true)] public Dictionary? AdditionalAttributes { get; set; } 15 | } 16 | -------------------------------------------------------------------------------- /src/BuildingBlocks/Blazor.UI/Components/Avatars/FshAvatar.razor: -------------------------------------------------------------------------------- 1 | 6 | 7 | @code { 8 | [Parameter] public string? Src { get; set; } 9 | [Parameter] public string? Text { get; set; } 10 | [Parameter] public Size Size { get; set; } = Size.Medium; 11 | [Parameter] public string? Class { get; set; } 12 | [Parameter(CaptureUnmatchedValues = true)] public Dictionary? AdditionalAttributes { get; set; } 13 | 14 | private string CssClass => string.IsNullOrWhiteSpace(Class) ? "fsh-avatar" : $"fsh-avatar {Class}"; 15 | } 16 | -------------------------------------------------------------------------------- /src/BuildingBlocks/Eventing/Outbox/IOutboxStore.cs: -------------------------------------------------------------------------------- 1 | using FSH.Framework.Eventing.Abstractions; 2 | 3 | namespace FSH.Framework.Eventing.Outbox; 4 | 5 | /// 6 | /// Abstraction for persisting and reading outbox messages. 7 | /// 8 | public interface IOutboxStore 9 | { 10 | Task AddAsync(IIntegrationEvent @event, CancellationToken ct = default); 11 | 12 | Task> GetPendingBatchAsync(int batchSize, CancellationToken ct = default); 13 | 14 | Task MarkAsProcessedAsync(OutboxMessage message, CancellationToken ct = default); 15 | 16 | Task MarkAsFailedAsync(OutboxMessage message, string error, bool isDead, CancellationToken ct = default); 17 | } 18 | 19 | -------------------------------------------------------------------------------- /src/Modules/Auditing/Modules.Auditing.Contracts/ISecurityAudit.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.Modules.Auditing.Contracts; 2 | 3 | public interface ISecurityAudit 4 | { 5 | ValueTask LoginSucceededAsync(string userId, string userName, string clientId, string ip, string userAgent, CancellationToken ct = default); 6 | ValueTask LoginFailedAsync(string subjectIdOrName, string clientId, string reason, string ip, CancellationToken ct = default); 7 | ValueTask TokenIssuedAsync(string userId, string userName, string clientId, string tokenFingerprint, DateTime expiresUtc, CancellationToken ct = default); 8 | ValueTask TokenRevokedAsync(string userId, string clientId, string reason, CancellationToken ct = default); 9 | } 10 | -------------------------------------------------------------------------------- /src/Modules/Auditing/Modules.Auditing/Features/v1/GetAuditsByTrace/GetAuditsByTraceQueryValidator.cs: -------------------------------------------------------------------------------- 1 | using FluentValidation; 2 | using FSH.Modules.Auditing.Contracts.v1.GetAuditsByTrace; 3 | 4 | namespace FSH.Modules.Auditing.Features.v1.GetAuditsByTrace; 5 | 6 | public sealed class GetAuditsByTraceQueryValidator : AbstractValidator 7 | { 8 | public GetAuditsByTraceQueryValidator() 9 | { 10 | RuleFor(q => q.TraceId) 11 | .NotEmpty(); 12 | 13 | RuleFor(q => q) 14 | .Must(q => !q.FromUtc.HasValue || !q.ToUtc.HasValue || q.FromUtc <= q.ToUtc) 15 | .WithMessage("FromUtc must be less than or equal to ToUtc."); 16 | } 17 | } 18 | 19 | -------------------------------------------------------------------------------- /src/Modules/Auditing/Modules.Auditing.Contracts/v1/GetExceptionAudits/GetExceptionAuditsQuery.cs: -------------------------------------------------------------------------------- 1 | using FSH.Modules.Auditing.Contracts; 2 | using FSH.Modules.Auditing.Contracts.Dtos; 3 | using Mediator; 4 | 5 | namespace FSH.Modules.Auditing.Contracts.v1.GetExceptionAudits; 6 | 7 | public sealed class GetExceptionAuditsQuery : IQuery> 8 | { 9 | public ExceptionArea? Area { get; init; } 10 | 11 | public AuditSeverity? Severity { get; init; } 12 | 13 | public string? ExceptionType { get; init; } 14 | 15 | public string? RouteOrLocation { get; init; } 16 | 17 | public DateTime? FromUtc { get; init; } 18 | 19 | public DateTime? ToUtc { get; init; } 20 | } 21 | 22 | -------------------------------------------------------------------------------- /src/Modules/Identity/Modules.Identity.Contracts/v1/Users/SearchUsers/SearchUsersQuery.cs: -------------------------------------------------------------------------------- 1 | using FSH.Framework.Shared.Persistence; 2 | using FSH.Modules.Identity.Contracts.DTOs; 3 | using Mediator; 4 | 5 | namespace FSH.Modules.Identity.Contracts.v1.Users.SearchUsers; 6 | 7 | public sealed class SearchUsersQuery : IPagedQuery, IQuery> 8 | { 9 | public int? PageNumber { get; set; } 10 | 11 | public int? PageSize { get; set; } 12 | 13 | public string? Sort { get; set; } 14 | 15 | public string? Search { get; set; } 16 | 17 | public bool? IsActive { get; set; } 18 | 19 | public bool? EmailConfirmed { get; set; } 20 | 21 | public string? RoleId { get; set; } 22 | } 23 | -------------------------------------------------------------------------------- /src/BuildingBlocks/Web/Modules/FshModuleAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace FSH.Framework.Web.Modules; 4 | 5 | [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] 6 | public sealed class FshModuleAttribute : Attribute 7 | { 8 | public Type ModuleType { get; } 9 | 10 | /// 11 | /// Optional ordering hint that allows hosts to control module startup sequencing. 12 | /// Lower numbers execute first. 13 | /// 14 | public int Order { get; } 15 | 16 | public FshModuleAttribute(Type moduleType, int order = 0) 17 | { 18 | ModuleType = moduleType ?? throw new ArgumentNullException(nameof(moduleType)); 19 | Order = order; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Modules/Multitenancy/Modules.Multitenancy/Features/v1/GetTenantTheme/GetTenantThemeQueryHandler.cs: -------------------------------------------------------------------------------- 1 | using FSH.Modules.Multitenancy.Contracts; 2 | using FSH.Modules.Multitenancy.Contracts.Dtos; 3 | using FSH.Modules.Multitenancy.Contracts.v1.GetTenantTheme; 4 | using Mediator; 5 | 6 | namespace FSH.Modules.Multitenancy.Features.v1.GetTenantTheme; 7 | 8 | public sealed class GetTenantThemeQueryHandler(ITenantThemeService themeService) 9 | : IQueryHandler 10 | { 11 | public async ValueTask Handle(GetTenantThemeQuery query, CancellationToken cancellationToken) 12 | { 13 | return await themeService.GetCurrentTenantThemeAsync(cancellationToken); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/BuildingBlocks/Shared/Shared.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | FSH.Framework.Shared 5 | FSH.Framework.Shared 6 | FullStackHero.Framework.Shared 7 | $(NoWarn);CA1716;CA1711;CA1019;CA1305 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/Modules/Multitenancy/Modules.Multitenancy.Contracts/Modules.Multitenancy.Contracts.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | FSH.Modules.Multitenancy.Contracts 4 | FSH.Modules.Multitenancy.Contracts 5 | FullStackHero.Modules.Multitenancy.Contracts 6 | $(NoWarn);CA1056;S2094 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/Modules/Identity/Modules.Identity.Contracts/Modules.Identity.Contracts.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | FSH.Modules.Identity.Contracts 5 | FSH.Modules.Identity.Contracts 6 | FullStackHero.Modules.Identity.Contracts 7 | $(NoWarn);CA1002;CA1056;CS1572;S2094 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/Modules/Multitenancy/Modules.Multitenancy.Contracts/Dtos/TenantMigrationStatusDto.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.Modules.Multitenancy.Contracts.Dtos; 2 | 3 | public sealed class TenantMigrationStatusDto 4 | { 5 | public string TenantId { get; set; } = default!; 6 | 7 | public string Name { get; set; } = string.Empty; 8 | 9 | public bool IsActive { get; set; } 10 | 11 | public DateTime? ValidUpto { get; set; } 12 | 13 | public bool HasPendingMigrations { get; set; } 14 | 15 | public string? Provider { get; set; } 16 | 17 | public string? LastAppliedMigration { get; set; } 18 | 19 | public IReadOnlyCollection PendingMigrations { get; set; } = Array.Empty(); 20 | 21 | public string? Error { get; set; } 22 | } 23 | -------------------------------------------------------------------------------- /src/Modules/Auditing/Modules.Auditing/Features/v1/GetAuditsByCorrelation/GetAuditsByCorrelationQueryValidator.cs: -------------------------------------------------------------------------------- 1 | using FluentValidation; 2 | using FSH.Modules.Auditing.Contracts.v1.GetAuditsByCorrelation; 3 | 4 | namespace FSH.Modules.Auditing.Features.v1.GetAuditsByCorrelation; 5 | 6 | public sealed class GetAuditsByCorrelationQueryValidator : AbstractValidator 7 | { 8 | public GetAuditsByCorrelationQueryValidator() 9 | { 10 | RuleFor(q => q.CorrelationId) 11 | .NotEmpty(); 12 | 13 | RuleFor(q => q) 14 | .Must(q => !q.FromUtc.HasValue || !q.ToUtc.HasValue || q.FromUtc <= q.ToUtc) 15 | .WithMessage("FromUtc must be less than or equal to ToUtc."); 16 | } 17 | } 18 | 19 | -------------------------------------------------------------------------------- /src/Modules/Auditing/Modules.Auditing/Infrastructure/Serialization/SystemTextJsonAuditSerializer.cs: -------------------------------------------------------------------------------- 1 | using FSH.Modules.Auditing.Contracts; 2 | using System.Text.Json; 3 | using System.Text.Json.Serialization; 4 | 5 | namespace FSH.Modules.Auditing; 6 | 7 | public sealed class SystemTextJsonAuditSerializer : IAuditSerializer 8 | { 9 | private static readonly JsonSerializerOptions Opts = new() 10 | { 11 | PropertyNamingPolicy = JsonNamingPolicy.CamelCase, 12 | DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, 13 | Converters = { new JsonStringEnumConverter() }, 14 | WriteIndented = false 15 | }; 16 | 17 | public string SerializePayload(object payload) => JsonSerializer.Serialize(payload, Opts); 18 | } 19 | 20 | -------------------------------------------------------------------------------- /src/Modules/Identity/Modules.Identity.Contracts/v1/Users/RegisterUser/RegisterUserCommand.cs: -------------------------------------------------------------------------------- 1 | using Mediator; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace FSH.Modules.Identity.Contracts.v1.Users.RegisterUser; 5 | 6 | public class RegisterUserCommand : ICommand 7 | { 8 | public string FirstName { get; set; } = default!; 9 | public string LastName { get; set; } = default!; 10 | public string Email { get; set; } = default!; 11 | public string UserName { get; set; } = default!; 12 | public string Password { get; set; } = default!; 13 | public string ConfirmPassword { get; set; } = default!; 14 | public string? PhoneNumber { get; set; } 15 | 16 | [JsonIgnore] 17 | public string? Origin { get; set; } 18 | } -------------------------------------------------------------------------------- /src/BuildingBlocks/Caching/CachingOptions.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.Framework.Caching; 2 | 3 | public sealed class CachingOptions 4 | { 5 | /// Redis connection string. If empty, falls back to in-memory. 6 | public string Redis { get; set; } = string.Empty; 7 | 8 | /// Default sliding expiration if caller doesn't specify. 9 | public TimeSpan? DefaultSlidingExpiration { get; set; } = TimeSpan.FromMinutes(5); 10 | 11 | /// Default absolute expiration (cap). 12 | public TimeSpan? DefaultAbsoluteExpiration { get; set; } = TimeSpan.FromMinutes(15); 13 | 14 | /// Optional prefix (env/tenant/app) applied to all keys. 15 | public string? KeyPrefix { get; set; } = "fsh_"; 16 | } 17 | -------------------------------------------------------------------------------- /src/BuildingBlocks/Web/OpenApi/OpenApiOptions.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.Framework.Web.OpenApi; 2 | public sealed class OpenApiOptions 3 | { 4 | public required string Title { get; init; } 5 | public string Version { get; init; } = "v1"; 6 | public required string Description { get; init; } 7 | 8 | public ContactOptions? Contact { get; init; } 9 | public LicenseOptions? License { get; init; } 10 | 11 | public sealed class ContactOptions 12 | { 13 | public string? Name { get; init; } 14 | public Uri? Url { get; init; } 15 | public string? Email { get; init; } 16 | } 17 | 18 | public sealed class LicenseOptions 19 | { 20 | public string? Name { get; init; } 21 | public Uri? Url { get; init; } 22 | } 23 | } -------------------------------------------------------------------------------- /src/Modules/Identity/Modules.Identity/Data/PasswordPolicyOptions.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.Modules.Identity.Data; 2 | 3 | public class PasswordPolicyOptions 4 | { 5 | /// Number of previous passwords to keep in history (prevent reuse) 6 | public int PasswordHistoryCount { get; set; } = 5; 7 | 8 | /// Number of days before password expires and must be changed 9 | public int PasswordExpiryDays { get; set; } = 90; 10 | 11 | /// Number of days before expiry to show warning to user 12 | public int PasswordExpiryWarningDays { get; set; } = 14; 13 | 14 | /// Set to false to disable password expiry enforcement 15 | public bool EnforcePasswordExpiry { get; set; } = true; 16 | } 17 | -------------------------------------------------------------------------------- /src/BuildingBlocks/Shared/Identity/RoleConstants.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.ObjectModel; 2 | 3 | namespace FSH.Framework.Shared.Constants; 4 | 5 | public static class RoleConstants 6 | { 7 | public const string Admin = nameof(Admin); 8 | public const string Basic = nameof(Basic); 9 | 10 | /// 11 | /// The base roles provided by the framework. 12 | /// 13 | public static IReadOnlyList DefaultRoles { get; } = new ReadOnlyCollection(new[] 14 | { 15 | Admin, 16 | Basic 17 | }); 18 | 19 | /// 20 | /// Determines whether the role is a framework-defined default. 21 | /// 22 | public static bool IsDefault(string roleName) => DefaultRoles.Contains(roleName); 23 | } -------------------------------------------------------------------------------- /src/BuildingBlocks/Storage/Storage.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | FSH.Framework.Storage 5 | FSH.Framework.Storage 6 | FullStackHero.Framework.Storage 7 | $(NoWarn);CA1031;CA1056;CA1002;CA2227;CA1812;CA1308;CA1062 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/Modules/Identity/Modules.Identity/Features/v1/Users/AssignUserRoles/AssignUserRolesCommandHandler.cs: -------------------------------------------------------------------------------- 1 | using FSH.Modules.Identity.Contracts.Services; 2 | using FSH.Modules.Identity.Contracts.v1.Users.AssignUserRoles; 3 | using Mediator; 4 | 5 | namespace FSH.Modules.Identity.Features.v1.Users.AssignUserRoles; 6 | 7 | public sealed class AssignUserRolesCommandHandler(IUserService userService) 8 | : ICommandHandler 9 | { 10 | public async ValueTask Handle(AssignUserRolesCommand command, CancellationToken cancellationToken) 11 | { 12 | ArgumentNullException.ThrowIfNull(command); 13 | 14 | return await userService.AssignRolesAsync(command.UserId, command.UserRoles, cancellationToken); 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/BuildingBlocks/Blazor.UI/Components/Button/FshButton.razor: -------------------------------------------------------------------------------- 1 | @inherits FshComponentBase 2 | 3 | 10 | @ChildContent 11 | 12 | 13 | @code { 14 | [Parameter] public Color Color { get; set; } = Color.Primary; 15 | [Parameter] public Variant Variant { get; set; } = Variant.Filled; 16 | [Parameter] public string? StartIcon { get; set; } 17 | [Parameter] public bool Disabled { get; set; } 18 | [Parameter] public EventCallback OnClick { get; set; } 19 | [Parameter] public RenderFragment? ChildContent { get; set; } 20 | } 21 | -------------------------------------------------------------------------------- /src/BuildingBlocks/Mailing/Mailing.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | FSH.Framework.Mailing 5 | FSH.Framework.Mailing 6 | FullStackHero.Framework.Mailing 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/Modules/Multitenancy/Modules.Multitenancy/Features/v1/GetTenantStatus/GetTenantStatusQueryHandler.cs: -------------------------------------------------------------------------------- 1 | using FSH.Modules.Multitenancy.Contracts; 2 | using FSH.Modules.Multitenancy.Contracts.Dtos; 3 | using FSH.Modules.Multitenancy.Contracts.v1.GetTenantStatus; 4 | using Mediator; 5 | 6 | namespace FSH.Modules.Multitenancy.Features.v1.GetTenantStatus; 7 | 8 | public sealed class GetTenantStatusQueryHandler(ITenantService tenantService) 9 | : IQueryHandler 10 | { 11 | public async ValueTask Handle(GetTenantStatusQuery query, CancellationToken cancellationToken) 12 | { 13 | ArgumentNullException.ThrowIfNull(query); 14 | return await tenantService.GetStatusAsync(query.TenantId); 15 | } 16 | } 17 | 18 | -------------------------------------------------------------------------------- /src/Playground/FSH.Playground.AppHost/FSH.Playground.AppHost.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | enable 6 | enable 7 | 9fe5df9a-b9b2-4202-bdb4-d30b01b71d1a 8 | false 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/Playground/Playground.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": false, 8 | "applicationUrl": "http://localhost:5030", 9 | "environmentVariables": { 10 | "ASPNETCORE_ENVIRONMENT": "Development" 11 | } 12 | }, 13 | "https": { 14 | "commandName": "Project", 15 | "dotnetRunMessages": true, 16 | "launchBrowser": true, 17 | "launchUrl": "scalar", 18 | "applicationUrl": "https://localhost:7030;http://localhost:5030", 19 | "environmentVariables": { 20 | "ASPNETCORE_ENVIRONMENT": "Development" 21 | } 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Modules/Identity/Modules.Identity.Contracts/Events/TokenGeneratedIntegrationEvent.cs: -------------------------------------------------------------------------------- 1 | using FSH.Framework.Eventing.Abstractions; 2 | 3 | namespace FSH.Modules.Identity.Contracts.Events; 4 | 5 | /// 6 | /// Integration event raised when a JWT token is generated for a user. 7 | /// Intended primarily as a sample event to exercise the eventing/outbox pipeline. 8 | /// 9 | public sealed record TokenGeneratedIntegrationEvent( 10 | Guid Id, 11 | DateTime OccurredOnUtc, 12 | string? TenantId, 13 | string CorrelationId, 14 | string Source, 15 | string UserId, 16 | string Email, 17 | string ClientId, 18 | string IpAddress, 19 | string UserAgent, 20 | string TokenFingerprint, 21 | DateTime AccessTokenExpiresAtUtc) 22 | : IIntegrationEvent; 23 | 24 | -------------------------------------------------------------------------------- /src/Playground/Playground.Blazor/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 | "applicationUrl": "http://localhost:5032", 9 | "environmentVariables": { 10 | "ASPNETCORE_ENVIRONMENT": "Development" 11 | } 12 | }, 13 | "https": { 14 | "commandName": "Project", 15 | "dotnetRunMessages": true, 16 | "launchBrowser": true, 17 | "applicationUrl": "https://localhost:7140;http://localhost:5032", 18 | "environmentVariables": { 19 | "ASPNETCORE_ENVIRONMENT": "Development" 20 | } 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/BuildingBlocks/Shared/Auditing/AuditAttributes.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.Framework.Shared.Auditing; 2 | 3 | /// Marks a property that should be excluded from audit diffs and payloads. 4 | [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] 5 | public sealed class AuditIgnoreAttribute : Attribute { } 6 | 7 | /// 8 | /// Marks a property as sensitive (to be masked or hashed when serialized). 9 | /// 10 | [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)] 11 | public sealed class AuditSensitiveAttribute : Attribute 12 | { 13 | public bool Hash { get; init; } 14 | public bool Redact { get; init; } 15 | 16 | public AuditSensitiveAttribute(bool hash = false, bool redact = false) 17 | => (Hash, Redact) = (hash, redact); 18 | } 19 | 20 | -------------------------------------------------------------------------------- /src/Modules/Auditing/Modules.Auditing.Contracts/Dtos/AuditSummaryAggregateDto.cs: -------------------------------------------------------------------------------- 1 | using FSH.Modules.Auditing.Contracts; 2 | 3 | namespace FSH.Modules.Auditing.Contracts.Dtos; 4 | 5 | public sealed class AuditSummaryAggregateDto 6 | { 7 | public IDictionary EventsByType { get; init; } = 8 | new Dictionary(); 9 | 10 | public IDictionary EventsBySeverity { get; init; } = 11 | new Dictionary(); 12 | 13 | public IDictionary EventsBySource { get; init; } = 14 | new Dictionary(StringComparer.OrdinalIgnoreCase); 15 | 16 | public IDictionary EventsByTenant { get; init; } = 17 | new Dictionary(StringComparer.OrdinalIgnoreCase); 18 | } 19 | -------------------------------------------------------------------------------- /src/Modules/Auditing/Modules.Auditing/Infrastructure/Http/ContentTypeHelper.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Net.Http.Headers; 2 | 3 | namespace FSH.Modules.Auditing; 4 | 5 | internal static class ContentTypeHelper 6 | { 7 | public static bool IsJsonLike(string? contentType, ISet allowed) 8 | { 9 | if (string.IsNullOrWhiteSpace(contentType)) return false; 10 | 11 | // Prefer robust parse; fallback to naive split if needed. 12 | if (MediaTypeHeaderValue.TryParse(contentType, out var mt)) 13 | return allowed.Contains(mt.MediaType.Value ?? string.Empty); 14 | 15 | var semi = contentType.IndexOf(';', StringComparison.Ordinal); 16 | var type = semi >= 0 ? contentType[..semi] : contentType; 17 | return allowed.Contains(type.Trim()); 18 | } 19 | } 20 | 21 | -------------------------------------------------------------------------------- /src/BuildingBlocks/Storage/FileType.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.Framework.Storage; 2 | 3 | public enum FileType 4 | { 5 | Image, 6 | Document, 7 | Pdf 8 | } 9 | 10 | public class FileValidationRules 11 | { 12 | public IReadOnlyList AllowedExtensions { get; init; } = Array.Empty(); 13 | public int MaxSizeInMB { get; init; } = 5; 14 | } 15 | 16 | public static class FileTypeMetadata 17 | { 18 | public static FileValidationRules GetRules(FileType type) => 19 | type switch 20 | { 21 | FileType.Image => new() { AllowedExtensions = [".jpg", ".jpeg", ".png"], MaxSizeInMB = 5 }, 22 | FileType.Pdf => new() { AllowedExtensions = [".pdf"], MaxSizeInMB = 10 }, 23 | _ => throw new NotSupportedException($"Unsupported file type: {type}") 24 | }; 25 | } -------------------------------------------------------------------------------- /src/BuildingBlocks/Shared/Persistence/IPagedQuery.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.Framework.Shared.Persistence; 2 | 3 | /// 4 | /// Shared pagination and sorting contract that can be implemented 5 | /// or extended by module-specific request types. 6 | /// 7 | public interface IPagedQuery 8 | { 9 | /// 10 | /// 1-based page number. Values less than 1 are normalized to 1. 11 | /// 12 | int? PageNumber { get; set; } 13 | 14 | /// 15 | /// Requested page size. Implementations may enforce caps. 16 | /// 17 | int? PageSize { get; set; } 18 | 19 | /// 20 | /// Multi-column sort expression, for example: "Name,-CreatedOn". 21 | /// "-" prefix indicates descending order. 22 | /// 23 | string? Sort { get; set; } 24 | } 25 | -------------------------------------------------------------------------------- /src/BuildingBlocks/Blazor.UI/Components/Inputs/FshCheckbox.razor: -------------------------------------------------------------------------------- 1 | @inherits FshComponentBase 2 | 10 | 11 | @code { 12 | [Parameter] public bool Checked { get; set; } 13 | [Parameter] public EventCallback CheckedChanged { get; set; } 14 | [Parameter] public string? Label { get; set; } 15 | [Parameter] public bool Disabled { get; set; } 16 | [Parameter] public Color Color { get; set; } = Color.Primary; 17 | [Parameter(CaptureUnmatchedValues = true)] public Dictionary? AdditionalAttributes { get; set; } 18 | } 19 | -------------------------------------------------------------------------------- /src/Modules/Multitenancy/Modules.Multitenancy/Features/v1/GetTenants/GetTenantsQueryHandler.cs: -------------------------------------------------------------------------------- 1 | using FSH.Framework.Shared.Persistence; 2 | using FSH.Modules.Multitenancy.Contracts.Dtos; 3 | using FSH.Modules.Multitenancy.Contracts.v1.GetTenants; 4 | using FSH.Modules.Multitenancy.Contracts; 5 | using Mediator; 6 | 7 | namespace FSH.Modules.Multitenancy.Features.v1.GetTenants; 8 | 9 | public sealed class GetTenantsQueryHandler(ITenantService tenantService) 10 | : IQueryHandler> 11 | { 12 | public async ValueTask> Handle(GetTenantsQuery query, CancellationToken cancellationToken) 13 | { 14 | ArgumentNullException.ThrowIfNull(query); 15 | return await tenantService.GetAllAsync(query, cancellationToken).ConfigureAwait(false); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Modules/Identity/Modules.Identity/Features/v1/Roles/GetRoles/GetRolesQueryHandler.cs: -------------------------------------------------------------------------------- 1 | using FSH.Modules.Identity.Contracts.DTOs; 2 | using FSH.Modules.Identity.Contracts.Services; 3 | using FSH.Modules.Identity.Contracts.v1.Roles.GetRoles; 4 | using Mediator; 5 | 6 | namespace FSH.Modules.Identity.Features.v1.Roles.GetRoles; 7 | 8 | public sealed class GetRolesQueryHandler : IQueryHandler> 9 | { 10 | private readonly IRoleService _roleService; 11 | 12 | public GetRolesQueryHandler(IRoleService roleService) 13 | { 14 | _roleService = roleService; 15 | } 16 | 17 | public async ValueTask> Handle(GetRolesQuery query, CancellationToken cancellationToken) 18 | { 19 | return await _roleService.GetRolesAsync().ConfigureAwait(false); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Modules/Identity/Modules.Identity/Features/v1/Users/GetUsers/GetUsersQueryHandler.cs: -------------------------------------------------------------------------------- 1 | using FSH.Modules.Identity.Contracts.DTOs; 2 | using FSH.Modules.Identity.Contracts.Services; 3 | using FSH.Modules.Identity.Contracts.v1.Users.GetUsers; 4 | using Mediator; 5 | 6 | namespace FSH.Modules.Identity.Features.v1.Users.GetUsers; 7 | 8 | public sealed class GetUsersQueryHandler : IQueryHandler> 9 | { 10 | private readonly IUserService _userService; 11 | 12 | public GetUsersQueryHandler(IUserService userService) 13 | { 14 | _userService = userService; 15 | } 16 | 17 | public async ValueTask> Handle(GetUsersQuery query, CancellationToken cancellationToken) 18 | { 19 | return await _userService.GetListAsync(cancellationToken).ConfigureAwait(false); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/BuildingBlocks/Eventing/Abstractions/IIntegrationEvent.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.Framework.Eventing.Abstractions; 2 | 3 | /// 4 | /// Base integration event contract used for cross-module and cross-service messaging. 5 | /// 6 | public interface IIntegrationEvent 7 | { 8 | Guid Id { get; } 9 | 10 | DateTime OccurredOnUtc { get; } 11 | 12 | /// 13 | /// Tenant identifier for tenant-scoped events. Null for global events. 14 | /// 15 | string? TenantId { get; } 16 | 17 | /// 18 | /// Correlation identifier to tie events to requests and traces. 19 | /// 20 | string CorrelationId { get; } 21 | 22 | /// 23 | /// Logical source of the event (e.g., module or service name). 24 | /// 25 | string Source { get; } 26 | } 27 | 28 | -------------------------------------------------------------------------------- /src/Modules/Auditing/Modules.Auditing.Contracts/Dtos/AuditSummaryDto.cs: -------------------------------------------------------------------------------- 1 | using FSH.Modules.Auditing.Contracts; 2 | 3 | namespace FSH.Modules.Auditing.Contracts.Dtos; 4 | 5 | public sealed class AuditSummaryDto 6 | { 7 | public Guid Id { get; set; } 8 | 9 | public DateTime OccurredAtUtc { get; set; } 10 | 11 | public AuditEventType EventType { get; set; } 12 | 13 | public AuditSeverity Severity { get; set; } 14 | 15 | public string? TenantId { get; set; } 16 | 17 | public string? UserId { get; set; } 18 | 19 | public string? UserName { get; set; } 20 | 21 | public string? TraceId { get; set; } 22 | 23 | public string? CorrelationId { get; set; } 24 | 25 | public string? RequestId { get; set; } 26 | 27 | public string? Source { get; set; } 28 | 29 | public AuditTag Tags { get; set; } 30 | } 31 | 32 | -------------------------------------------------------------------------------- /src/Modules/Auditing/Modules.Auditing/Features/v1/GetAudits/GetAuditsQueryValidator.cs: -------------------------------------------------------------------------------- 1 | using FluentValidation; 2 | using FSH.Modules.Auditing.Contracts.v1.GetAudits; 3 | 4 | namespace FSH.Modules.Auditing.Features.v1.GetAudits; 5 | 6 | public sealed class GetAuditsQueryValidator : AbstractValidator 7 | { 8 | public GetAuditsQueryValidator() 9 | { 10 | RuleFor(q => q.PageNumber) 11 | .GreaterThan(0) 12 | .When(q => q.PageNumber.HasValue); 13 | 14 | RuleFor(q => q.PageSize) 15 | .InclusiveBetween(1, 100) 16 | .When(q => q.PageSize.HasValue); 17 | 18 | RuleFor(q => q) 19 | .Must(q => !q.FromUtc.HasValue || !q.ToUtc.HasValue || q.FromUtc <= q.ToUtc) 20 | .WithMessage("FromUtc must be less than or equal to ToUtc."); 21 | } 22 | } 23 | 24 | -------------------------------------------------------------------------------- /src/Modules/Multitenancy/Modules.Multitenancy/Data/Configurations/TenantProvisioningConfiguration.cs: -------------------------------------------------------------------------------- 1 | using FSH.Framework.Shared.Multitenancy; 2 | using FSH.Modules.Multitenancy.Provisioning; 3 | using Microsoft.EntityFrameworkCore; 4 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 5 | 6 | namespace FSH.Modules.Multitenancy.Data.Configurations; 7 | 8 | public class TenantProvisioningConfiguration : IEntityTypeConfiguration 9 | { 10 | public void Configure(EntityTypeBuilder builder) 11 | { 12 | ArgumentNullException.ThrowIfNull(builder); 13 | 14 | builder.ToTable("TenantProvisionings", MultitenancyConstants.Schema); 15 | 16 | builder.HasMany(p => p.Steps) 17 | .WithOne(s => s.Provisioning!) 18 | .HasForeignKey(s => s.ProvisioningId); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Modules/Multitenancy/Modules.Multitenancy/Features/v1/UpgradeTenant/UpgradeTenantCommandHandler.cs: -------------------------------------------------------------------------------- 1 | using FSH.Modules.Multitenancy.Contracts; 2 | using FSH.Modules.Multitenancy.Contracts.v1.UpgradeTenant; 3 | using Mediator; 4 | 5 | namespace FSH.Modules.Multitenancy.Features.v1.UpgradeTenant; 6 | 7 | public sealed class UpgradeTenantCommandHandler(ITenantService service) 8 | : ICommandHandler 9 | { 10 | public async ValueTask Handle(UpgradeTenantCommand command, CancellationToken cancellationToken) 11 | { 12 | ArgumentNullException.ThrowIfNull(command); 13 | var validUpto = await service.UpgradeSubscription(command.Tenant, command.ExtendedExpiryDate); 14 | return new UpgradeTenantCommandResponse(validUpto, command.Tenant); 15 | } 16 | } -------------------------------------------------------------------------------- /src/Tools/CLI/Program.cs: -------------------------------------------------------------------------------- 1 | using FSH.CLI.Commands; 2 | using Spectre.Console.Cli; 3 | 4 | var app = new CommandApp(); 5 | 6 | app.Configure(config => 7 | { 8 | config.SetApplicationName("fsh"); 9 | config.SetApplicationVersion(GetVersion()); 10 | 11 | config.AddCommand("new") 12 | .WithDescription("Create a new FullStackHero project") 13 | .WithExample("new") 14 | .WithExample("new", "MyApp") 15 | .WithExample("new", "MyApp", "--preset", "quickstart") 16 | .WithExample("new", "MyApp", "--type", "api-blazor", "--arch", "monolith", "--db", "postgres"); 17 | }); 18 | 19 | return await app.RunAsync(args); 20 | 21 | static string GetVersion() 22 | { 23 | var assembly = typeof(Program).Assembly; 24 | var version = assembly.GetName().Version; 25 | return version?.ToString(3) ?? "1.0.0"; 26 | } 27 | -------------------------------------------------------------------------------- /src/Modules/Auditing/Modules.Auditing/Persistence/AuditRecord.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.Modules.Auditing; 2 | 3 | public sealed class AuditRecord 4 | { 5 | public Guid Id { get; set; } 6 | public DateTime OccurredAtUtc { get; set; } 7 | public DateTime ReceivedAtUtc { get; set; } 8 | 9 | public int EventType { get; set; } 10 | public byte Severity { get; set; } 11 | 12 | public string? TenantId { get; set; } 13 | public string? UserId { get; set; } 14 | public string? UserName { get; set; } 15 | public string? TraceId { get; set; } 16 | public string? SpanId { get; set; } 17 | public string? CorrelationId { get; set; } 18 | public string? RequestId { get; set; } 19 | public string? Source { get; set; } 20 | 21 | public long Tags { get; set; } 22 | 23 | public string PayloadJson { get; set; } = default!; 24 | } 25 | 26 | -------------------------------------------------------------------------------- /src/Modules/Identity/Modules.Identity.Contracts/DTOs/UserSessionDto.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.Modules.Identity.Contracts.DTOs; 2 | 3 | public class UserSessionDto 4 | { 5 | public Guid Id { get; set; } 6 | public string? UserId { get; set; } 7 | public string? UserName { get; set; } 8 | public string? UserEmail { get; set; } 9 | public string? IpAddress { get; set; } 10 | public string? DeviceType { get; set; } 11 | public string? Browser { get; set; } 12 | public string? BrowserVersion { get; set; } 13 | public string? OperatingSystem { get; set; } 14 | public string? OsVersion { get; set; } 15 | public DateTime CreatedAt { get; set; } 16 | public DateTime LastActivityAt { get; set; } 17 | public DateTime ExpiresAt { get; set; } 18 | public bool IsActive { get; set; } 19 | public bool IsCurrentSession { get; set; } 20 | } 21 | -------------------------------------------------------------------------------- /src/BuildingBlocks/Blazor.UI/Components/Chips/FshChip.razor: -------------------------------------------------------------------------------- 1 | 8 | @ChildContent 9 | 10 | 11 | @code { 12 | [Parameter] public Color Color { get; set; } = Color.Default; 13 | [Parameter] public Variant Variant { get; set; } = Variant.Filled; 14 | [Parameter] public string? Icon { get; set; } 15 | [Parameter] public string? Class { get; set; } 16 | [Parameter] public RenderFragment? ChildContent { get; set; } 17 | [Parameter(CaptureUnmatchedValues = true)] public Dictionary? AdditionalAttributes { get; set; } 18 | 19 | private string CssClass => string.IsNullOrWhiteSpace(Class) ? "fsh-chip" : $"fsh-chip {Class}"; 20 | } 21 | -------------------------------------------------------------------------------- /src/Modules/Auditing/Modules.Auditing.Contracts/AuditHttpOptions.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.Modules.Auditing.Contracts; 2 | 3 | public sealed class AuditHttpOptions 4 | { 5 | public bool CaptureBodies { get; set; } = true; 6 | public int MaxRequestBytes { get; set; } = 8_192; 7 | public int MaxResponseBytes { get; set; } = 16_384; 8 | 9 | public HashSet AllowedContentTypes { get; } = 10 | new(StringComparer.OrdinalIgnoreCase) 11 | { 12 | "application/json", 13 | "application/problem+json" 14 | }; 15 | 16 | public HashSet ExcludePathStartsWith { get; } = 17 | new(StringComparer.OrdinalIgnoreCase) 18 | { 19 | "/health", "/metrics", "/_framework", "/swagger", "/scalar", "/openapi" 20 | }; 21 | 22 | public AuditSeverity MinExceptionSeverity { get; set; } = AuditSeverity.Error; 23 | } 24 | -------------------------------------------------------------------------------- /src/Modules/Identity/Modules.Identity/Features/v1/Roles/GetRoleById/GetRoleByIdQueryHandler.cs: -------------------------------------------------------------------------------- 1 | using FSH.Modules.Identity.Contracts.DTOs; 2 | using FSH.Modules.Identity.Contracts.Services; 3 | using FSH.Modules.Identity.Contracts.v1.Roles.GetRole; 4 | using Mediator; 5 | 6 | namespace FSH.Modules.Identity.Features.v1.Roles.GetRoleById; 7 | 8 | public sealed class GetRoleByIdQueryHandler : IQueryHandler 9 | { 10 | private readonly IRoleService _roleService; 11 | 12 | public GetRoleByIdQueryHandler(IRoleService roleService) 13 | { 14 | _roleService = roleService; 15 | } 16 | 17 | public async ValueTask Handle(GetRoleQuery query, CancellationToken cancellationToken) 18 | { 19 | ArgumentNullException.ThrowIfNull(query); 20 | return await _roleService.GetRoleAsync(query.Id).ConfigureAwait(false); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Modules/Identity/Modules.Identity/Features/v1/Users/DeleteUser/DeleteUserCommandHandler.cs: -------------------------------------------------------------------------------- 1 | using FSH.Modules.Identity.Contracts.Services; 2 | using FSH.Modules.Identity.Contracts.v1.Users.DeleteUser; 3 | using Mediator; 4 | 5 | namespace FSH.Modules.Identity.Features.v1.Users.DeleteUser; 6 | 7 | public sealed class DeleteUserCommandHandler : ICommandHandler 8 | { 9 | private readonly IUserService _userService; 10 | 11 | public DeleteUserCommandHandler(IUserService userService) 12 | { 13 | _userService = userService; 14 | } 15 | 16 | public async ValueTask Handle(DeleteUserCommand command, CancellationToken cancellationToken) 17 | { 18 | ArgumentNullException.ThrowIfNull(command); 19 | 20 | await _userService.DeleteAsync(command.Id).ConfigureAwait(false); 21 | 22 | return Unit.Value; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Modules/Identity/Modules.Identity/Features/v1/Roles/DeleteRole/DeleteRoleCommandHandler.cs: -------------------------------------------------------------------------------- 1 | using FSH.Modules.Identity.Contracts.Services; 2 | using FSH.Modules.Identity.Contracts.v1.Roles.DeleteRole; 3 | using Mediator; 4 | 5 | namespace FSH.Modules.Identity.Features.v1.Roles.DeleteRole; 6 | 7 | public sealed class DeleteRoleCommandHandler : ICommandHandler 8 | { 9 | private readonly IRoleService _roleService; 10 | 11 | public DeleteRoleCommandHandler(IRoleService roleService) 12 | { 13 | _roleService = roleService; 14 | } 15 | 16 | public async ValueTask Handle(DeleteRoleCommand command, CancellationToken cancellationToken) 17 | { 18 | ArgumentNullException.ThrowIfNull(command); 19 | 20 | await _roleService.DeleteRoleAsync(command.Id).ConfigureAwait(false); 21 | 22 | return Unit.Value; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Modules/Identity/Modules.Identity/Features/v1/Users/GetUserById/GetUserByIdQueryHandler.cs: -------------------------------------------------------------------------------- 1 | using FSH.Modules.Identity.Contracts.DTOs; 2 | using FSH.Modules.Identity.Contracts.Services; 3 | using FSH.Modules.Identity.Contracts.v1.Users.GetUser; 4 | using Mediator; 5 | 6 | namespace FSH.Modules.Identity.Features.v1.Users.GetUserById; 7 | 8 | public sealed class GetUserByIdQueryHandler : IQueryHandler 9 | { 10 | private readonly IUserService _userService; 11 | 12 | public GetUserByIdQueryHandler(IUserService userService) 13 | { 14 | _userService = userService; 15 | } 16 | 17 | public async ValueTask Handle(GetUserQuery query, CancellationToken cancellationToken) 18 | { 19 | ArgumentNullException.ThrowIfNull(query); 20 | return await _userService.GetAsync(query.Id, cancellationToken).ConfigureAwait(false); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/BuildingBlocks/Persistence/Specifications/ISpecificationOfTResult.cs: -------------------------------------------------------------------------------- 1 | using System.Linq.Expressions; 2 | 3 | namespace FSH.Framework.Persistence; 4 | 5 | /// 6 | /// Projected specification that composes a query for 7 | /// and then selects into . 8 | /// 9 | /// 10 | /// Includes may be ignored when a selector is present; behavior is documented 11 | /// at the evaluator/extension level. 12 | /// 13 | /// The root entity type. 14 | /// The projected result type. 15 | public interface ISpecification : ISpecification 16 | where T : class 17 | { 18 | /// 19 | /// Projection applied at the end of the query pipeline. 20 | /// 21 | Expression> Selector { get; } 22 | } 23 | 24 | -------------------------------------------------------------------------------- /src/Playground/Migrations.PostgreSQL/Migrations.PostgreSQL.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | FSH.Playground.Migrations.PostgreSQL 5 | FSH.Playground.Migrations.PostgreSQL 6 | false 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/BuildingBlocks/Blazor.UI/Blazor.UI.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | FSH.Framework.Blazor.UI 5 | FSH.Framework.Blazor.UI 6 | FullStackHero.Framework.Blazor.UI 7 | 8 | $(NoWarn);MUD0002 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/BuildingBlocks/Blazor.UI/Components/Feedback/Snackbar/FshSnackbar.razor.cs: -------------------------------------------------------------------------------- 1 | using MudBlazor; 2 | 3 | namespace FSH.Framework.Blazor.UI.Components.Feedback.Snackbar; 4 | 5 | /// 6 | /// Convenience wrapper for snackbar calls with consistent styling. 7 | /// 8 | public sealed class FshSnackbar 9 | { 10 | private readonly ISnackbar _snackbar; 11 | 12 | public FshSnackbar(ISnackbar snackbar) 13 | { 14 | _snackbar = snackbar; 15 | } 16 | 17 | public void Success(string message) => Add(message, Severity.Success); 18 | public void Info(string message) => Add(message, Severity.Info); 19 | public void Warning(string message) => Add(message, Severity.Warning); 20 | public void Error(string message) => Add(message, Severity.Error); 21 | 22 | private void Add(string message, Severity severity) 23 | { 24 | _snackbar.Add(message, severity); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/BuildingBlocks/Core/Exceptions/NotFoundException.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | 3 | namespace FSH.Framework.Core.Exceptions; 4 | 5 | /// 6 | /// Exception representing a 404 Not Found error. 7 | /// 8 | public class NotFoundException : CustomException 9 | { 10 | public NotFoundException() 11 | : base("Resource not found.", Array.Empty(), HttpStatusCode.NotFound) 12 | { 13 | } 14 | 15 | public NotFoundException(string message) 16 | : base(message, Array.Empty(), HttpStatusCode.NotFound) 17 | { 18 | } 19 | 20 | public NotFoundException(string message, IEnumerable errors) 21 | : base(message, errors.ToList(), HttpStatusCode.NotFound) 22 | { 23 | } 24 | 25 | public NotFoundException(string message, Exception innerException) 26 | : base(message, innerException, HttpStatusCode.NotFound) 27 | { 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/BuildingBlocks/Core/Exceptions/ForbiddenException.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | 3 | namespace FSH.Framework.Core.Exceptions; 4 | /// 5 | /// Exception representing a 403 Forbidden error. 6 | /// 7 | public class ForbiddenException : CustomException 8 | { 9 | public ForbiddenException() 10 | : base("Unauthorized access.", Array.Empty(), HttpStatusCode.Forbidden) 11 | { 12 | } 13 | 14 | public ForbiddenException(string message) 15 | : base(message, Array.Empty(), HttpStatusCode.Forbidden) 16 | { 17 | } 18 | 19 | public ForbiddenException(string message, IEnumerable errors) 20 | : base(message, errors.ToList(), HttpStatusCode.Forbidden) 21 | { 22 | } 23 | 24 | public ForbiddenException(string message, Exception innerException) 25 | : base(message, innerException, HttpStatusCode.Forbidden) 26 | { 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/BuildingBlocks/Blazor.UI/Components/Data/Pagination/FshPagination.razor: -------------------------------------------------------------------------------- 1 | @inherits FshComponentBase 2 | 10 | 11 | @code { 12 | [Parameter] public int Page { get; set; } = 1; 13 | [Parameter] public int PageSize { get; set; } = 10; 14 | [Parameter] public int[] PageSizeOptions { get; set; } = [10, 20, 50]; 15 | [Parameter] public EventCallback PageChanged { get; set; } 16 | [Parameter] public EventCallback PageSizeChanged { get; set; } 17 | } 18 | -------------------------------------------------------------------------------- /src/BuildingBlocks/Eventing/EventingOptions.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.Framework.Eventing; 2 | 3 | /// 4 | /// Configuration options for the eventing building block. 5 | /// 6 | public sealed class EventingOptions 7 | { 8 | /// 9 | /// Provider for the event bus implementation. Defaults to InMemory. 10 | /// 11 | public string Provider { get; set; } = "InMemory"; 12 | 13 | /// 14 | /// Batch size for outbox dispatching. 15 | /// 16 | public int OutboxBatchSize { get; set; } = 100; 17 | 18 | /// 19 | /// Maximum number of retries before an outbox message is marked as dead. 20 | /// 21 | public int OutboxMaxRetries { get; set; } = 5; 22 | 23 | /// 24 | /// Whether inbox-based idempotent handling is enabled. 25 | /// 26 | public bool EnableInbox { get; set; } = true; 27 | } 28 | 29 | -------------------------------------------------------------------------------- /scripts/openapi/generate-api-clients.ps1: -------------------------------------------------------------------------------- 1 | param( 2 | [string]$SpecUrl = "https://localhost:7030/openapi/v1.json" 3 | ) 4 | 5 | $ErrorActionPreference = "Stop" 6 | 7 | $scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path 8 | $repoRoot = Resolve-Path (Join-Path $scriptDir ".." "..") 9 | $configPath = Join-Path $scriptDir "nswag-playground.json" 10 | $outputDir = Join-Path $repoRoot "src/Playground/Playground.Blazor/ApiClient" 11 | 12 | Write-Host "Ensuring dotnet local tools are restored..." -ForegroundColor Cyan 13 | dotnet tool restore | Out-Host 14 | 15 | Write-Host "Ensuring output directory exists: $outputDir" -ForegroundColor Cyan 16 | New-Item -ItemType Directory -Force -Path $outputDir | Out-Null 17 | 18 | Write-Host "Generating API client from spec: $SpecUrl" -ForegroundColor Cyan 19 | dotnet nswag run $configPath /variables:SpecUrl=$SpecUrl 20 | 21 | Write-Host "Done. Generated clients are in $outputDir" -ForegroundColor Green 22 | -------------------------------------------------------------------------------- /src/BuildingBlocks/Blazor.UI/Components/Data/FshTable.razor: -------------------------------------------------------------------------------- 1 | @typeparam TItem 2 | 10 | 11 | @HeaderContent 12 | 13 | 14 | @RowTemplate 15 | 16 | 17 | 18 | @code { 19 | [Parameter] public IEnumerable Items { get; set; } = Array.Empty(); 20 | [Parameter] public bool Loading { get; set; } 21 | [Parameter] public RenderFragment? HeaderContent { get; set; } 22 | [Parameter] public RenderFragment RowTemplate { get; set; } = default!; 23 | [Parameter(CaptureUnmatchedValues = true)] public Dictionary? AdditionalAttributes { get; set; } 24 | } 25 | -------------------------------------------------------------------------------- /src/Modules/Identity/Modules.Identity/Features/v1/Users/SearchUsers/SearchUsersQueryValidator.cs: -------------------------------------------------------------------------------- 1 | using FluentValidation; 2 | using FSH.Modules.Identity.Contracts.v1.Users.SearchUsers; 3 | 4 | namespace FSH.Modules.Identity.Features.v1.Users.SearchUsers; 5 | 6 | public sealed class SearchUsersQueryValidator : AbstractValidator 7 | { 8 | public SearchUsersQueryValidator() 9 | { 10 | RuleFor(q => q.PageNumber) 11 | .GreaterThan(0) 12 | .When(q => q.PageNumber.HasValue); 13 | 14 | RuleFor(q => q.PageSize) 15 | .InclusiveBetween(1, 100) 16 | .When(q => q.PageSize.HasValue); 17 | 18 | RuleFor(q => q.Search) 19 | .MaximumLength(200) 20 | .When(q => !string.IsNullOrEmpty(q.Search)); 21 | 22 | RuleFor(q => q.RoleId) 23 | .MaximumLength(450) 24 | .When(q => !string.IsNullOrEmpty(q.RoleId)); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Modules/Identity/Modules.Identity/Features/v1/Users/ConfirmEmail/ConfirmEmailCommandHandler.cs: -------------------------------------------------------------------------------- 1 | using FSH.Modules.Identity.Contracts.Services; 2 | using FSH.Modules.Identity.Contracts.v1.Users.ConfirmEmail; 3 | using Mediator; 4 | 5 | namespace FSH.Modules.Identity.Features.v1.Users.ConfirmEmail; 6 | 7 | public sealed class ConfirmEmailCommandHandler : ICommandHandler 8 | { 9 | private readonly IUserService _userService; 10 | 11 | public ConfirmEmailCommandHandler(IUserService userService) 12 | { 13 | _userService = userService; 14 | } 15 | 16 | public async ValueTask Handle(ConfirmEmailCommand command, CancellationToken cancellationToken) 17 | { 18 | ArgumentNullException.ThrowIfNull(command); 19 | 20 | return await _userService.ConfirmEmailAsync(command.UserId, command.Code, command.Tenant, cancellationToken) 21 | .ConfigureAwait(false); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Modules/Identity/Modules.Identity/Features/v1/Users/GetUserRoles/GetUserRolesQueryHandler.cs: -------------------------------------------------------------------------------- 1 | using FSH.Modules.Identity.Contracts.DTOs; 2 | using FSH.Modules.Identity.Contracts.Services; 3 | using FSH.Modules.Identity.Contracts.v1.Users.GetUserRoles; 4 | using Mediator; 5 | 6 | namespace FSH.Modules.Identity.Features.v1.Users.GetUserRoles; 7 | 8 | public sealed class GetUserRolesQueryHandler : IQueryHandler> 9 | { 10 | private readonly IUserService _userService; 11 | 12 | public GetUserRolesQueryHandler(IUserService userService) 13 | { 14 | _userService = userService; 15 | } 16 | 17 | public async ValueTask> Handle(GetUserRolesQuery query, CancellationToken cancellationToken) 18 | { 19 | ArgumentNullException.ThrowIfNull(query); 20 | return await _userService.GetUserRolesAsync(query.UserId, cancellationToken).ConfigureAwait(false); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Modules/Identity/Modules.Identity/Features/v1/Roles/UpdateRolePermissions/UpdatePermissionsCommandHandler.cs: -------------------------------------------------------------------------------- 1 | using FSH.Modules.Identity.Contracts.Services; 2 | using FSH.Modules.Identity.Contracts.v1.Roles.UpdatePermissions; 3 | using Mediator; 4 | 5 | namespace FSH.Modules.Identity.Features.v1.Roles.UpdateRolePermissions; 6 | 7 | public sealed class UpdatePermissionsCommandHandler : ICommandHandler 8 | { 9 | private readonly IRoleService _roleService; 10 | 11 | public UpdatePermissionsCommandHandler(IRoleService roleService) 12 | { 13 | _roleService = roleService; 14 | } 15 | 16 | public async ValueTask Handle(UpdatePermissionsCommand command, CancellationToken cancellationToken) 17 | { 18 | ArgumentNullException.ThrowIfNull(command); 19 | return await _roleService.UpdatePermissionsAsync(command.RoleId, command.Permissions).ConfigureAwait(false); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Modules/Identity/Modules.Identity/Features/v1/Sessions/GetUserSessions/GetUserSessionsQueryHandler.cs: -------------------------------------------------------------------------------- 1 | using FSH.Modules.Identity.Contracts.DTOs; 2 | using FSH.Modules.Identity.Contracts.Services; 3 | using FSH.Modules.Identity.Contracts.v1.Sessions.GetUserSessions; 4 | using Mediator; 5 | 6 | namespace FSH.Modules.Identity.Features.v1.Sessions.GetUserSessions; 7 | 8 | public sealed class GetUserSessionsQueryHandler : IQueryHandler> 9 | { 10 | private readonly ISessionService _sessionService; 11 | 12 | public GetUserSessionsQueryHandler(ISessionService sessionService) 13 | { 14 | _sessionService = sessionService; 15 | } 16 | 17 | public async ValueTask> Handle(GetUserSessionsQuery query, CancellationToken cancellationToken) 18 | { 19 | return await _sessionService.GetUserSessionsForAdminAsync(query.UserId.ToString(), cancellationToken); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Modules/Identity/Modules.Identity/IdentityMetrics.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.Metrics; 2 | 3 | namespace FSH.Modules.Identity; 4 | 5 | public sealed class IdentityMetrics : IDisposable 6 | { 7 | public const string MeterName = "FSH.Modules.Identity"; 8 | private readonly Counter _tokensGenerated; 9 | private readonly Meter _meter; 10 | 11 | public IdentityMetrics(IMeterFactory meterFactory) 12 | { 13 | _meter = meterFactory.Create(MeterName); 14 | 15 | _tokensGenerated = _meter.CreateCounter( 16 | name: "identity_tokens_generated", 17 | unit: "tokens", 18 | description: "Number of tokens generated"); 19 | } 20 | 21 | public void TokenGenerated(string emailId) 22 | { 23 | _tokensGenerated.Add(1, new KeyValuePair("user.email", emailId)); 24 | } 25 | 26 | public void Dispose() 27 | { 28 | _meter?.Dispose(); 29 | } 30 | } -------------------------------------------------------------------------------- /src/Modules/Identity/Modules.Identity/Features/v1/Users/GetUserProfile/GetCurrentUserProfileQueryHandler.cs: -------------------------------------------------------------------------------- 1 | using FSH.Modules.Identity.Contracts.DTOs; 2 | using FSH.Modules.Identity.Contracts.Services; 3 | using FSH.Modules.Identity.Contracts.v1.Users.GetUserProfile; 4 | using Mediator; 5 | 6 | namespace FSH.Modules.Identity.Features.v1.Users.GetUserProfile; 7 | 8 | public sealed class GetCurrentUserProfileQueryHandler : IQueryHandler 9 | { 10 | private readonly IUserService _userService; 11 | 12 | public GetCurrentUserProfileQueryHandler(IUserService userService) 13 | { 14 | _userService = userService; 15 | } 16 | 17 | public async ValueTask Handle(GetCurrentUserProfileQuery query, CancellationToken cancellationToken) 18 | { 19 | ArgumentNullException.ThrowIfNull(query); 20 | return await _userService.GetAsync(query.UserId, cancellationToken).ConfigureAwait(false); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Modules/Multitenancy/Modules.Multitenancy/Features/v1/TenantProvisioning/GetTenantProvisioningStatus/GetTenantProvisioningStatusQueryHandler.cs: -------------------------------------------------------------------------------- 1 | using FSH.Modules.Multitenancy.Contracts.Dtos; 2 | using FSH.Modules.Multitenancy.Contracts.v1.TenantProvisioning; 3 | using FSH.Modules.Multitenancy.Provisioning; 4 | using Mediator; 5 | 6 | namespace FSH.Modules.Multitenancy.Features.v1.TenantProvisioning.GetTenantProvisioningStatus; 7 | 8 | public sealed class GetTenantProvisioningStatusQueryHandler(ITenantProvisioningService provisioningService) 9 | : IQueryHandler 10 | { 11 | public async ValueTask Handle(GetTenantProvisioningStatusQuery query, CancellationToken cancellationToken) 12 | { 13 | ArgumentNullException.ThrowIfNull(query); 14 | return await provisioningService.GetStatusAsync(query.TenantId, cancellationToken).ConfigureAwait(false); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/BuildingBlocks/Blazor.UI/ServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | namespace FSH.Framework.Blazor.UI; 3 | 4 | public static class ServiceCollectionExtensions 5 | { 6 | public static IServiceCollection AddHeroUI(this IServiceCollection services) 7 | { 8 | services.AddMudServices(options => 9 | { 10 | options.SnackbarConfiguration.PositionClass = Defaults.Classes.Position.BottomRight; 11 | options.SnackbarConfiguration.ShowCloseIcon = true; 12 | options.SnackbarConfiguration.SnackbarVariant = Variant.Filled; 13 | options.SnackbarConfiguration.MaxDisplayedSnackbars = 3; 14 | }); 15 | 16 | services.AddMudPopoverService(); 17 | services.AddScoped(); 18 | services.AddSingleton(FSH.Framework.Blazor.UI.Theme.FshTheme.Build()); 19 | 20 | return services; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Modules/Auditing/Modules.Auditing/Persistence/AuditDbInitializer.cs: -------------------------------------------------------------------------------- 1 | using FSH.Framework.Persistence; 2 | using Microsoft.EntityFrameworkCore; 3 | using Microsoft.Extensions.Logging; 4 | 5 | namespace FSH.Modules.Auditing.Persistence; 6 | 7 | internal sealed class AuditDbInitializer( 8 | ILogger logger, 9 | AuditDbContext context) : IDbInitializer 10 | { 11 | public async Task MigrateAsync(CancellationToken cancellationToken) 12 | { 13 | if ((await context.Database.GetPendingMigrationsAsync(cancellationToken).ConfigureAwait(false)).Any()) 14 | { 15 | await context.Database.MigrateAsync(cancellationToken).ConfigureAwait(false); 16 | logger.LogInformation("[{Tenant}] applied database migrations for audit module", context.TenantInfo?.Identifier); 17 | } 18 | } 19 | 20 | public Task SeedAsync(CancellationToken cancellationToken) 21 | { 22 | return Task.CompletedTask; 23 | } 24 | } -------------------------------------------------------------------------------- /src/BuildingBlocks/Web/Versioning/Extensions.cs: -------------------------------------------------------------------------------- 1 | using Asp.Versioning; 2 | using Microsoft.Extensions.DependencyInjection; 3 | 4 | namespace FSH.Framework.Web.Versioning; 5 | 6 | public static class Extensions 7 | { 8 | public static IServiceCollection AddHeroVersioning(this IServiceCollection services) 9 | { 10 | services 11 | .AddApiVersioning(options => 12 | { 13 | options.ReportApiVersions = true; 14 | options.DefaultApiVersion = new ApiVersion(1, 0); 15 | options.AssumeDefaultVersionWhenUnspecified = true; 16 | options.ApiVersionReader = new UrlSegmentApiVersionReader(); 17 | }) 18 | .AddApiExplorer(options => 19 | { 20 | options.GroupNameFormat = "'v'VVV"; 21 | options.SubstituteApiVersionInUrl = true; 22 | }) 23 | .EnableApiVersionBinding(); 24 | return services; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Modules/Identity/Modules.Identity/Features/v1/Users/GetUserPermissions/GetCurrentUserPermissionsQueryHandler.cs: -------------------------------------------------------------------------------- 1 | using FSH.Modules.Identity.Contracts.Services; 2 | using FSH.Modules.Identity.Contracts.v1.Users.GetUserPermissions; 3 | using Mediator; 4 | 5 | namespace FSH.Modules.Identity.Features.v1.Users.GetUserPermissions; 6 | 7 | public sealed class GetCurrentUserPermissionsQueryHandler : IQueryHandler?> 8 | { 9 | private readonly IUserService _userService; 10 | 11 | public GetCurrentUserPermissionsQueryHandler(IUserService userService) 12 | { 13 | _userService = userService; 14 | } 15 | 16 | public async ValueTask?> Handle(GetCurrentUserPermissionsQuery query, CancellationToken cancellationToken) 17 | { 18 | ArgumentNullException.ThrowIfNull(query); 19 | return await _userService.GetPermissionsAsync(query.UserId, cancellationToken).ConfigureAwait(false); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/BuildingBlocks/Core/Exceptions/UnauthorizedException.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | 3 | namespace FSH.Framework.Core.Exceptions; 4 | /// 5 | /// Exception representing a 401 Unauthorized error (authentication failure). 6 | /// 7 | public class UnauthorizedException : CustomException 8 | { 9 | public UnauthorizedException() 10 | : base("Authentication failed.", Array.Empty(), HttpStatusCode.Unauthorized) 11 | { 12 | } 13 | 14 | public UnauthorizedException(string message) 15 | : base(message, Array.Empty(), HttpStatusCode.Unauthorized) 16 | { 17 | } 18 | 19 | public UnauthorizedException(string message, IEnumerable errors) 20 | : base(message, errors.ToList(), HttpStatusCode.Unauthorized) 21 | { 22 | } 23 | 24 | public UnauthorizedException(string message, Exception innerException) 25 | : base(message, innerException, HttpStatusCode.Unauthorized) 26 | { 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Modules/Identity/Modules.Identity/Features/v1/Roles/UpsertRole/UpsertRoleCommandHandler.cs: -------------------------------------------------------------------------------- 1 | using FSH.Modules.Identity.Contracts.DTOs; 2 | using FSH.Modules.Identity.Contracts.Services; 3 | using FSH.Modules.Identity.Contracts.v1.Roles.UpsertRole; 4 | using Mediator; 5 | 6 | namespace FSH.Modules.Identity.Features.v1.Roles.UpsertRole; 7 | 8 | public sealed class UpsertRoleCommandHandler : ICommandHandler 9 | { 10 | private readonly IRoleService _roleService; 11 | 12 | public UpsertRoleCommandHandler(IRoleService roleService) 13 | { 14 | _roleService = roleService; 15 | } 16 | 17 | public async ValueTask Handle(UpsertRoleCommand command, CancellationToken cancellationToken) 18 | { 19 | ArgumentNullException.ThrowIfNull(command); 20 | 21 | return await _roleService.CreateOrUpdateRoleAsync(command.Id, command.Name, command.Description ?? string.Empty) 22 | .ConfigureAwait(false); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Modules/Identity/Modules.Identity/Features/v1/Users/ResetPassword/ResetPasswordCommandHandler.cs: -------------------------------------------------------------------------------- 1 | using FSH.Modules.Identity.Contracts.Services; 2 | using FSH.Modules.Identity.Contracts.v1.Users.ResetPassword; 3 | using Mediator; 4 | 5 | namespace FSH.Modules.Identity.Features.v1.Users.ResetPassword; 6 | 7 | public sealed class ResetPasswordCommandHandler : ICommandHandler 8 | { 9 | private readonly IUserService _userService; 10 | 11 | public ResetPasswordCommandHandler(IUserService userService) 12 | { 13 | _userService = userService; 14 | } 15 | 16 | public async ValueTask Handle(ResetPasswordCommand command, CancellationToken cancellationToken) 17 | { 18 | ArgumentNullException.ThrowIfNull(command); 19 | 20 | await _userService.ResetPasswordAsync(command.Email, command.Password, command.Token, cancellationToken).ConfigureAwait(false); 21 | 22 | return "Password has been reset."; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/BuildingBlocks/Jobs/Jobs.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | FSH.Framework.Jobs 5 | FSH.Framework.Jobs 6 | FullStackHero.Framework.Jobs 7 | $(NoWarn);CA1031;S3376;S3993 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/Modules/Identity/Modules.Identity/Features/v1/Roles/GetRoleWithPermissions/GetRoleWithPermissionsQueryHandler.cs: -------------------------------------------------------------------------------- 1 | using FSH.Modules.Identity.Contracts.DTOs; 2 | using FSH.Modules.Identity.Contracts.Services; 3 | using FSH.Modules.Identity.Contracts.v1.Roles.GetRoleWithPermissions; 4 | using Mediator; 5 | 6 | namespace FSH.Modules.Identity.Features.v1.Roles.GetRoleWithPermissions; 7 | 8 | public sealed class GetRoleWithPermissionsQueryHandler : IQueryHandler 9 | { 10 | private readonly IRoleService _roleService; 11 | 12 | public GetRoleWithPermissionsQueryHandler(IRoleService roleService) 13 | { 14 | _roleService = roleService; 15 | } 16 | 17 | public async ValueTask Handle(GetRoleWithPermissionsQuery query, CancellationToken cancellationToken) 18 | { 19 | ArgumentNullException.ThrowIfNull(query); 20 | return await _roleService.GetWithPermissionsAsync(query.Id, cancellationToken).ConfigureAwait(false); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Modules/Identity/Modules.Identity/Features/v1/Roles/GetRoles/GetRolesEndpoint.cs: -------------------------------------------------------------------------------- 1 | using FSH.Framework.Shared.Identity; 2 | using FSH.Framework.Shared.Identity.Authorization; 3 | using FSH.Modules.Identity.Contracts.v1.Roles.GetRoles; 4 | using Mediator; 5 | using Microsoft.AspNetCore.Builder; 6 | using Microsoft.AspNetCore.Http; 7 | using Microsoft.AspNetCore.Routing; 8 | 9 | namespace FSH.Modules.Identity.Features.v1.Roles.GetRoles; 10 | 11 | public static class GetRolesEndpoint 12 | { 13 | public static RouteHandlerBuilder MapGetRolesEndpoint(this IEndpointRouteBuilder endpoints) 14 | { 15 | return endpoints.MapGet("/roles", (IMediator mediator, CancellationToken cancellationToken) => 16 | mediator.Send(new GetRolesQuery(), cancellationToken)) 17 | .WithName("ListRoles") 18 | .WithSummary("List all roles") 19 | .RequirePermission(IdentityPermissionConstants.Roles.View) 20 | .WithDescription("Retrieve all roles available for the current tenant."); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Modules/Identity/Modules.Identity/Features/v1/Users/GetUsers/GetUsersListEndpoint.cs: -------------------------------------------------------------------------------- 1 | using FSH.Framework.Shared.Identity; 2 | using FSH.Framework.Shared.Identity.Authorization; 3 | using FSH.Modules.Identity.Contracts.v1.Users.GetUsers; 4 | using Mediator; 5 | using Microsoft.AspNetCore.Builder; 6 | using Microsoft.AspNetCore.Http; 7 | using Microsoft.AspNetCore.Routing; 8 | 9 | namespace FSH.Modules.Identity.Features.v1.Users.GetUsers; 10 | 11 | public static class GetUsersListEndpoint 12 | { 13 | internal static RouteHandlerBuilder MapGetUsersListEndpoint(this IEndpointRouteBuilder endpoints) 14 | { 15 | return endpoints.MapGet("/users", (CancellationToken cancellationToken, IMediator mediator) => 16 | mediator.Send(new GetUsersQuery(), cancellationToken)) 17 | .WithName("ListUsers") 18 | .WithSummary("List users") 19 | .RequirePermission(IdentityPermissionConstants.Users.View) 20 | .WithDescription("Retrieve a list of users for the current tenant."); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Modules/Identity/Modules.Identity/Features/v1/Users/UserImageValidator.cs: -------------------------------------------------------------------------------- 1 | using FluentValidation; 2 | using FSH.Framework.Storage; 3 | using FSH.Framework.Storage.DTOs; 4 | 5 | namespace FSH.Modules.Identity.Features.v1.Users; 6 | 7 | public class UserImageValidator : AbstractValidator 8 | { 9 | public UserImageValidator() : this(FileType.Image) { } 10 | public UserImageValidator(FileType fileType) 11 | { 12 | var rules = FileTypeMetadata.GetRules(fileType); 13 | 14 | RuleFor(x => x.FileName) 15 | .NotEmpty() 16 | .Must(file => rules.AllowedExtensions.Any(ext => file.EndsWith(ext, StringComparison.OrdinalIgnoreCase))) 17 | .WithMessage($"Only these extensions are allowed: {string.Join(", ", rules.AllowedExtensions)}"); 18 | 19 | RuleFor(x => x.Data) 20 | .NotEmpty() 21 | .Must(data => data.Count <= rules.MaxSizeInMB * 1024 * 1024) 22 | .WithMessage($"File must be <= {rules.MaxSizeInMB} MB."); 23 | } 24 | } -------------------------------------------------------------------------------- /src/Modules/Multitenancy/Modules.Multitenancy/Features/v1/ResetTenantTheme/ResetTenantThemeCommandHandler.cs: -------------------------------------------------------------------------------- 1 | using Finbuckle.MultiTenant.Abstractions; 2 | using FSH.Framework.Shared.Multitenancy; 3 | using FSH.Modules.Multitenancy.Contracts; 4 | using FSH.Modules.Multitenancy.Contracts.v1.ResetTenantTheme; 5 | using Mediator; 6 | 7 | namespace FSH.Modules.Multitenancy.Features.v1.ResetTenantTheme; 8 | 9 | public sealed class ResetTenantThemeCommandHandler( 10 | ITenantThemeService themeService, 11 | IMultiTenantContextAccessor tenantAccessor) 12 | : ICommandHandler 13 | { 14 | public async ValueTask Handle(ResetTenantThemeCommand command, CancellationToken cancellationToken) 15 | { 16 | var tenantId = tenantAccessor.MultiTenantContext?.TenantInfo?.Id 17 | ?? throw new InvalidOperationException("No tenant context available"); 18 | 19 | await themeService.ResetThemeAsync(tenantId, cancellationToken); 20 | 21 | return Unit.Value; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/BuildingBlocks/Web/Security/SecurityHeadersOptions.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.Framework.Web.Security; 2 | 3 | public sealed class SecurityHeadersOptions 4 | { 5 | /// 6 | /// Enables or disables the security headers middleware entirely. 7 | /// 8 | public bool Enabled { get; set; } = true; 9 | 10 | /// 11 | /// Paths to bypass (e.g., OpenAPI/Scalar assets). 12 | /// 13 | public string[] ExcludedPaths { get; set; } = ["/scalar", "/openapi"]; 14 | 15 | /// 16 | /// Whether to allow inline styles in CSP (default true for MudBlazor/Scalar compatibility). 17 | /// 18 | public bool AllowInlineStyles { get; set; } = true; 19 | 20 | /// 21 | /// Additional script sources to append to CSP. 22 | /// 23 | public string[] ScriptSources { get; set; } = []; 24 | 25 | /// 26 | /// Additional style sources to append to CSP. 27 | /// 28 | public string[] StyleSources { get; set; } = []; 29 | } 30 | -------------------------------------------------------------------------------- /src/Modules/Multitenancy/Modules.Multitenancy/MultitenancyOptions.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.Modules.Multitenancy; 2 | 3 | /// 4 | /// Options controlling multitenancy behavior at startup. 5 | /// 6 | public sealed class MultitenancyOptions 7 | { 8 | /// 9 | /// When true, runs per-tenant migrations and seeding for all registered IDbInitializer 10 | /// implementations during UseHeroMultiTenantDatabases. Recommended for development and demos. 11 | /// In production, prefer running migrations explicitly and leaving this disabled for faster startup. 12 | /// 13 | public bool RunTenantMigrationsOnStartup { get; set; } 14 | 15 | /// 16 | /// When true, enqueues tenant provisioning (migrate/seed) jobs on startup for tenants that have not completed provisioning. 17 | /// Useful to ensure the root tenant is provisioned automatically on first run when startup migrations are disabled. 18 | /// 19 | public bool AutoProvisionOnStartup { get; set; } = true; 20 | } 21 | -------------------------------------------------------------------------------- /src/Modules/Identity/Modules.Identity/Features/v1/Roles/GetRoleById/GetRoleByIdEndpoint.cs: -------------------------------------------------------------------------------- 1 | using FSH.Framework.Shared.Identity; 2 | using FSH.Framework.Shared.Identity.Authorization; 3 | using FSH.Modules.Identity.Contracts.v1.Roles.GetRole; 4 | using Mediator; 5 | using Microsoft.AspNetCore.Builder; 6 | using Microsoft.AspNetCore.Http; 7 | using Microsoft.AspNetCore.Routing; 8 | 9 | namespace FSH.Modules.Identity.Features.v1.Roles.GetRoleById; 10 | 11 | public static class GetRoleByIdEndpoint 12 | { 13 | public static RouteHandlerBuilder MapGetRoleByIdEndpoint(this IEndpointRouteBuilder endpoints) 14 | { 15 | return endpoints.MapGet("/roles/{id:guid}", (string id, IMediator mediator, CancellationToken cancellationToken) => 16 | mediator.Send(new GetRoleQuery(id), cancellationToken)) 17 | .WithName("GetRole") 18 | .WithSummary("Get role by ID") 19 | .RequirePermission(IdentityPermissionConstants.Roles.View) 20 | .WithDescription("Retrieve details of a specific role by its unique identifier."); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Modules/Identity/Modules.Identity/Features/v1/Users/GetUserById/GetUserByIdEndpoint.cs: -------------------------------------------------------------------------------- 1 | using FSH.Framework.Shared.Identity; 2 | using FSH.Framework.Shared.Identity.Authorization; 3 | using FSH.Modules.Identity.Contracts.v1.Users.GetUser; 4 | using Mediator; 5 | using Microsoft.AspNetCore.Builder; 6 | using Microsoft.AspNetCore.Http; 7 | using Microsoft.AspNetCore.Routing; 8 | 9 | namespace FSH.Modules.Identity.Features.v1.Users.GetUserById; 10 | 11 | public static class GetUserByIdEndpoint 12 | { 13 | internal static RouteHandlerBuilder MapGetUserByIdEndpoint(this IEndpointRouteBuilder endpoints) 14 | { 15 | return endpoints.MapGet("/users/{id:guid}", (string id, IMediator mediator, CancellationToken cancellationToken) => 16 | mediator.Send(new GetUserQuery(id), cancellationToken)) 17 | .WithName("GetUser") 18 | .WithSummary("Get user by ID") 19 | .RequirePermission(IdentityPermissionConstants.Users.View) 20 | .WithDescription("Retrieve a user's profile details by unique user identifier."); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Modules/Identity/Modules.Identity/Features/v1/Users/ConfirmEmail/ConfirmEmailEndpoint.cs: -------------------------------------------------------------------------------- 1 | using FSH.Modules.Identity.Contracts.v1.Users.ConfirmEmail; 2 | using Mediator; 3 | using Microsoft.AspNetCore.Builder; 4 | using Microsoft.AspNetCore.Http; 5 | using Microsoft.AspNetCore.Routing; 6 | 7 | namespace FSH.Modules.Identity.Features.v1.Users.ConfirmEmail; 8 | 9 | public static class ConfirmEmailEndpoint 10 | { 11 | internal static RouteHandlerBuilder MapConfirmEmailEndpoint(this IEndpointRouteBuilder endpoints) 12 | { 13 | return endpoints.MapGet("/confirm-email", async (string userId, string code, string tenant, IMediator mediator, CancellationToken cancellationToken) => 14 | { 15 | var result = await mediator.Send(new ConfirmEmailCommand(userId, code, tenant), cancellationToken); 16 | return Results.Ok(result); 17 | }) 18 | .WithName("ConfirmEmail") 19 | .WithSummary("Confirm user email") 20 | .WithDescription("Confirm a user's email address.") 21 | .AllowAnonymous(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Modules/Identity/Modules.Identity/Features/v1/Users/FshUser.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Identity; 2 | using FSH.Modules.Identity.Features.v1.Users.PasswordHistory; 3 | 4 | namespace FSH.Modules.Identity.Features.v1.Users; 5 | 6 | public class FshUser : IdentityUser 7 | { 8 | public string? FirstName { get; set; } 9 | public string? LastName { get; set; } 10 | public Uri? ImageUrl { get; set; } 11 | public bool IsActive { get; set; } 12 | public string? RefreshToken { get; set; } 13 | public DateTime RefreshTokenExpiryTime { get; set; } 14 | 15 | public string? ObjectId { get; set; } 16 | 17 | /// Timestamp when the user last changed their password 18 | public DateTime LastPasswordChangeDate { get; set; } = DateTime.UtcNow; 19 | 20 | // Navigation property for password history 21 | public virtual ICollection PasswordHistories { get; set; } = new List(); 22 | } -------------------------------------------------------------------------------- /src/Tests/Multitenacy.Tests/Multitenacy.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net10.0 4 | false 5 | false 6 | enable 7 | enable 8 | $(NoWarn);S125;S3261 9 | 10 | 11 | 12 | 13 | 14 | 15 | all 16 | runtime; build; native; contentfiles; analyzers; buildtransitive 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/Modules/Auditing/Modules.Auditing/Persistence/AuditRecordConfiguration.cs: -------------------------------------------------------------------------------- 1 | using Finbuckle.MultiTenant.EntityFrameworkCore.Extensions; 2 | using Microsoft.EntityFrameworkCore; 3 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 4 | 5 | namespace FSH.Modules.Auditing.Persistence; 6 | 7 | public class AuditRecordConfiguration : IEntityTypeConfiguration 8 | { 9 | public void Configure(EntityTypeBuilder builder) 10 | { 11 | ArgumentNullException.ThrowIfNull(builder); 12 | builder.ToTable("AuditRecords", "audit"); 13 | builder.IsMultiTenant(); 14 | builder.HasKey(x => x.Id); 15 | builder.Property(x => x.EventType).HasConversion(); 16 | builder.Property(x => x.Severity).HasConversion(); 17 | builder.Property(x => x.Tags).HasConversion(); 18 | builder.Property(x => x.PayloadJson).HasColumnType("jsonb"); 19 | builder.HasIndex(x => x.TenantId); 20 | builder.HasIndex(x => x.EventType); 21 | builder.HasIndex(x => x.OccurredAtUtc); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Modules/Identity/Modules.Identity/Features/v1/Users/GetUserRoles/GetUserRolesEndpoint.cs: -------------------------------------------------------------------------------- 1 | using FSH.Framework.Shared.Identity; 2 | using FSH.Framework.Shared.Identity.Authorization; 3 | using FSH.Modules.Identity.Contracts.v1.Users.GetUserRoles; 4 | using Mediator; 5 | using Microsoft.AspNetCore.Builder; 6 | using Microsoft.AspNetCore.Http; 7 | using Microsoft.AspNetCore.Routing; 8 | 9 | namespace FSH.Modules.Identity.Features.v1.Users.GetUserRoles; 10 | 11 | public static class GetUserRolesEndpoint 12 | { 13 | internal static RouteHandlerBuilder MapGetUserRolesEndpoint(this IEndpointRouteBuilder endpoints) 14 | { 15 | return endpoints.MapGet("/users/{id:guid}/roles", (string id, IMediator mediator, CancellationToken cancellationToken) => 16 | mediator.Send(new GetUserRolesQuery(id), cancellationToken)) 17 | .WithName("GetUserRoles") 18 | .WithSummary("Get user roles") 19 | .RequirePermission(IdentityPermissionConstants.Users.View) 20 | .WithDescription("Retrieve the roles assigned to a specific user."); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Playground/Playground.Blazor/Services/CookieAuthenticationStateProvider.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Components.Authorization; 2 | using Microsoft.AspNetCore.Components.Server; 3 | 4 | namespace FSH.Playground.Blazor.Services; 5 | 6 | /// 7 | /// Simple authentication state provider that reads from cookie authentication. 8 | /// Uses the built-in ServerAuthenticationStateProvider which automatically reads HttpContext.User. 9 | /// 10 | /// 11 | /// This class intentionally has no custom logic - it inherits all behavior from ServerAuthenticationStateProvider, 12 | /// which automatically reads from HttpContext.User populated by the ASP.NET Core Cookie Authentication middleware. 13 | /// The class exists to provide a named type for DI registration and potential future customization. 14 | /// 15 | #pragma warning disable S2094 // Classes should not be empty - intentionally inherits all behavior from base class 16 | internal sealed class CookieAuthenticationStateProvider : ServerAuthenticationStateProvider; 17 | #pragma warning restore S2094 18 | -------------------------------------------------------------------------------- /terraform/modules/alb/outputs.tf: -------------------------------------------------------------------------------- 1 | output "arn" { 2 | description = "The ARN of the load balancer" 3 | value = aws_lb.this.arn 4 | } 5 | 6 | output "id" { 7 | description = "The ID of the load balancer" 8 | value = aws_lb.this.id 9 | } 10 | 11 | output "dns_name" { 12 | description = "The DNS name of the load balancer" 13 | value = aws_lb.this.dns_name 14 | } 15 | 16 | output "zone_id" { 17 | description = "The canonical hosted zone ID of the load balancer" 18 | value = aws_lb.this.zone_id 19 | } 20 | 21 | output "http_listener_arn" { 22 | description = "The ARN of the HTTP listener" 23 | value = aws_lb_listener.http.arn 24 | } 25 | 26 | output "https_listener_arn" { 27 | description = "The ARN of the HTTPS listener (if enabled)" 28 | value = var.enable_https ? aws_lb_listener.https[0].arn : null 29 | } 30 | 31 | output "listener_arn" { 32 | description = "The ARN of the primary listener (HTTPS if enabled, otherwise HTTP)" 33 | value = var.enable_https ? aws_lb_listener.https[0].arn : aws_lb_listener.http.arn 34 | } 35 | -------------------------------------------------------------------------------- /src/BuildingBlocks/Caching/Caching.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | FSH.Framework.Caching 5 | FSH.Framework.Caching 6 | FullStackHero.Framework.Caching 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/Modules/Auditing/Modules.Auditing/Core/AuditingConfigurator.cs: -------------------------------------------------------------------------------- 1 | // Add this hosted service class once in your auditing module 2 | using FSH.Modules.Auditing.Contracts; 3 | using Microsoft.Extensions.Hosting; 4 | 5 | namespace FSH.Modules.Auditing; 6 | 7 | public sealed class AuditingConfigurator : IHostedService 8 | { 9 | private readonly IAuditPublisher _publisher; 10 | private readonly IAuditSerializer _serializer; 11 | private readonly IEnumerable _enrichers; 12 | 13 | public AuditingConfigurator( 14 | IAuditPublisher publisher, 15 | IAuditSerializer serializer, 16 | IEnumerable enrichers) 17 | { 18 | _publisher = publisher; 19 | _serializer = serializer; 20 | _enrichers = enrichers; 21 | } 22 | 23 | public Task StartAsync(CancellationToken cancellationToken) 24 | { 25 | Audit.Configure(_publisher, _serializer, _enrichers); 26 | return Task.CompletedTask; 27 | } 28 | public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; 29 | } 30 | 31 | -------------------------------------------------------------------------------- /src/Modules/Identity/Modules.Identity/Features/v1/Sessions/GetMySessions/GetMySessionsEndpoint.cs: -------------------------------------------------------------------------------- 1 | using FSH.Framework.Shared.Identity; 2 | using FSH.Framework.Shared.Identity.Authorization; 3 | using FSH.Modules.Identity.Contracts.v1.Sessions.GetMySessions; 4 | using Mediator; 5 | using Microsoft.AspNetCore.Builder; 6 | using Microsoft.AspNetCore.Http; 7 | using Microsoft.AspNetCore.Routing; 8 | 9 | namespace FSH.Modules.Identity.Features.v1.Sessions.GetMySessions; 10 | 11 | public static class GetMySessionsEndpoint 12 | { 13 | internal static RouteHandlerBuilder MapGetMySessionsEndpoint(this IEndpointRouteBuilder endpoints) 14 | { 15 | return endpoints.MapGet("/sessions/me", (CancellationToken cancellationToken, IMediator mediator) => 16 | mediator.Send(new GetMySessionsQuery(), cancellationToken)) 17 | .WithName("GetMySessions") 18 | .WithSummary("Get current user's sessions") 19 | .RequirePermission(IdentityPermissionConstants.Sessions.View) 20 | .WithDescription("Retrieve all active sessions for the currently authenticated user."); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Modules/Identity/Modules.Identity/Features/v1/Users/ChangePassword/ChangePasswordEndpoint.cs: -------------------------------------------------------------------------------- 1 | using FSH.Modules.Identity.Contracts.v1.Users.ChangePassword; 2 | using Mediator; 3 | using Microsoft.AspNetCore.Builder; 4 | using Microsoft.AspNetCore.Http; 5 | using Microsoft.AspNetCore.Mvc; 6 | using Microsoft.AspNetCore.Routing; 7 | 8 | namespace FSH.Modules.Identity.Features.v1.Users.ChangePassword; 9 | 10 | public static class ChangePasswordEndpoint 11 | { 12 | internal static RouteHandlerBuilder MapChangePasswordEndpoint(this IEndpointRouteBuilder endpoints) 13 | { 14 | return endpoints.MapPost("/change-password", async ( 15 | [FromBody] ChangePasswordCommand command, 16 | IMediator mediator, 17 | CancellationToken cancellationToken) => 18 | { 19 | var result = await mediator.Send(command, cancellationToken); 20 | return Results.Ok(result); 21 | }) 22 | .WithName("ChangePassword") 23 | .WithSummary("Change password") 24 | .WithDescription("Change the current user's password."); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/BuildingBlocks/Persistence/Specifications/SpecificationOfTResult.cs: -------------------------------------------------------------------------------- 1 | using FSH.Framework.Persistence.Specifications; 2 | using System.Linq.Expressions; 3 | 4 | namespace FSH.Framework.Persistence; 5 | 6 | /// 7 | /// Base specification that composes a query for and 8 | /// projects it into . 9 | /// 10 | /// The root entity type. 11 | /// The projected result type. 12 | public abstract class Specification : Specification, ISpecification 13 | where T : class 14 | { 15 | public Expression> Selector { get; private set; } = default!; 16 | 17 | /// 18 | /// Configures the projection applied at the end of the query pipeline. 19 | /// 20 | /// Projection expression. 21 | protected void Select(Expression> selector) 22 | { 23 | ArgumentNullException.ThrowIfNull(selector); 24 | Selector = selector; 25 | } 26 | } 27 | 28 | -------------------------------------------------------------------------------- /src/Modules/Auditing/Modules.Auditing/Persistence/AuditDbContext.cs: -------------------------------------------------------------------------------- 1 | using Finbuckle.MultiTenant.Abstractions; 2 | using FSH.Framework.Persistence.Context; 3 | using FSH.Framework.Shared.Multitenancy; 4 | using FSH.Framework.Shared.Persistence; 5 | using Microsoft.EntityFrameworkCore; 6 | using Microsoft.Extensions.Hosting; 7 | using Microsoft.Extensions.Options; 8 | 9 | namespace FSH.Modules.Auditing.Persistence; 10 | 11 | public sealed class AuditDbContext : BaseDbContext 12 | { 13 | public AuditDbContext( 14 | IMultiTenantContextAccessor multiTenantContextAccessor, 15 | DbContextOptions options, 16 | IOptions settings, 17 | IHostEnvironment environment) : base(multiTenantContextAccessor, options, settings, environment) { } 18 | 19 | public DbSet AuditRecords => Set(); 20 | protected override void OnModelCreating(ModelBuilder modelBuilder) 21 | { 22 | ArgumentNullException.ThrowIfNull(modelBuilder); 23 | modelBuilder.ApplyConfigurationsFromAssembly(typeof(AuditDbContext).Assembly); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/BuildingBlocks/Mailing/Extensions.cs: -------------------------------------------------------------------------------- 1 | using FSH.Framework.Mailing.Services; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using Microsoft.Extensions.Options; 4 | 5 | namespace FSH.Framework.Mailing; 6 | 7 | public static class Extensions 8 | { 9 | public static IServiceCollection AddHeroMailing(this IServiceCollection services) 10 | { 11 | services.AddTransient(); 12 | services 13 | .AddOptions() 14 | .BindConfiguration(nameof(MailOptions)) 15 | .Validate(o => !string.IsNullOrWhiteSpace(o.From), "MailOptions: From is required.") 16 | .Validate(o => !string.IsNullOrWhiteSpace(o.Host), "MailOptions: Host is required.") 17 | .Validate(o => o.Port > 0, "MailOptions: Port must be greater than zero.") 18 | .Validate(o => !string.IsNullOrWhiteSpace(o.UserName), "MailOptions: UserName is required.") 19 | .Validate(o => !string.IsNullOrWhiteSpace(o.Password), "MailOptions: Password is required.") 20 | .ValidateOnStart(); 21 | return services; 22 | } 23 | } -------------------------------------------------------------------------------- /src/Modules/Auditing/Modules.Auditing.Contracts/v1/GetAudits/GetAuditsQuery.cs: -------------------------------------------------------------------------------- 1 | using FSH.Framework.Shared.Persistence; 2 | using FSH.Modules.Auditing.Contracts; 3 | using FSH.Modules.Auditing.Contracts.Dtos; 4 | using Mediator; 5 | 6 | namespace FSH.Modules.Auditing.Contracts.v1.GetAudits; 7 | 8 | public sealed class GetAuditsQuery : IPagedQuery, IQuery> 9 | { 10 | public int? PageNumber { get; set; } 11 | 12 | public int? PageSize { get; set; } 13 | 14 | public string? Sort { get; set; } 15 | 16 | public DateTime? FromUtc { get; set; } 17 | 18 | public DateTime? ToUtc { get; set; } 19 | 20 | public string? TenantId { get; set; } 21 | 22 | public string? UserId { get; set; } 23 | 24 | public AuditEventType? EventType { get; set; } 25 | 26 | public AuditSeverity? Severity { get; set; } 27 | 28 | public AuditTag? Tags { get; set; } 29 | 30 | public string? Source { get; set; } 31 | 32 | public string? CorrelationId { get; set; } 33 | 34 | public string? TraceId { get; set; } 35 | 36 | public string? Search { get; set; } 37 | } 38 | 39 | -------------------------------------------------------------------------------- /src/Modules/Auditing/Modules.Auditing/Features/v1/GetAuditById/GetAuditByIdEndpoint.cs: -------------------------------------------------------------------------------- 1 | using FSH.Framework.Shared.Identity; 2 | using FSH.Framework.Shared.Identity.Authorization; 3 | using FSH.Modules.Auditing.Contracts.v1.GetAuditById; 4 | using Mediator; 5 | using Microsoft.AspNetCore.Builder; 6 | using Microsoft.AspNetCore.Http; 7 | using Microsoft.AspNetCore.Routing; 8 | 9 | namespace FSH.Modules.Auditing.Features.v1.GetAuditById; 10 | 11 | public static class GetAuditByIdEndpoint 12 | { 13 | public static RouteHandlerBuilder MapGetAuditByIdEndpoint(this IEndpointRouteBuilder group) 14 | { 15 | return group.MapGet( 16 | "/{id:guid}", 17 | async (Guid id, IMediator mediator, CancellationToken cancellationToken) => 18 | await mediator.Send(new GetAuditByIdQuery(id), cancellationToken)) 19 | .WithName("GetAuditById") 20 | .WithSummary("Get audit event by ID") 21 | .WithDescription("Retrieve full details for a single audit event.") 22 | .RequirePermission(AuditingPermissionConstants.View); 23 | } 24 | } 25 | 26 | -------------------------------------------------------------------------------- /src/BuildingBlocks/Shared/Multitenancy/MultitenancyConstants.cs: -------------------------------------------------------------------------------- 1 | namespace FSH.Framework.Shared.Multitenancy; 2 | 3 | public static class MultitenancyConstants 4 | { 5 | public static class Root 6 | { 7 | public const string Id = "root"; 8 | public const string Name = "Root"; 9 | public const string EmailAddress = "admin@root.com"; 10 | public const string DefaultProfilePicture = "assets/defaults/profile-picture.webp"; 11 | public const string Issuer = "mukesh.murugan"; 12 | } 13 | 14 | public const string DefaultPassword = "123Pa$$word!"; 15 | public const string Identifier = "tenant"; 16 | public const string Schema = "tenant"; 17 | 18 | public static class Permissions 19 | { 20 | public const string View = "Permissions.Tenants.View"; 21 | public const string Create = "Permissions.Tenants.Create"; 22 | public const string Update = "Permissions.Tenants.Update"; 23 | public const string ViewTheme = "Permissions.Tenants.ViewTheme"; 24 | public const string UpdateTheme = "Permissions.Tenants.UpdateTheme"; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Modules/Multitenancy/Modules.Multitenancy/Features/v1/UpdateTenantTheme/UpdateTenantThemeCommandHandler.cs: -------------------------------------------------------------------------------- 1 | using Finbuckle.MultiTenant.Abstractions; 2 | using FSH.Framework.Shared.Multitenancy; 3 | using FSH.Modules.Multitenancy.Contracts; 4 | using FSH.Modules.Multitenancy.Contracts.v1.UpdateTenantTheme; 5 | using Mediator; 6 | 7 | namespace FSH.Modules.Multitenancy.Features.v1.UpdateTenantTheme; 8 | 9 | public sealed class UpdateTenantThemeCommandHandler( 10 | ITenantThemeService themeService, 11 | IMultiTenantContextAccessor tenantAccessor) 12 | : ICommandHandler 13 | { 14 | public async ValueTask Handle(UpdateTenantThemeCommand command, CancellationToken cancellationToken) 15 | { 16 | ArgumentNullException.ThrowIfNull(command); 17 | 18 | var tenantId = tenantAccessor.MultiTenantContext?.TenantInfo?.Id 19 | ?? throw new InvalidOperationException("No tenant context available"); 20 | 21 | await themeService.UpdateThemeAsync(tenantId, command.Theme, cancellationToken); 22 | 23 | return Unit.Value; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Modules/Identity/Modules.Identity/Features/v1/Sessions/GetMySessions/GetMySessionsQueryHandler.cs: -------------------------------------------------------------------------------- 1 | using FSH.Framework.Core.Context; 2 | using FSH.Modules.Identity.Contracts.DTOs; 3 | using FSH.Modules.Identity.Contracts.Services; 4 | using FSH.Modules.Identity.Contracts.v1.Sessions.GetMySessions; 5 | using Mediator; 6 | 7 | namespace FSH.Modules.Identity.Features.v1.Sessions.GetMySessions; 8 | 9 | public sealed class GetMySessionsQueryHandler : IQueryHandler> 10 | { 11 | private readonly ISessionService _sessionService; 12 | private readonly ICurrentUser _currentUser; 13 | 14 | public GetMySessionsQueryHandler(ISessionService sessionService, ICurrentUser currentUser) 15 | { 16 | _sessionService = sessionService; 17 | _currentUser = currentUser; 18 | } 19 | 20 | public async ValueTask> Handle(GetMySessionsQuery query, CancellationToken cancellationToken) 21 | { 22 | var userId = _currentUser.GetUserId().ToString(); 23 | return await _sessionService.GetUserSessionsAsync(userId, cancellationToken); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Modules/Identity/Modules.Identity/Features/v1/Users/DeleteUser/DeleteUserEndpoint.cs: -------------------------------------------------------------------------------- 1 | using FSH.Framework.Shared.Identity; 2 | using FSH.Framework.Shared.Identity.Authorization; 3 | using FSH.Modules.Identity.Contracts.v1.Users.DeleteUser; 4 | using Mediator; 5 | using Microsoft.AspNetCore.Builder; 6 | using Microsoft.AspNetCore.Http; 7 | using Microsoft.AspNetCore.Routing; 8 | 9 | namespace FSH.Modules.Identity.Features.v1.Users.DeleteUser; 10 | 11 | public static class DeleteUserEndpoint 12 | { 13 | internal static RouteHandlerBuilder MapDeleteUserEndpoint(this IEndpointRouteBuilder endpoints) 14 | { 15 | return endpoints.MapDelete("/users/{id:guid}", async (string id, IMediator mediator, CancellationToken cancellationToken) => 16 | { 17 | await mediator.Send(new DeleteUserCommand(id), cancellationToken); 18 | return Results.NoContent(); 19 | }) 20 | .WithName("DeleteUser") 21 | .WithSummary("Delete user") 22 | .RequirePermission(IdentityPermissionConstants.Users.Delete) 23 | .WithDescription("Delete a user by unique identifier."); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Modules/Identity/Modules.Identity/Features/v1/Users/UpdateUser/UpdateUserCommandHandler.cs: -------------------------------------------------------------------------------- 1 | using FSH.Modules.Identity.Contracts.Services; 2 | using FSH.Modules.Identity.Contracts.v1.Users.UpdateUser; 3 | using Mediator; 4 | 5 | namespace FSH.Modules.Identity.Features.v1.Users.UpdateUser; 6 | 7 | public sealed class UpdateUserCommandHandler : ICommandHandler 8 | { 9 | private readonly IUserService _userService; 10 | 11 | public UpdateUserCommandHandler(IUserService userService) 12 | { 13 | _userService = userService; 14 | } 15 | 16 | public async ValueTask Handle(UpdateUserCommand command, CancellationToken cancellationToken) 17 | { 18 | ArgumentNullException.ThrowIfNull(command); 19 | 20 | await _userService.UpdateAsync( 21 | command.Id, 22 | command.FirstName ?? string.Empty, 23 | command.LastName ?? string.Empty, 24 | command.PhoneNumber ?? string.Empty, 25 | command.Image!, 26 | command.DeleteCurrentImage).ConfigureAwait(false); 27 | 28 | return Unit.Value; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /terraform/apps/playground/envs/prod/us-east-1/prod.us-east-1.tfvars: -------------------------------------------------------------------------------- 1 | environment = "prod" 2 | region = "us-east-1" 3 | 4 | vpc_cidr_block = "10.30.0.0/16" 5 | 6 | public_subnets = { 7 | a = { 8 | cidr_block = "10.30.0.0/24" 9 | az = "us-east-1a" 10 | } 11 | b = { 12 | cidr_block = "10.30.1.0/24" 13 | az = "us-east-1b" 14 | } 15 | } 16 | 17 | private_subnets = { 18 | a = { 19 | cidr_block = "10.30.10.0/24" 20 | az = "us-east-1a" 21 | } 22 | b = { 23 | cidr_block = "10.30.11.0/24" 24 | az = "us-east-1b" 25 | } 26 | } 27 | 28 | app_s3_bucket_name = "CHANGE_ME-app-prod-us-east-1" 29 | 30 | db_name = "fshdb" 31 | db_username = "fshadmin" 32 | db_password = "CHANGE_ME_STRONG_PASSWORD" 33 | 34 | api_container_image = "CHANGE_ME_API_IMAGE" 35 | api_container_port = 8080 36 | api_cpu = "512" 37 | api_memory = "1024" 38 | api_desired_count = 3 39 | 40 | blazor_container_image = "CHANGE_ME_BLAZOR_IMAGE" 41 | blazor_container_port = 8080 42 | blazor_cpu = "512" 43 | blazor_memory = "1024" 44 | blazor_desired_count = 3 45 | -------------------------------------------------------------------------------- /src/Playground/Playground.Blazor/Components/App.razor: -------------------------------------------------------------------------------- 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/BuildingBlocks/Persistence/Persistence.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | FSH.Framework.Persistence 5 | FSH.Framework.Persistence 6 | FullStackHero.Framework.Persistence 7 | $(NoWarn);S4144;CS0618 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/Modules/Auditing/Modules.Auditing/Features/v1/GetAudits/GetAuditsEndpoint.cs: -------------------------------------------------------------------------------- 1 | using FSH.Framework.Shared.Identity; 2 | using FSH.Framework.Shared.Identity.Authorization; 3 | using FSH.Modules.Auditing.Contracts.v1.GetAudits; 4 | using Mediator; 5 | using Microsoft.AspNetCore.Builder; 6 | using Microsoft.AspNetCore.Http; 7 | using Microsoft.AspNetCore.Mvc; 8 | using Microsoft.AspNetCore.Routing; 9 | 10 | namespace FSH.Modules.Auditing.Features.v1.GetAudits; 11 | 12 | public static class GetAuditsEndpoint 13 | { 14 | public static RouteHandlerBuilder MapGetAuditsEndpoint(this IEndpointRouteBuilder group) 15 | { 16 | return group.MapGet( 17 | "/", 18 | async ([AsParameters] GetAuditsQuery query, IMediator mediator, CancellationToken cancellationToken) => 19 | await mediator.Send(query, cancellationToken)) 20 | .WithName("GetAudits") 21 | .WithSummary("List and search audit events") 22 | .WithDescription("Retrieve audit events with pagination and filters.") 23 | .RequirePermission(AuditingPermissionConstants.View); 24 | } 25 | } 26 | 27 | -------------------------------------------------------------------------------- /terraform/apps/playground/envs/staging/us-east-1/staging.us-east-1.tfvars: -------------------------------------------------------------------------------- 1 | environment = "staging" 2 | region = "us-east-1" 3 | 4 | vpc_cidr_block = "10.20.0.0/16" 5 | 6 | public_subnets = { 7 | a = { 8 | cidr_block = "10.20.0.0/24" 9 | az = "us-east-1a" 10 | } 11 | b = { 12 | cidr_block = "10.20.1.0/24" 13 | az = "us-east-1b" 14 | } 15 | } 16 | 17 | private_subnets = { 18 | a = { 19 | cidr_block = "10.20.10.0/24" 20 | az = "us-east-1a" 21 | } 22 | b = { 23 | cidr_block = "10.20.11.0/24" 24 | az = "us-east-1b" 25 | } 26 | } 27 | 28 | app_s3_bucket_name = "CHANGE_ME-app-staging-us-east-1" 29 | 30 | db_name = "fshdb" 31 | db_username = "fshadmin" 32 | db_password = "CHANGE_ME_STRONG_PASSWORD" 33 | 34 | api_container_image = "CHANGE_ME_API_IMAGE" 35 | api_container_port = 8080 36 | api_cpu = "256" 37 | api_memory = "512" 38 | api_desired_count = 2 39 | 40 | blazor_container_image = "CHANGE_ME_BLAZOR_IMAGE" 41 | blazor_container_port = 8080 42 | blazor_cpu = "256" 43 | blazor_memory = "512" 44 | blazor_desired_count = 2 45 | -------------------------------------------------------------------------------- /src/BuildingBlocks/Eventing/Eventing.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net10.0 5 | enable 6 | enable 7 | FSH.Framework.Eventing 8 | FSH.Framework.Eventing 9 | FullStackHero.Framework.Eventing 10 | $(NoWarn);CA1711;CA1716;CA1031;S2139;S1066 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/Modules/Identity/Modules.Identity/Features/v1/Roles/DeleteRole/DeleteRoleEndpoint.cs: -------------------------------------------------------------------------------- 1 | using FSH.Framework.Shared.Identity; 2 | using FSH.Framework.Shared.Identity.Authorization; 3 | using FSH.Modules.Identity.Contracts.v1.Roles.DeleteRole; 4 | using Mediator; 5 | using Microsoft.AspNetCore.Builder; 6 | using Microsoft.AspNetCore.Http; 7 | using Microsoft.AspNetCore.Routing; 8 | 9 | namespace FSH.Modules.Identity.Features.v1.Roles.DeleteRole; 10 | 11 | public static class DeleteRoleEndpoint 12 | { 13 | public static RouteHandlerBuilder MapDeleteRoleEndpoint(this IEndpointRouteBuilder endpoints) 14 | { 15 | return endpoints.MapDelete("/roles/{id:guid}", async (string id, IMediator mediator, CancellationToken cancellationToken) => 16 | { 17 | await mediator.Send(new DeleteRoleCommand(id), cancellationToken); 18 | return Results.NoContent(); 19 | }) 20 | .WithName("DeleteRole") 21 | .WithSummary("Delete role by ID") 22 | .RequirePermission(IdentityPermissionConstants.Roles.Delete) 23 | .WithDescription("Remove an existing role by its unique identifier."); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Modules/Identity/Modules.Identity/Features/v1/Roles/GetRoleWithPermissions/GetRolePermissionsEndpoint.cs: -------------------------------------------------------------------------------- 1 | using FSH.Framework.Shared.Identity; 2 | using FSH.Framework.Shared.Identity.Authorization; 3 | using FSH.Modules.Identity.Contracts.v1.Roles.GetRoleWithPermissions; 4 | using Mediator; 5 | using Microsoft.AspNetCore.Builder; 6 | using Microsoft.AspNetCore.Http; 7 | using Microsoft.AspNetCore.Routing; 8 | 9 | namespace FSH.Modules.Identity.Features.v1.Roles.GetRoleWithPermissions; 10 | 11 | public static class GetRolePermissionsEndpoint 12 | { 13 | public static RouteHandlerBuilder MapGetRolePermissionsEndpoint(this IEndpointRouteBuilder endpoints) 14 | { 15 | return endpoints.MapGet("/{id:guid}/permissions", (string id, IMediator mediator, CancellationToken cancellationToken) => 16 | mediator.Send(new GetRoleWithPermissionsQuery(id), cancellationToken)) 17 | .WithName("GetRolePermissions") 18 | .WithSummary("Get role permissions") 19 | .RequirePermission(IdentityPermissionConstants.Roles.View) 20 | .WithDescription("Retrieve a role along with its assigned permissions."); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Modules/Identity/Modules.Identity/Features/v1/Roles/UpsertRole/CreateOrUpdateRoleEndpoint.cs: -------------------------------------------------------------------------------- 1 | using FSH.Framework.Shared.Identity; 2 | using FSH.Framework.Shared.Identity.Authorization; 3 | using FSH.Modules.Identity.Contracts.v1.Roles.UpsertRole; 4 | using Mediator; 5 | using Microsoft.AspNetCore.Builder; 6 | using Microsoft.AspNetCore.Http; 7 | using Microsoft.AspNetCore.Mvc; 8 | using Microsoft.AspNetCore.Routing; 9 | 10 | namespace FSH.Modules.Identity.Features.v1.Roles.UpsertRole; 11 | 12 | public static class CreateOrUpdateRoleEndpoint 13 | { 14 | public static RouteHandlerBuilder MapCreateOrUpdateRoleEndpoint(this IEndpointRouteBuilder endpoints) 15 | { 16 | return endpoints.MapPost("/roles", (IMediator mediator, [FromBody] UpsertRoleCommand request, CancellationToken cancellationToken) => 17 | mediator.Send(request, cancellationToken)) 18 | .WithName("CreateOrUpdateRole") 19 | .WithSummary("Create or update role") 20 | .RequirePermission(IdentityPermissionConstants.Roles.Create) 21 | .WithDescription("Create a new role or update an existing role's name and description."); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Modules/Auditing/Modules.Auditing.Contracts/Dtos/AuditDetailDto.cs: -------------------------------------------------------------------------------- 1 | using FSH.Modules.Auditing.Contracts; 2 | using System.Text.Json; 3 | 4 | namespace FSH.Modules.Auditing.Contracts.Dtos; 5 | 6 | public sealed class AuditDetailDto 7 | { 8 | public Guid Id { get; set; } 9 | 10 | public DateTime OccurredAtUtc { get; set; } 11 | 12 | public DateTime ReceivedAtUtc { get; set; } 13 | 14 | public AuditEventType EventType { get; set; } 15 | 16 | public AuditSeverity Severity { get; set; } 17 | 18 | public string? TenantId { get; set; } 19 | 20 | public string? UserId { get; set; } 21 | 22 | public string? UserName { get; set; } 23 | 24 | public string? TraceId { get; set; } 25 | 26 | public string? SpanId { get; set; } 27 | 28 | public string? CorrelationId { get; set; } 29 | 30 | public string? RequestId { get; set; } 31 | 32 | public string? Source { get; set; } 33 | 34 | public AuditTag Tags { get; set; } 35 | 36 | /// 37 | /// Masked, deserialized payload. Serialized back to JSON for clients. 38 | /// 39 | public JsonElement Payload { get; set; } 40 | } 41 | 42 | --------------------------------------------------------------------------------