├── .dockerignore ├── .editorconfig ├── .github └── FUNDING.yml ├── .gitignore ├── .vscode ├── launch.json ├── settings.json └── tasks.json ├── CleanArchitecture.sln ├── LICENSE ├── README.md ├── docker-compose.dcproj ├── docker-compose.override.yml ├── docker-compose.yml ├── nuget.config ├── src ├── ClientApps │ ├── CleanHr.Blazor │ │ ├── App.razor │ │ ├── CleanHr.Blazor.csproj │ │ ├── Common │ │ │ ├── AuthorizationDelegatingHandler.cs │ │ │ ├── BootstrapValidationClassProvider.cs │ │ │ ├── ExceptionLogger.cs │ │ │ ├── HostAuthenticationStateProvider.cs │ │ │ ├── JwtTokenParser.cs │ │ │ └── LocalStorageKey.cs │ │ ├── Components │ │ │ ├── DepartmentComponents │ │ │ │ ├── CreateDepartmentModalComponent.razor │ │ │ │ ├── CreateDepartmentModalComponent.razor.cs │ │ │ │ ├── DeleteDepartmentModalComponent.razor │ │ │ │ ├── DeleteDepartmentModalComponent.razor.cs │ │ │ │ ├── DepartmentDetailsModalComponent.razor │ │ │ │ ├── DepartmentDetailsModalComponent.razor.cs │ │ │ │ ├── DepartmentListComponent.razor │ │ │ │ ├── DepartmentListComponent.razor.cs │ │ │ │ ├── UpdateDepartmentModalComponent.razor │ │ │ │ └── UpdateDepartmentModalComponent.razor.cs │ │ │ ├── EmployeeComponents │ │ │ │ ├── CreateEmployeeModalComponent.razor │ │ │ │ ├── CreateEmployeeModalComponent.razor.cs │ │ │ │ ├── DeleteEmployeeModalComponent.razor │ │ │ │ ├── DeleteEmployeeModalComponent.razor.cs │ │ │ │ ├── EmployeeDetailsModalComponent.razor │ │ │ │ ├── EmployeeDetailsModalComponent.razor.cs │ │ │ │ ├── EmployeeListComponent.razor │ │ │ │ ├── EmployeeListComponent.razor.cs │ │ │ │ ├── UpdateEmployeeModalComponent.razor │ │ │ │ └── UpdateEmployeeModalComponent.razor.cs │ │ │ ├── HomeComponents │ │ │ │ └── IndexComponent.razor │ │ │ ├── IdentityComponents │ │ │ │ ├── ForgotPasswordComponent.razor │ │ │ │ ├── ForgotPasswordComponent.razor.cs │ │ │ │ ├── LoginComponent.razor │ │ │ │ ├── LoginComponent.razor.cs │ │ │ │ ├── RegisterComponent.razor │ │ │ │ └── RegisterComponent.razor.cs │ │ │ └── SharedComponents │ │ │ │ ├── EmptyLayoutComponent.razor │ │ │ │ ├── MainLayoutComponent.razor │ │ │ │ ├── MainLayoutComponent.razor.cs │ │ │ │ ├── SideNavMenuComponent.razor │ │ │ │ ├── SideNavMenuComponent.razor.css │ │ │ │ ├── SurveyPrompt.razor │ │ │ │ └── UnauthorizedTextComponent.razor │ │ ├── Extensions │ │ │ ├── EditContextExtensions.cs │ │ │ ├── LocalStorageServiceExtensions.cs │ │ │ └── ServiceCollectionExtensions.cs │ │ ├── Models │ │ │ ├── DepartmentModels │ │ │ │ ├── CreateDepartmentModel.cs │ │ │ │ ├── DepartmentDetailsModel.cs │ │ │ │ └── UpdateDepartmentModel.cs │ │ │ ├── EmployeeModels │ │ │ │ ├── CreateEmployeeModel.cs │ │ │ │ ├── EmployeeDetailsModel.cs │ │ │ │ └── UpdateEmployeeModel.cs │ │ │ ├── IdentityModels │ │ │ │ ├── ChangePasswordModel.cs │ │ │ │ ├── EditUserModel.cs │ │ │ │ ├── ForgotPasswordModel.cs │ │ │ │ ├── LoggedInUserInfo.cs │ │ │ │ ├── LoginModel.cs │ │ │ │ ├── RegisterModel.cs │ │ │ │ ├── ResetPasswordModel.cs │ │ │ │ ├── RoleModel.cs │ │ │ │ ├── SetUserPasswordModel.cs │ │ │ │ ├── UserDetailsModel.cs │ │ │ │ ├── UserModel.cs │ │ │ │ └── UserSearchModel.cs │ │ │ ├── PaginatedList.cs │ │ │ └── SelectListItem.cs │ │ ├── Program.cs │ │ ├── Properties │ │ │ └── launchSettings.json │ │ ├── Services │ │ │ ├── DepartmentService.cs │ │ │ ├── EmployeeService.cs │ │ │ └── UserService.cs │ │ ├── _Imports.razor │ │ └── wwwroot │ │ │ ├── assets │ │ │ ├── demo │ │ │ │ ├── chart-area-demo.js │ │ │ │ ├── chart-bar-demo.js │ │ │ │ ├── chart-pie-demo.js │ │ │ │ └── datatables-demo.js │ │ │ └── img │ │ │ │ └── error-404-monochrome.svg │ │ │ ├── css │ │ │ ├── app.css │ │ │ ├── bootstrap │ │ │ │ ├── css │ │ │ │ │ ├── bootstrap.min.css │ │ │ │ │ └── bootstrap.min.css.map │ │ │ │ └── js │ │ │ │ │ ├── bootstrap.bundle.min.js │ │ │ │ │ └── bootstrap.bundle.min.js.map │ │ │ ├── open-iconic │ │ │ │ ├── FONT-LICENSE │ │ │ │ ├── ICON-LICENSE │ │ │ │ ├── README.md │ │ │ │ └── font │ │ │ │ │ ├── css │ │ │ │ │ └── open-iconic-bootstrap.min.css │ │ │ │ │ └── fonts │ │ │ │ │ ├── open-iconic.eot │ │ │ │ │ ├── open-iconic.otf │ │ │ │ │ ├── open-iconic.svg │ │ │ │ │ ├── open-iconic.ttf │ │ │ │ │ └── open-iconic.woff │ │ │ └── styles.css │ │ │ ├── favicon.ico │ │ │ ├── icon-512.png │ │ │ ├── index.html │ │ │ ├── js │ │ │ ├── datatables-simple-demo.js │ │ │ └── scripts.js │ │ │ ├── manifest.json │ │ │ ├── service-worker.js │ │ │ └── service-worker.published.js │ ├── Dockerfile │ ├── docker-compose.yml │ └── nginx.conf └── ServerApp │ ├── Core │ ├── CleanHr.Application │ │ ├── Caching │ │ │ ├── Handlers │ │ │ │ ├── IDepartmentCacheHandler.cs │ │ │ │ └── IEmployeeCacheHandler.cs │ │ │ └── Repositories │ │ │ │ ├── IDepartmentCacheRepository.cs │ │ │ │ └── IEmployeeCacheRepository.cs │ │ ├── CleanHr.Application.csproj │ │ ├── Commands │ │ │ ├── DepartmentCommands │ │ │ │ ├── CreateDepartmentCommand.cs │ │ │ │ ├── DeleteDepartmentCommand.cs │ │ │ │ └── UpdateDepartmentCommand.cs │ │ │ ├── EmployeeCommands │ │ │ │ ├── CreateEmployeeCommand.cs │ │ │ │ ├── DeleteEmployeeCommand.cs │ │ │ │ └── UpdateEmployeeCommand.cs │ │ │ └── IdentityCommands │ │ │ │ └── UserCommands │ │ │ │ ├── ResetPasswordCommand.cs │ │ │ │ ├── SendEmailVerificationCodeCommand.cs │ │ │ │ ├── SendPasswordResetCodeCommand.cs │ │ │ │ ├── StoreRefreshTokenCommand.cs │ │ │ │ ├── StoreUserPasswordCommand.cs │ │ │ │ ├── UpdateDialCodeCommand.cs │ │ │ │ ├── UpdateLanguageCultureCommand.cs │ │ │ │ ├── UpdateRefreshTokenCommand.cs │ │ │ │ └── VerifyUserEmailCommand.cs │ │ ├── GlobalUsings.cs │ │ ├── Infrastructures │ │ │ ├── IEmailSender.cs │ │ │ └── IExceptionLogger.cs │ │ ├── Queries │ │ │ ├── DepartmentQueries │ │ │ │ ├── GetDepartmentByIdQuery.cs │ │ │ │ ├── GetDepartmentListQuery.cs │ │ │ │ ├── IsDepartmentExistentByIdQuery.cs │ │ │ │ ├── IsDepartmentExistentByNameQuery.cs │ │ │ │ └── IsDepartmentNameUniqueQuery.cs │ │ │ ├── EmployeeQueries │ │ │ │ ├── GetEmployeeByIdQuery.cs │ │ │ │ ├── GetEmployeeListQuery.cs │ │ │ │ └── IsEmployeeExistentByIdQuery.cs │ │ │ └── IdentityQueries │ │ │ │ └── UserQueries │ │ │ │ ├── CheckIfOldPasswordQuery.cs │ │ │ │ ├── GetEmailVerificationCodeQuery.cs │ │ │ │ ├── GetLanguageCultureQuery.cs │ │ │ │ ├── GetPasswordResetCodeQuery.cs │ │ │ │ ├── GetRefreshTokenQuery.cs │ │ │ │ ├── HasUserActiveEmailVerificationCodeQuery.cs │ │ │ │ └── IsRefreshTokenValidQuery.cs │ │ └── Services │ │ │ └── ViewRenderService.cs │ └── CleanHr.Domain │ │ ├── Aggregates │ │ ├── DepartmentAggregate │ │ │ ├── Department.cs │ │ │ ├── DepartmentDomainErrors.cs │ │ │ └── IDepartmentRepository.cs │ │ ├── EmployeeAggregate │ │ │ ├── Employee.cs │ │ │ ├── EmployeeFactory.cs │ │ │ └── IEmployeeRepository.cs │ │ ├── ITimeFields.cs │ │ └── IdentityAggregate │ │ │ ├── ApplicationRole.cs │ │ │ ├── ApplicationUser.cs │ │ │ ├── EmailVerificationCode.cs │ │ │ ├── PasswordResetCode.cs │ │ │ ├── RefreshToken.cs │ │ │ └── UserOldPassword.cs │ │ ├── CleanHr.Domain.csproj │ │ ├── Exceptions │ │ ├── DomainValidationException.cs │ │ └── EntityNotFoundException.cs │ │ ├── Primitives │ │ ├── AggregateRoot.cs │ │ ├── Entity.cs │ │ └── ValueObject.cs │ │ └── ValueObjects │ │ ├── DateOfBirth.cs │ │ ├── DepartmentName.cs │ │ ├── Email.cs │ │ ├── EmployeeName.cs │ │ └── PhoneNumber.cs │ ├── Infrastructure │ ├── CleanHr.Infrastructure.Services │ │ ├── CleanHr.Infrastructure.Services.csproj │ │ ├── Configs │ │ │ └── SendGridConfig.cs │ │ ├── EmailSender.cs │ │ ├── ExceptionLogger.cs │ │ └── ServiceCollectionExtensions.cs │ ├── CleanHr.Persistence.Cache │ │ ├── CleanHr.Persistence.Cache.csproj │ │ ├── Handlers │ │ │ ├── DepartmentCacheHandler.cs │ │ │ └── EmployeeCacheHandler.cs │ │ ├── Keys │ │ │ ├── DepartmentCacheKeys.cs │ │ │ └── EmployeeCacheKeys.cs │ │ ├── Repositories │ │ │ ├── DepartmentCacheRepository.cs │ │ │ └── EmployeeCacheRepository.cs │ │ └── ServiceCollectionExtensions.cs │ └── CleanHr.Persistence.RelationalDB │ │ ├── CleanHr.Persistence.RelationalDB.csproj │ │ ├── CleanHrDbContext.cs │ │ ├── EntityConfigurations │ │ ├── BaseEntityConfiguration.cs │ │ ├── DepartmentAggregate │ │ │ └── DepartmentConfiguration.cs │ │ ├── EmployeeAggregate │ │ │ └── EmployeeConfiguration.cs │ │ └── IdentityAggregate │ │ │ ├── ApplicationRoleConfiguration.cs │ │ │ ├── ApplicationUserConfiguration.cs │ │ │ ├── EmailVerificationCodeConfiguration.cs │ │ │ ├── PasswordResetCodeConfiguration.cs │ │ │ ├── RefreshTokenConfiguration.cs │ │ │ └── UserOldPasswordConfiguration.cs │ │ ├── Extensions │ │ ├── ApplicationBuilderExtensions.cs │ │ ├── ModelBuilderExtensions.cs │ │ ├── QueryableExtensions.cs │ │ └── ServiceCollectionExtensions.cs │ │ ├── GlobalSuppressions.cs │ │ ├── Migrations │ │ ├── 20220905133719_InitialCreate.Designer.cs │ │ ├── 20220905133719_InitialCreate.cs │ │ └── EmployeeManagementDbContextModelSnapshot.cs │ │ └── Repositories │ │ ├── DepartmentRepository.cs │ │ └── EmployeeRepository.cs │ └── Presentation │ └── CleanHr.Api │ ├── .config │ └── dotnet-tools.json │ ├── CleanHr.Api.csproj │ ├── Configs │ └── JwtConfig.cs │ ├── Constants │ └── HealthCheckTags.cs │ ├── Dockerfile │ ├── Extensions │ ├── FluentValidationServiceCollectionExtensions.cs │ ├── ServiceCollectionExtensions.cs │ ├── SwaggerGenerationServicesExtensions.cs │ └── WebApplicationExtensions.cs │ ├── Features │ ├── Department │ │ ├── Endpoints │ │ │ ├── CreateDepartmentEndpoint.cs │ │ │ ├── DeleteDepartmentEndpoint.cs │ │ │ ├── DepartmentEndpointBase.cs │ │ │ ├── GetDepartmentByIdEndpoint.cs │ │ │ ├── GetDepartmentListEndpoint.cs │ │ │ ├── GetDepartmentSelectListEndpoint.cs │ │ │ └── UpdateDepartmentEndpoint.cs │ │ ├── Models │ │ │ ├── CreateDepartmentModel.cs │ │ │ ├── DepartmentBaseModel.cs │ │ │ └── UpdateDepartmentModel.cs │ │ └── Validators │ │ │ ├── CreateDepartmentModelValidator.cs │ │ │ ├── DepartmentBaseModelValidator.cs │ │ │ ├── DepartmentRuleBuilderExtensions.cs │ │ │ └── UpdateDepartmentModelValidator.cs │ ├── Employee │ │ ├── Endpoints │ │ │ ├── CreateEmployeeEndpoint.cs │ │ │ ├── DeleteEmployeeEndpoint.cs │ │ │ ├── EmployeeEndpointBase.cs │ │ │ ├── GetEmployeeDetailsByIdEndpoint.cs │ │ │ ├── GetEmployeeListEndpoint.cs │ │ │ └── UpdateEmployeeEndpoint.cs │ │ ├── Models │ │ │ ├── CreateEmployeeModel.cs │ │ │ ├── EmployeeBaseModel.cs │ │ │ └── UpdateEmployeeModel.cs │ │ └── Validators │ │ │ ├── CreateEmployeeModelValidator.cs │ │ │ ├── EmployeeBaseModelValidator.cs │ │ │ └── UpdateEmployeeModelValidator.cs │ ├── ExternalLogin │ │ ├── Endpoints │ │ │ ├── ExternalLoginEndpointBase.cs │ │ │ ├── ExternalLoginSignInCallbackEndpoint.cs │ │ │ ├── ExternalLoginSignInEndpoint.cs │ │ │ ├── ExternalLoginSignUpCallbackEndpoint.cs │ │ │ └── ExternalLoginSignUpEndpoint.cs │ │ ├── Models │ │ │ └── TestModel.cs │ │ └── Validators │ │ │ └── TestModelValidator.cs │ └── User │ │ ├── Endpoints │ │ ├── ConfirmUserEmailEndpoint.cs │ │ ├── GetRefreshedAccessTokenEndpoint.cs │ │ ├── ResendUserEmailConfirmationCodeEndpoint.cs │ │ ├── ResetUserPasswordEndpoint.cs │ │ ├── SendUserPasswordResetCodeEndpoint.cs │ │ ├── UserEndpointBase.cs │ │ ├── UserLoginEndpoint.cs │ │ ├── UserLogoutEndpoint.cs │ │ └── UserRegistrationEndpoint.cs │ │ ├── Models │ │ ├── EmailConfirmationModel.cs │ │ ├── ForgotPasswordModel.cs │ │ ├── LoginModel.cs │ │ ├── RegistrationModel.cs │ │ ├── ResendEmailConfirmationCodeModel.cs │ │ ├── ResetPasswordModel.cs │ │ └── TokenRefreshModel.cs │ │ └── Validators │ │ ├── EmailConfirmationModelValidator.cs │ │ ├── ForgotPasswordModelValidator.cs │ │ ├── LoginModelValidator.cs │ │ ├── RegistrationModelValidator.cs │ │ ├── ResendEmailConfirmationCodeModelValidator.cs │ │ ├── ResetPasswordModelValidator.cs │ │ └── TokenRefreshModelValidator.cs │ ├── Filters │ ├── BadRequestResultFilter.cs │ └── ExceptionHandlerFilter.cs │ ├── GlobalSuppressions.cs │ ├── GlobalUsings.cs │ ├── Health │ ├── DbConnectionHealthCheck.cs │ ├── ReadinessHealthCheck.cs │ └── SendGridConnectionHealthCheck.cs │ ├── Helpers │ ├── ConfigurationHelper.cs │ └── TokenGenerator.cs │ ├── Pages │ ├── ExternalLoginConfirmationPage.cshtml │ └── ExternalLoginConfirmationPage.cshtml.cs │ ├── Program.cs │ ├── Properties │ └── launchSettings.json │ ├── Startup.cs │ ├── Utilities │ └── SlugifyParameterTransformer.cs │ ├── Workers │ └── ConfigurationLoadingBackgroundService.cs │ ├── appsettings.Development.json │ └── appsettings.json └── test └── CleanHr.Application.Tests ├── CleanHr.Application.Tests.csproj ├── DepartmentCommandTests └── CreateDepartmentCommandHandlerTests.cs ├── UnitTest1.cs └── Usings.cs /.dockerignore: -------------------------------------------------------------------------------- 1 | **/.classpath 2 | **/.dockerignore 3 | **/.env 4 | **/.git 5 | **/.gitignore 6 | **/.project 7 | **/.settings 8 | **/.toolstarget 9 | **/.vs 10 | **/.vscode 11 | **/*.*proj.user 12 | **/*.dbmdl 13 | **/*.jfm 14 | **/azds.yaml 15 | **/bin 16 | **/charts 17 | **/docker-compose* 18 | **/Dockerfile* 19 | **/node_modules 20 | **/npm-debug.log 21 | **/obj 22 | **/secrets.dev.yaml 23 | **/values.dev.yaml 24 | LICENSE 25 | README.md -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [TanvirArjel] 4 | patreon: TanvirArjel 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 14 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | // Use IntelliSense to find out which attributes exist for C# debugging 6 | // Use hover for the description of the existing attributes 7 | // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md 8 | "name": ".NET Core Launch (web)", 9 | "type": "coreclr", 10 | "request": "launch", 11 | "preLaunchTask": "build", 12 | // If you have changed target frameworks, make sure to update the program path. 13 | "program": "${workspaceFolder}/src/ServerApp/Presentation/EmployeeManagement.Api/bin/Debug/net6.0/EmployeeManagement.Api.dll", 14 | "args": [], 15 | "cwd": "${workspaceFolder}/src/ServerApp/Presentation/EmployeeManagement.Api", 16 | "stopAtEntry": false, 17 | // Enable launching a web browser when ASP.NET Core starts. For more information: https://aka.ms/VSCode-CS-LaunchJson-WebBrowser 18 | "serverReadyAction": { 19 | "action": "openExternally", 20 | "pattern": "\\bNow listening on:\\s+(https?://\\S+)" 21 | }, 22 | "env": { 23 | "ASPNETCORE_ENVIRONMENT": "Development" 24 | }, 25 | "sourceFileMap": { 26 | "/Views": "${workspaceFolder}/Views" 27 | } 28 | }, 29 | { 30 | "name": ".NET Core Attach", 31 | "type": "coreclr", 32 | "request": "attach" 33 | }, 34 | { 35 | "name": "Launch and Debug Standalone Blazor WebAssembly App", 36 | "type": "blazorwasm", 37 | "request": "launch", 38 | "cwd": "${workspaceFolder}/src/ClientApps/BlazorWasmApp" 39 | } 40 | ] 41 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "appsettings", 4 | "Arjel", 5 | "Blazor", 6 | "Brotli", 7 | "datetime", 8 | "datetimeoffset", 9 | "Hasher", 10 | "healthz", 11 | "Hmac", 12 | "Mediat", 13 | "Middlewares", 14 | "nvarchar", 15 | "Serilog", 16 | "Swashbuckle", 17 | "Tanvir", 18 | "uniqueidentifier" 19 | ], 20 | "dotnet-test-explorer.testProjectPath": "**/*Tests.@(csproj|vbproj|fsproj)", 21 | "dotnet.defaultSolution": "CleanArchitecture.sln" 22 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "build", 6 | "command": "dotnet", 7 | "type": "process", 8 | "args": [ 9 | "build", 10 | "${workspaceFolder}/src/ServerApp/Presentation/EmployeeManagement.Api/EmployeeManagement.Api.csproj", 11 | "/property:GenerateFullPaths=true", 12 | "/consoleloggerparameters:NoSummary" 13 | ], 14 | "problemMatcher": "$msCompile" 15 | }, 16 | { 17 | "label": "publish", 18 | "command": "dotnet", 19 | "type": "process", 20 | "args": [ 21 | "publish", 22 | "${workspaceFolder}/src/ServerApp/Presentation/EmployeeManagement.Api/EmployeeManagement.Api.csproj", 23 | "/property:GenerateFullPaths=true", 24 | "/consoleloggerparameters:NoSummary" 25 | ], 26 | "problemMatcher": "$msCompile" 27 | }, 28 | { 29 | "label": "watch", 30 | "command": "dotnet", 31 | "type": "process", 32 | "args": [ 33 | "watch", 34 | "run", 35 | "--project", 36 | "${workspaceFolder}/src/ServerApp/Presentation/EmployeeManagement.Api/EmployeeManagement.Api.csproj" 37 | ], 38 | "problemMatcher": "$msCompile" 39 | } 40 | ] 41 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Tanvir Ahmad Arjel 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /docker-compose.dcproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 2.1 5 | Linux 6 | 7fd9b922-ac6e-44f8-ae81-02d4107dc7f5 7 | LaunchBrowser 8 | {Scheme}://localhost:{ServicePort}/swagger 9 | employeemanagement.api 10 | 11 | 12 | 13 | docker-compose.yml 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /docker-compose.override.yml: -------------------------------------------------------------------------------- 1 | version: '3.9' 2 | 3 | services: 4 | employeemanagement.api: 5 | environment: 6 | - ASPNETCORE_ENVIRONMENT=Development 7 | - ASPNETCORE_URLS=https://+:443;http://+:80 8 | ports: 9 | - "80" 10 | - "443" 11 | volumes: 12 | - ${APPDATA}/Microsoft/UserSecrets:/root/.microsoft/usersecrets:ro 13 | - ${APPDATA}/ASP.NET/Https:/root/.aspnet/https:ro 14 | 15 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.9' 2 | 3 | services: 4 | sqlserver: 5 | image: mcr.microsoft.com/mssql/server:latest 6 | environment: 7 | SA_PASSWORD: "MyPass@word" 8 | ACCEPT_EULA: "Y" 9 | MSSQL_PID: 'Express' 10 | ports: ["1440:1433"] 11 | 12 | cleanhr.api: 13 | image: ${DOCKER_REGISTRY-}cleanhrapi 14 | build: 15 | context: . 16 | dockerfile: src/ServerApp/Presentation/CleanHr.Api/Dockerfile 17 | ports: ["7100:80","7101:443"] 18 | depends_on: ["sqlserver"] 19 | 20 | -------------------------------------------------------------------------------- /nuget.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/ClientApps/CleanHr.Blazor/App.razor: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | Not found 13 | 14 | Sorry, there's nothing at this address. 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/ClientApps/CleanHr.Blazor/CleanHr.Blazor.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | true 6 | AllEnabledByDefault 7 | true 8 | service-worker-assets.js 9 | e3055905-8d05-4650-845d-cb9e1e9082a0 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | all 24 | runtime; build; native; contentfiles; analyzers; buildtransitive 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /src/ClientApps/CleanHr.Blazor/Common/AuthorizationDelegatingHandler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.Http; 3 | using System.Net.Http.Headers; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using Blazored.LocalStorage; 7 | using TanvirArjel.Extensions.Microsoft.DependencyInjection; 8 | 9 | namespace CleanHr.Blazor.Common; 10 | 11 | [TransientService] 12 | public class AuthorizationDelegatingHandler(ILocalStorageService localStorage) : DelegatingHandler 13 | { 14 | private readonly ILocalStorageService _localStorage = localStorage; 15 | 16 | protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) 17 | { 18 | ArgumentNullException.ThrowIfNull(request); 19 | 20 | try 21 | { 22 | // LocalStorageService throws exception in when it is being used in .NET MAUI Blazor. 23 | string jsonWebToken = await _localStorage.GetItemAsync(LocalStorageKey.Jwt, cancellationToken); 24 | 25 | if (string.IsNullOrWhiteSpace(jsonWebToken) == false) 26 | { 27 | request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", jsonWebToken); 28 | } 29 | } 30 | catch (Exception exception) 31 | { 32 | Console.WriteLine("LocalStorageSerivce throws exception."); 33 | Console.WriteLine(exception); 34 | } 35 | 36 | HttpResponseMessage httpResponseMessage = await base.SendAsync(request, cancellationToken); 37 | 38 | return httpResponseMessage; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/ClientApps/CleanHr.Blazor/Common/BootstrapValidationClassProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using Microsoft.AspNetCore.Components.Forms; 4 | 5 | namespace CleanHr.Blazor.Common; 6 | 7 | public class BootstrapValidationClassProvider : FieldCssClassProvider 8 | { 9 | public override string GetFieldCssClass(EditContext editContext, in FieldIdentifier fieldIdentifier) 10 | { 11 | ArgumentNullException.ThrowIfNull(editContext); 12 | 13 | bool isValid = !editContext.GetValidationMessages(fieldIdentifier).Any(); 14 | 15 | if (editContext.IsModified(fieldIdentifier)) 16 | { 17 | return isValid ? "is-valid" : "is-invalid"; 18 | } 19 | 20 | return isValid ? string.Empty : "is-invalid"; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/ClientApps/CleanHr.Blazor/Common/ExceptionLogger.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using TanvirArjel.Extensions.Microsoft.DependencyInjection; 4 | 5 | namespace CleanHr.Blazor.Common; 6 | 7 | [ScopedService] 8 | public class ExceptionLogger 9 | { 10 | public async Task LogAsync(Exception exception) 11 | { 12 | if (exception != null) 13 | { 14 | Console.WriteLine(exception); 15 | } 16 | 17 | await Task.CompletedTask; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/ClientApps/CleanHr.Blazor/Common/LocalStorageKey.cs: -------------------------------------------------------------------------------- 1 | namespace CleanHr.Blazor.Common; 2 | 3 | public static class LocalStorageKey 4 | { 5 | public const string Jwt = "LoggedInUserInfo"; 6 | } 7 | -------------------------------------------------------------------------------- /src/ClientApps/CleanHr.Blazor/Components/DepartmentComponents/DepartmentDetailsModalComponent.razor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using CleanHr.Blazor.Models.DepartmentModels; 4 | using CleanHr.Blazor.Services; 5 | 6 | namespace CleanHr.Blazor.Components.DepartmentComponents; 7 | 8 | public partial class DepartmentDetailsModalComponent(DepartmentService departmentService) 9 | { 10 | private readonly DepartmentService _departmentService = departmentService; 11 | 12 | private string ModalClass { get; set; } = string.Empty; 13 | 14 | private bool ShowBackdrop { get; set; } 15 | 16 | private DepartmentDetailsModel DepartmentDetailsModel { get; set; } 17 | 18 | public async Task ShowAsync(Guid departmentId) 19 | { 20 | DepartmentDetailsModel = await _departmentService.GetByIdAsync(departmentId); 21 | ModalClass = "show d-block"; 22 | ShowBackdrop = true; 23 | StateHasChanged(); 24 | } 25 | 26 | private void Close() 27 | { 28 | ModalClass = string.Empty; 29 | ShowBackdrop = false; 30 | StateHasChanged(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/ClientApps/CleanHr.Blazor/Components/EmployeeComponents/EmployeeDetailsModalComponent.razor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using CleanHr.Blazor.Models.EmployeeModels; 4 | using CleanHr.Blazor.Services; 5 | 6 | namespace CleanHr.Blazor.Components.EmployeeComponents; 7 | 8 | public partial class EmployeeDetailsModalComponent(EmployeeService employeeService) 9 | { 10 | private readonly EmployeeService _employeeService = employeeService; 11 | 12 | private string ModalClass { get; set; } = string.Empty; 13 | 14 | private bool ShowBackdrop { get; set; } 15 | 16 | private EmployeeDetailsModel EmployeeDetailsModel { get; set; } 17 | 18 | public async Task OpenAsync(Guid employeeId) 19 | { 20 | EmployeeDetailsModel = await _employeeService.GetDetailsByIdAsync(employeeId); 21 | ModalClass = "show d-block"; 22 | ShowBackdrop = true; 23 | StateHasChanged(); 24 | } 25 | 26 | private void Close() 27 | { 28 | ModalClass = string.Empty; 29 | ShowBackdrop = false; 30 | StateHasChanged(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/ClientApps/CleanHr.Blazor/Components/IdentityComponents/ForgotPasswordComponent.razor.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using CleanHr.Blazor.Common; 3 | using CleanHr.Blazor.Models.IdentityModels; 4 | using Microsoft.AspNetCore.Components.Forms; 5 | using TanvirArjel.Blazor.Components; 6 | 7 | namespace CleanHr.Blazor.Components.IdentityComponents; 8 | 9 | public partial class ForgotPasswordComponent 10 | { 11 | private EditContext FormContext { get; set; } 12 | 13 | private ForgotPasswordModel ForgotPasswordModel { get; set; } = new ForgotPasswordModel(); 14 | 15 | private CustomValidationMessages ValidationMessages { get; set; } 16 | 17 | private bool IsSubmitBtnDisabled { get; set; } 18 | 19 | protected override void OnInitialized() 20 | { 21 | FormContext = new EditContext(ForgotPasswordModel); 22 | FormContext.SetFieldCssClassProvider(new BootstrapValidationClassProvider()); 23 | } 24 | 25 | private async Task HandleValidSubmitAsync() 26 | { 27 | await Task.CompletedTask; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/ClientApps/CleanHr.Blazor/Components/SharedComponents/EmptyLayoutComponent.razor: -------------------------------------------------------------------------------- 1 | @inherits LayoutComponentBase 2 | 3 | 4 | @Body 5 | -------------------------------------------------------------------------------- /src/ClientApps/CleanHr.Blazor/Components/SharedComponents/MainLayoutComponent.razor.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using CleanHr.Blazor.Common; 3 | 4 | namespace CleanHr.Blazor.Components.SharedComponents; 5 | 6 | public partial class MainLayoutComponent(HostAuthStateProvider hostAuthStateProvider) 7 | { 8 | private readonly HostAuthStateProvider _hostAuthStateProvider = hostAuthStateProvider; 9 | 10 | private async Task LogOutAsync() 11 | { 12 | await _hostAuthStateProvider.LogOutAsync("identity/login"); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/ClientApps/CleanHr.Blazor/Components/SharedComponents/SideNavMenuComponent.razor.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TanvirArjel/CleanArchitecture/679c29821943268fb2a15ee7640f171b0a06125d/src/ClientApps/CleanHr.Blazor/Components/SharedComponents/SideNavMenuComponent.razor.css -------------------------------------------------------------------------------- /src/ClientApps/CleanHr.Blazor/Components/SharedComponents/SurveyPrompt.razor: -------------------------------------------------------------------------------- 1 | 2 | 3 | @Title 4 | 5 | 6 | Please take our 7 | brief survey 8 | 9 | and tell us what you think. 10 | 11 | 12 | @code { 13 | // Demonstrates how a parent component can supply parameters 14 | [Parameter] 15 | public string Title { get; set; } 16 | } 17 | -------------------------------------------------------------------------------- /src/ClientApps/CleanHr.Blazor/Components/SharedComponents/UnauthorizedTextComponent.razor: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 401: Unauthorized 5 | 6 | 7 | You are not authorized to see the requested page. Please login by clicking the following button to move forward. 8 | 9 | 10 | Go Login 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/ClientApps/CleanHr.Blazor/Extensions/EditContextExtensions.cs: -------------------------------------------------------------------------------- 1 | using CleanHr.Blazor.Common; 2 | using Microsoft.AspNetCore.Components.Forms; 3 | using TanvirArjel.ArgumentChecker; 4 | 5 | namespace CleanHr.Blazor.Extensions; 6 | 7 | public static class EditContextExtensions 8 | { 9 | public static void AddBootstrapValidationClassProvider(this EditContext editContext) 10 | { 11 | editContext.ThrowIfNull(nameof(editContext)); 12 | 13 | editContext.SetFieldCssClassProvider(new BootstrapValidationClassProvider()); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/ClientApps/CleanHr.Blazor/Extensions/LocalStorageServiceExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Blazored.LocalStorage; 4 | using CleanHr.Blazor.Common; 5 | using CleanHr.Blazor.Models.IdentityModels; 6 | 7 | namespace CleanHr.Blazor.Extensions; 8 | 9 | public static class LocalStorageServiceExtensions 10 | { 11 | public static async Task GetUserInfoAsync(this ILocalStorageService localStorage) 12 | { 13 | ArgumentNullException.ThrowIfNull(localStorage); 14 | 15 | LoggedInUserInfo loggedInUserInfo = await localStorage.GetItemAsync(LocalStorageKey.Jwt); 16 | 17 | return loggedInUserInfo; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/ClientApps/CleanHr.Blazor/Extensions/ServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IdentityModel.Tokens.Jwt; 3 | using Blazored.LocalStorage; 4 | using CleanHr.Blazor.Common; 5 | using Microsoft.AspNetCore.Components.Authorization; 6 | using Microsoft.Extensions.DependencyInjection; 7 | using TanvirArjel.Extensions.Microsoft.DependencyInjection; 8 | 9 | namespace CleanHr.Blazor.Extensions; 10 | 11 | public static class ServiceCollectionExtensions 12 | { 13 | public static void AddDependencyServices(this IServiceCollection services) 14 | { 15 | ArgumentNullException.ThrowIfNull(services); 16 | 17 | services.AddBlazoredLocalStorage(config => config.JsonSerializerOptions.WriteIndented = true); 18 | 19 | services.AddOptions(); 20 | services.AddAuthorizationCore(); 21 | services.AddScoped(); 22 | services.AddScoped(sp => sp.GetRequiredService()); 23 | 24 | services.AddScoped(); 25 | 26 | services.AddServicesOfAllTypes(typeof(JwtTokenParser).Assembly); 27 | 28 | services.AddHttpClient("EmployeeManagementApi", c => 29 | { 30 | c.BaseAddress = new Uri("http://localhost:5100/api/"); 31 | c.DefaultRequestHeaders.Add("Accept", "application/json"); 32 | c.DefaultRequestHeaders.Add("User-Agent", "HttpClientFactory-Sample"); 33 | }).AddHttpMessageHandler(); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/ClientApps/CleanHr.Blazor/Models/DepartmentModels/CreateDepartmentModel.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel; 2 | using System.ComponentModel.DataAnnotations; 3 | 4 | namespace CleanHr.Blazor.Models.DepartmentModels; 5 | 6 | public class CreateDepartmentModel 7 | { 8 | [Required] 9 | [DisplayName("Department Name")] 10 | [MinLength(2, ErrorMessage = "{0} should be at least {1} characters long.")] 11 | [MaxLength(20, ErrorMessage = "{0} should not be more than {1} characters.")] 12 | public string Name { get; set; } 13 | 14 | [Required] 15 | [DisplayName("Description")] 16 | [MinLength(20, ErrorMessage = "{0} should be at least {1} characters long.")] 17 | [MaxLength(200, ErrorMessage = "{0} should not be more than {1} characters.")] 18 | public string Description { get; set; } 19 | } 20 | -------------------------------------------------------------------------------- /src/ClientApps/CleanHr.Blazor/Models/DepartmentModels/DepartmentDetailsModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace CleanHr.Blazor.Models.DepartmentModels; 4 | 5 | public class DepartmentDetailsModel 6 | { 7 | public Guid Id { get; set; } 8 | 9 | public string Name { get; set; } 10 | 11 | public string Description { get; set; } 12 | 13 | public bool IsActive { get; set; } 14 | 15 | public DateTime CreatedAtUtc { get; set; } 16 | 17 | public DateTime? LastModifiedAtUtc { get; set; } 18 | } 19 | -------------------------------------------------------------------------------- /src/ClientApps/CleanHr.Blazor/Models/DepartmentModels/UpdateDepartmentModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel; 3 | using System.ComponentModel.DataAnnotations; 4 | 5 | namespace CleanHr.Blazor.Models.DepartmentModels; 6 | 7 | public class UpdateDepartmentModel 8 | { 9 | public Guid Id { get; set; } 10 | 11 | [Required] 12 | [DisplayName("Department Name")] 13 | [MinLength(2, ErrorMessage = "{0} should be at least {1} characters long.")] 14 | [MaxLength(20, ErrorMessage = "{0} should not be more than {1} characters.")] 15 | public string Name { get; set; } 16 | 17 | [Required] 18 | [DisplayName("Description")] 19 | [MinLength(20, ErrorMessage = "{0} should be at least {1} characters long.")] 20 | [MaxLength(200, ErrorMessage = "{0} should not be more than {1} characters.")] 21 | public string Description { get; set; } 22 | 23 | public bool IsActive { get; set; } 24 | } 25 | -------------------------------------------------------------------------------- /src/ClientApps/CleanHr.Blazor/Models/EmployeeModels/CreateEmployeeModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel; 3 | using System.ComponentModel.DataAnnotations; 4 | using TanvirArjel.CustomValidation.Attributes; 5 | 6 | namespace CleanHr.Blazor.Models.EmployeeModels; 7 | 8 | public class CreateEmployeeModel 9 | { 10 | [Required] 11 | [DisplayName("Employee Name")] 12 | [MinLength(4, ErrorMessage = "{0} should be at least {1} characters long.")] 13 | [MaxLength(50)] 14 | public string Name { get; set; } 15 | 16 | [Required(ErrorMessage = "Please select your deparment.")] 17 | [DisplayName("Department")] 18 | public Guid? DepartmentId { get; set; } 19 | 20 | [Required(ErrorMessage = "Please select your date of birth.")] 21 | [DataType(DataType.Date)] 22 | [DisplayName("Date Of Birth")] 23 | [MinAge(15, 0, 0, ErrorMessage = "The minimum age has to be 15 years.")] 24 | public DateTime? DateOfBirth { get; set; } 25 | 26 | [EmailAddress] 27 | [Required] 28 | [DisplayName("Email")] 29 | [MinLength(8, ErrorMessage = "{0} should be at least {1} characters long.")] 30 | [MaxLength(50, ErrorMessage = "{0} should not be more than {1} characters.")] 31 | public string Email { get; set; } 32 | 33 | [Required] 34 | [DisplayName("Phone Number")] 35 | [MinLength(10, ErrorMessage = "{0} should be at least {1} characters long.")] 36 | [MaxLength(15, ErrorMessage = "{0} should not be more than {1} characters.")] 37 | public string PhoneNumber { get; set; } 38 | } 39 | -------------------------------------------------------------------------------- /src/ClientApps/CleanHr.Blazor/Models/EmployeeModels/EmployeeDetailsModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel; 3 | using System.ComponentModel.DataAnnotations; 4 | 5 | namespace CleanHr.Blazor.Models.EmployeeModels; 6 | 7 | public class EmployeeDetailsModel 8 | { 9 | public Guid Id { get; set; } 10 | 11 | [DisplayName("Employee Name")] 12 | public string Name { get; set; } 13 | 14 | public Guid DepartmentId { get; set; } 15 | 16 | [DisplayName("Department Name")] 17 | public string DepartmentName { get; set; } 18 | 19 | [DisplayFormat(DataFormatString = "{0:dd-MMM-yyyy}")] 20 | [DisplayName("Date Of Birth")] 21 | public DateTime DateOfBirth { get; set; } 22 | 23 | public string Email { get; set; } 24 | 25 | [DisplayName("Phone Number")] 26 | public string PhoneNumber { get; set; } 27 | 28 | public bool IsActive { get; set; } 29 | 30 | public DateTime CreatedAtUtc { get; set; } 31 | 32 | public DateTime? LastModifiedAtUtc { get; set; } 33 | } 34 | -------------------------------------------------------------------------------- /src/ClientApps/CleanHr.Blazor/Models/EmployeeModels/UpdateEmployeeModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel; 3 | using System.ComponentModel.DataAnnotations; 4 | 5 | namespace CleanHr.Blazor.Models.EmployeeModels; 6 | 7 | public class UpdateEmployeeModel 8 | { 9 | [Required] 10 | public Guid Id { get; set; } 11 | 12 | [Required] 13 | [DisplayName("Employee Name")] 14 | [MinLength(4, ErrorMessage = "{0} should be at least {1} characters long.")] 15 | [MaxLength(50)] 16 | public string Name { get; set; } 17 | 18 | [Required(ErrorMessage = "Please select your deparment.")] 19 | [DisplayName("Department")] 20 | public Guid? DepartmentId { get; set; } 21 | 22 | [DataType(DataType.Date)] 23 | [DisplayName("Date Of Birth")] 24 | public DateTime DateOfBirth { get; set; } 25 | 26 | [Required] 27 | [DisplayName("Email")] 28 | [MinLength(8, ErrorMessage = "{0} should be at least {1} characters long.")] 29 | [MaxLength(50, ErrorMessage = "{0} should not be more than {1} characters.")] 30 | public string Email { get; set; } 31 | 32 | [Required] 33 | [DisplayName("Phone Number")] 34 | [MinLength(10, ErrorMessage = "{0} should be at least {1} characters long.")] 35 | [MaxLength(15, ErrorMessage = "{0} should not be more than {1} characters.")] 36 | public string PhoneNumber { get; set; } 37 | } 38 | -------------------------------------------------------------------------------- /src/ClientApps/CleanHr.Blazor/Models/IdentityModels/ChangePasswordModel.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace CleanHr.Blazor.Models.IdentityModels; 4 | 5 | public class ChangePasswordModel 6 | { 7 | public int UserId { get; set; } 8 | 9 | [Required(ErrorMessage = "Please enter your current password.")] 10 | [MinLength(8, ErrorMessage = "The {0} must be at least {1} characters long.")] 11 | [MaxLength(20, ErrorMessage = "The {0} cannot be more than {1} characters long.")] 12 | [Display(Name = "Current password")] 13 | public string CurrentPassword { get; set; } 14 | 15 | [Required(ErrorMessage = "Please enter your new password.")] 16 | [MinLength(8, ErrorMessage = "The {0} must be at least {1} characters long.")] 17 | [MaxLength(20, ErrorMessage = "The {0} cannot be more than {1} characters long.")] 18 | public string Password { get; set; } 19 | 20 | [Required(ErrorMessage = "Please confirm your new password.")] 21 | [Compare("Password")] 22 | [Display(Name = "Confirm password")] 23 | public string ConfirmPassword { get; set; } 24 | } 25 | -------------------------------------------------------------------------------- /src/ClientApps/CleanHr.Blazor/Models/IdentityModels/EditUserModel.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.ComponentModel.DataAnnotations; 3 | 4 | namespace CleanHr.Blazor.Models.IdentityModels; 5 | 6 | public class EditUserModel 7 | { 8 | [Required] 9 | public int Id { get; set; } 10 | 11 | [Required] 12 | [MinLength(5, ErrorMessage = "The {0} must be at least {1} characters long.")] 13 | [RegularExpression(@"^[a-zA-Z,\s]+$", ErrorMessage = "The {0} should contain only letters.")] 14 | public string FullName { get; set; } 15 | 16 | [Required] 17 | [RegularExpression(@"^[a-zA-Z][a-zA-Z0-9]+$", ErrorMessage = "The {0} should contain only letters and digits.")] 18 | [MinLength(3, ErrorMessage = "The {0} must be at least {1} characters long.")] 19 | [MaxLength(20, ErrorMessage = "The {0} should not be more than {1} characters long.")] 20 | public string UserName { get; set; } 21 | 22 | [Required] 23 | [EmailAddress] 24 | public string Email { get; set; } 25 | 26 | public bool IsActive { get; set; } 27 | 28 | public Dictionary Roles { get; set; } 29 | } 30 | -------------------------------------------------------------------------------- /src/ClientApps/CleanHr.Blazor/Models/IdentityModels/ForgotPasswordModel.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace CleanHr.Blazor.Models.IdentityModels; 4 | 5 | public class ForgotPasswordModel 6 | { 7 | [Required(ErrorMessage = "Please enter your email address.")] 8 | [EmailAddress] 9 | public string Email { get; set; } 10 | } 11 | -------------------------------------------------------------------------------- /src/ClientApps/CleanHr.Blazor/Models/IdentityModels/LoggedInUserInfo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace CleanHr.Blazor.Models.IdentityModels; 4 | 5 | public class LoggedInUserInfo 6 | { 7 | public Guid UserId { get; set; } 8 | 9 | public string FullName { get; set; } 10 | 11 | public string UserName { get; set; } 12 | 13 | public string Email { get; set; } 14 | 15 | public string AccessToken { get; set; } 16 | 17 | public DateTime AccessTokenExpireAtUtc { get; set; } 18 | 19 | public string RefreshToken { get; set; } 20 | 21 | public DateTime RefreshTokenExpireAtUtc { get; set; } 22 | } 23 | -------------------------------------------------------------------------------- /src/ClientApps/CleanHr.Blazor/Models/IdentityModels/LoginModel.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace CleanHr.Blazor.Models.IdentityModels; 4 | 5 | public class LoginModel 6 | { 7 | [Required(ErrorMessage = "Please enter your email or username.")] 8 | [MinLength(4, ErrorMessage = "The {0} must be at least {1} characters long.")] 9 | [MaxLength(50, ErrorMessage = "The {1} can't be more than {1} characters long.")] 10 | [Display(Name = "Email/UserName")] 11 | public string EmailOrUserName { get; set; } 12 | 13 | [Required(ErrorMessage = "Please enter your password.")] 14 | [MinLength(6, ErrorMessage = "The {0} must be at least {1} characters long.")] 15 | [MaxLength(20, ErrorMessage = "The {1} can't be more than {1} characters long.")] 16 | public string Password { get; set; } 17 | } 18 | -------------------------------------------------------------------------------- /src/ClientApps/CleanHr.Blazor/Models/IdentityModels/RegisterModel.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace CleanHr.Blazor.Models.IdentityModels; 4 | 5 | public class RegistrationModel 6 | { 7 | [Required(ErrorMessage = "Please enter your first name.")] 8 | [MaxLength(30, ErrorMessage = "The {1} can't be more than {1} characters long.")] 9 | public string FirstName { get; set; } 10 | 11 | [Required(ErrorMessage = "Please enter your last name.")] 12 | [MaxLength(30, ErrorMessage = "The {1} can't be more than {1} characters long.")] 13 | public string LastName { get; set; } 14 | 15 | [Required(ErrorMessage = "Please enter your email address.")] 16 | [MinLength(4, ErrorMessage = "The {0} must be at least {1} characters long.")] 17 | [MaxLength(50, ErrorMessage = "The {1} can't be more than {1} characters long.")] 18 | [EmailAddress(ErrorMessage = "Please enter a valid email address.")] 19 | [Display(Name = "Email")] 20 | public string Email { get; set; } 21 | 22 | [Required(ErrorMessage = "Please enter your password.")] 23 | [MinLength(6, ErrorMessage = "The {0} must be at least {1} characters long.")] 24 | [MaxLength(20, ErrorMessage = "The {1} can't be more than {1} characters long.")] 25 | public string Password { get; set; } 26 | 27 | [Required(ErrorMessage = "Please enter your confirm password.")] 28 | [Compare(nameof(Password), ErrorMessage = "Confirm password does match with password.")] 29 | public string ConfirmPassword { get; set; } 30 | } 31 | -------------------------------------------------------------------------------- /src/ClientApps/CleanHr.Blazor/Models/IdentityModels/ResetPasswordModel.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace CleanHr.Blazor.Models.IdentityModels; 4 | 5 | public class ResetPasswordModel 6 | { 7 | [Required] 8 | [EmailAddress] 9 | public string Email { get; set; } 10 | 11 | [Required] 12 | [StringLength(20, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)] 13 | [DataType(DataType.Password)] 14 | public string Password { get; set; } 15 | 16 | [Required] 17 | [DataType(DataType.Password)] 18 | [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")] 19 | public string ConfirmPassword { get; set; } 20 | 21 | [Required] 22 | [StringLength(6, MinimumLength = 6, ErrorMessage = "{0} should be exactly {1} characters long.")] 23 | public string Code { get; set; } 24 | } 25 | -------------------------------------------------------------------------------- /src/ClientApps/CleanHr.Blazor/Models/IdentityModels/RoleModel.cs: -------------------------------------------------------------------------------- 1 | namespace CleanHr.Blazor.Models.IdentityModels; 2 | 3 | public class RoleModel 4 | { 5 | public int Id { get; set; } 6 | 7 | public string Name { get; set; } 8 | } 9 | -------------------------------------------------------------------------------- /src/ClientApps/CleanHr.Blazor/Models/IdentityModels/SetUserPasswordModel.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace CleanHr.Blazor.Models.IdentityModels; 4 | 5 | public class SetUserPasswordModel 6 | { 7 | public int UserId { get; set; } 8 | 9 | public string UserName { get; set; } 10 | 11 | [Required] 12 | [MinLength(8, ErrorMessage = "The {0} must be at least {1} characters long.")] 13 | [MaxLength(20, ErrorMessage = "The {0} cannot be more than {1} characters long.")] 14 | public string Password { get; set; } 15 | 16 | [Required] 17 | [Compare("Password")] 18 | [Display(Name = "Confirm password")] 19 | public string ConfirmPassword { get; set; } 20 | } 21 | -------------------------------------------------------------------------------- /src/ClientApps/CleanHr.Blazor/Models/IdentityModels/UserDetailsModel.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace CleanHr.Blazor.Models.IdentityModels; 4 | 5 | public class UserDetailsModel 6 | { 7 | public int Id { get; set; } 8 | 9 | public string FullName { get; set; } 10 | 11 | public string UserName { get; set; } 12 | 13 | public string Email { get; set; } 14 | 15 | public bool IsActive { get; set; } 16 | 17 | public ICollection Roles { get; set; } 18 | } 19 | -------------------------------------------------------------------------------- /src/ClientApps/CleanHr.Blazor/Models/IdentityModels/UserModel.cs: -------------------------------------------------------------------------------- 1 | namespace CleanHr.Blazor.Models.IdentityModels; 2 | 3 | public class UserModel 4 | { 5 | public int Id { get; set; } 6 | 7 | public string FullName { get; set; } 8 | 9 | public string UserName { get; set; } 10 | 11 | public string Email { get; set; } 12 | 13 | public bool IsActive { get; set; } 14 | } 15 | -------------------------------------------------------------------------------- /src/ClientApps/CleanHr.Blazor/Models/IdentityModels/UserSearchModel.cs: -------------------------------------------------------------------------------- 1 | namespace CleanHr.Blazor.Models.IdentityModels; 2 | 3 | public class UserSearchModel 4 | { 5 | public string FullName { get; set; } 6 | 7 | public string UserName { get; set; } 8 | 9 | public string Email { get; set; } 10 | 11 | public string IsActive { get; set; } 12 | } 13 | -------------------------------------------------------------------------------- /src/ClientApps/CleanHr.Blazor/Models/PaginatedList.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace CleanHr.Blazor.Models; 4 | 5 | public class PaginatedList 6 | where T : class 7 | { 8 | public int PageIndex { get; set; } 9 | 10 | public int PageSize { get; set; } 11 | 12 | public int TotalPages { get; set; } 13 | 14 | public long TotalItems { get; set; } 15 | 16 | public ICollection Items { get; set; } = new List(); 17 | } 18 | -------------------------------------------------------------------------------- /src/ClientApps/CleanHr.Blazor/Models/SelectListItem.cs: -------------------------------------------------------------------------------- 1 | namespace CleanHr.Blazor.Models; 2 | 3 | public class SelectListItem 4 | { 5 | public string Text { get; set; } 6 | 7 | public string Value { get; set; } 8 | } 9 | -------------------------------------------------------------------------------- /src/ClientApps/CleanHr.Blazor/Program.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using CleanHr.Blazor.Extensions; 3 | using Microsoft.AspNetCore.Components.Web; 4 | using Microsoft.AspNetCore.Components.WebAssembly.Hosting; 5 | using TanvirArjel.Blazor.DependencyInjection; 6 | 7 | namespace CleanHr.Blazor; 8 | 9 | public static class Program 10 | { 11 | public static async Task Main(string[] args) 12 | { 13 | WebAssemblyHostBuilder builder = WebAssemblyHostBuilder.CreateDefault(args); 14 | builder.RootComponents.Add("#app"); 15 | builder.RootComponents.Add("head::after"); 16 | 17 | builder.Services.AddComponents(); 18 | 19 | builder.Services.AddDependencyServices(); 20 | 21 | await builder.Build().RunAsync(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/ClientApps/CleanHr.Blazor/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:43169", 7 | "sslPort": 44364 8 | } 9 | }, 10 | "profiles": { 11 | "BlazorWasmApp": { 12 | "commandName": "Project", 13 | "dotnetRunMessages": true, 14 | "launchBrowser": true, 15 | "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", 16 | "applicationUrl": "http://localhost:5200;https://localhost:5201", 17 | "environmentVariables": { 18 | "ASPNETCORE_ENVIRONMENT": "Development" 19 | } 20 | }, 21 | "IIS Express": { 22 | "commandName": "IISExpress", 23 | "launchBrowser": true, 24 | "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", 25 | "environmentVariables": { 26 | "ASPNETCORE_ENVIRONMENT": "Development" 27 | } 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /src/ClientApps/CleanHr.Blazor/_Imports.razor: -------------------------------------------------------------------------------- 1 | @using System.Net.Http 2 | @using System.Net.Http.Json 3 | @using Microsoft.AspNetCore.Authorization 4 | @using Microsoft.AspNetCore.Components.Authorization 5 | @using Microsoft.AspNetCore.Components.Forms 6 | @using Microsoft.AspNetCore.Components.Routing 7 | @using Microsoft.AspNetCore.Components.Web 8 | @using Microsoft.AspNetCore.Components.Web.Virtualization 9 | @using Microsoft.AspNetCore.Components.WebAssembly.Http 10 | @using Microsoft.JSInterop 11 | @using CleanHr.Blazor 12 | @using CleanHr.Blazor.Components.SharedComponents 13 | @using TanvirArjel.Blazor.Components 14 | -------------------------------------------------------------------------------- /src/ClientApps/CleanHr.Blazor/wwwroot/assets/demo/chart-bar-demo.js: -------------------------------------------------------------------------------- 1 | // Set new default font family and font color to mimic Bootstrap's default styling 2 | Chart.defaults.global.defaultFontFamily = '-apple-system,system-ui,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif'; 3 | Chart.defaults.global.defaultFontColor = '#292b2c'; 4 | 5 | // Bar Chart Example 6 | var ctx = document.getElementById("myBarChart"); 7 | var myLineChart = new Chart(ctx, { 8 | type: 'bar', 9 | data: { 10 | labels: ["January", "February", "March", "April", "May", "June"], 11 | datasets: [{ 12 | label: "Revenue", 13 | backgroundColor: "rgba(2,117,216,1)", 14 | borderColor: "rgba(2,117,216,1)", 15 | data: [4215, 5312, 6251, 7841, 9821, 14984], 16 | }], 17 | }, 18 | options: { 19 | scales: { 20 | xAxes: [{ 21 | time: { 22 | unit: 'month' 23 | }, 24 | gridLines: { 25 | display: false 26 | }, 27 | ticks: { 28 | maxTicksLimit: 6 29 | } 30 | }], 31 | yAxes: [{ 32 | ticks: { 33 | min: 0, 34 | max: 15000, 35 | maxTicksLimit: 5 36 | }, 37 | gridLines: { 38 | display: true 39 | } 40 | }], 41 | }, 42 | legend: { 43 | display: false 44 | } 45 | } 46 | }); 47 | -------------------------------------------------------------------------------- /src/ClientApps/CleanHr.Blazor/wwwroot/assets/demo/chart-pie-demo.js: -------------------------------------------------------------------------------- 1 | // Set new default font family and font color to mimic Bootstrap's default styling 2 | Chart.defaults.global.defaultFontFamily = '-apple-system,system-ui,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif'; 3 | Chart.defaults.global.defaultFontColor = '#292b2c'; 4 | 5 | // Pie Chart Example 6 | var ctx = document.getElementById("myPieChart"); 7 | var myPieChart = new Chart(ctx, { 8 | type: 'pie', 9 | data: { 10 | labels: ["Blue", "Red", "Yellow", "Green"], 11 | datasets: [{ 12 | data: [12.21, 15.58, 11.25, 8.32], 13 | backgroundColor: ['#007bff', '#dc3545', '#ffc107', '#28a745'], 14 | }], 15 | }, 16 | }); 17 | -------------------------------------------------------------------------------- /src/ClientApps/CleanHr.Blazor/wwwroot/assets/demo/datatables-demo.js: -------------------------------------------------------------------------------- 1 | // Call the dataTables jQuery plugin 2 | $(document).ready(function() { 3 | $('#dataTable').DataTable(); 4 | }); 5 | -------------------------------------------------------------------------------- /src/ClientApps/CleanHr.Blazor/wwwroot/css/app.css: -------------------------------------------------------------------------------- 1 | @import url('open-iconic/font/css/open-iconic-bootstrap.min.css'); 2 | 3 | html, body { 4 | font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; 5 | } 6 | 7 | a, .btn-link { 8 | color: #0366d6; 9 | } 10 | 11 | .btn-primary { 12 | color: #fff; 13 | background-color: #1b6ec2; 14 | border-color: #1861ac; 15 | } 16 | 17 | .content { 18 | padding-top: 1.1rem; 19 | } 20 | 21 | .valid.modified:not([type=checkbox]) { 22 | border: 1px solid #26b050; 23 | } 24 | 25 | .invalid { 26 | border: 1px solid red; 27 | } 28 | 29 | .validation-message { 30 | color: #dc3545; 31 | } 32 | 33 | #blazor-error-ui { 34 | background: lightyellow; 35 | bottom: 0; 36 | box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); 37 | display: none; 38 | left: 0; 39 | padding: 0.6rem 1.25rem 0.7rem 1.25rem; 40 | position: fixed; 41 | width: 100%; 42 | z-index: 1000; 43 | } 44 | 45 | #blazor-error-ui .dismiss { 46 | cursor: pointer; 47 | position: absolute; 48 | right: 0.75rem; 49 | top: 0.5rem; 50 | } 51 | 52 | .btn-form-floating { 53 | padding: 13px 1rem !important; 54 | } 55 | -------------------------------------------------------------------------------- /src/ClientApps/CleanHr.Blazor/wwwroot/css/open-iconic/ICON-LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Waybury 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /src/ClientApps/CleanHr.Blazor/wwwroot/css/open-iconic/font/fonts/open-iconic.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TanvirArjel/CleanArchitecture/679c29821943268fb2a15ee7640f171b0a06125d/src/ClientApps/CleanHr.Blazor/wwwroot/css/open-iconic/font/fonts/open-iconic.eot -------------------------------------------------------------------------------- /src/ClientApps/CleanHr.Blazor/wwwroot/css/open-iconic/font/fonts/open-iconic.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TanvirArjel/CleanArchitecture/679c29821943268fb2a15ee7640f171b0a06125d/src/ClientApps/CleanHr.Blazor/wwwroot/css/open-iconic/font/fonts/open-iconic.otf -------------------------------------------------------------------------------- /src/ClientApps/CleanHr.Blazor/wwwroot/css/open-iconic/font/fonts/open-iconic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TanvirArjel/CleanArchitecture/679c29821943268fb2a15ee7640f171b0a06125d/src/ClientApps/CleanHr.Blazor/wwwroot/css/open-iconic/font/fonts/open-iconic.ttf -------------------------------------------------------------------------------- /src/ClientApps/CleanHr.Blazor/wwwroot/css/open-iconic/font/fonts/open-iconic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TanvirArjel/CleanArchitecture/679c29821943268fb2a15ee7640f171b0a06125d/src/ClientApps/CleanHr.Blazor/wwwroot/css/open-iconic/font/fonts/open-iconic.woff -------------------------------------------------------------------------------- /src/ClientApps/CleanHr.Blazor/wwwroot/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TanvirArjel/CleanArchitecture/679c29821943268fb2a15ee7640f171b0a06125d/src/ClientApps/CleanHr.Blazor/wwwroot/favicon.ico -------------------------------------------------------------------------------- /src/ClientApps/CleanHr.Blazor/wwwroot/icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TanvirArjel/CleanArchitecture/679c29821943268fb2a15ee7640f171b0a06125d/src/ClientApps/CleanHr.Blazor/wwwroot/icon-512.png -------------------------------------------------------------------------------- /src/ClientApps/CleanHr.Blazor/wwwroot/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | BlazorWasmApp 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | Loading... 23 | 24 | 25 | An unhandled error has occurred. 26 | Reload 27 | 🗙 28 | 29 | 30 | 31 | 32 | 33 | 34 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /src/ClientApps/CleanHr.Blazor/wwwroot/js/datatables-simple-demo.js: -------------------------------------------------------------------------------- 1 | window.addEventListener('DOMContentLoaded', event => { 2 | // Simple-DataTables 3 | // https://github.com/fiduswriter/Simple-DataTables/wiki 4 | 5 | const datatablesSimple = document.getElementById('datatablesSimple'); 6 | if (datatablesSimple) { 7 | new simpleDatatables.DataTable(datatablesSimple); 8 | } 9 | }); 10 | -------------------------------------------------------------------------------- /src/ClientApps/CleanHr.Blazor/wwwroot/js/scripts.js: -------------------------------------------------------------------------------- 1 | window.addEventListener('DOMContentLoaded', event => { 2 | 3 | // Toggle the side navigation 4 | setTimeout(function () { 5 | const sidebarToggle = document.body.querySelector('#sidebarToggle'); 6 | if (sidebarToggle) { 7 | // Uncomment Below to persist sidebar toggle between refreshes 8 | // if (localStorage.getItem('sb|sidebar-toggle') === 'true') { 9 | // document.body.classList.toggle('sb-sidenav-toggled'); 10 | // } 11 | sidebarToggle.addEventListener('click', event => { 12 | console.log("Button clicked"); 13 | event.preventDefault(); 14 | document.body.classList.toggle('sb-sidenav-toggled'); 15 | localStorage.setItem('sb|sidebar-toggle', document.body.classList.contains('sb-sidenav-toggled')); 16 | }); 17 | } 18 | },3000) 19 | 20 | }); 21 | -------------------------------------------------------------------------------- /src/ClientApps/CleanHr.Blazor/wwwroot/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "BlazorWasmApp", 3 | "short_name": "BlazorWasmApp", 4 | "start_url": "./", 5 | "display": "standalone", 6 | "background_color": "#ffffff", 7 | "theme_color": "#03173d", 8 | "icons": [ 9 | { 10 | "src": "icon-512.png", 11 | "type": "image/png", 12 | "sizes": "512x512" 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /src/ClientApps/CleanHr.Blazor/wwwroot/service-worker.js: -------------------------------------------------------------------------------- 1 | // In development, always fetch from the network and do not enable offline support. 2 | // This is because caching would make development more difficult (changes would not 3 | // be reflected on the first load after each change). 4 | self.addEventListener('fetch', () => { }); 5 | -------------------------------------------------------------------------------- /src/ClientApps/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build 2 | WORKDIR /src 3 | COPY ["ClearHr.Blazor/ClearnHr.Blazor.csproj", "ClearnHr.Blazor/"] 4 | RUN dotnet restore "CleanHr.Blazor/CleanHr.Blazor.csproj" 5 | COPY . . 6 | WORKDIR "/src/ClientApps/CleanHr.Blazor" 7 | RUN dotnet build "CleanHr.Blazor.csproj" -c Release -o /app/build 8 | 9 | FROM build AS publish 10 | RUN dotnet publish "CleanHr.Blazor.csproj" -c Release -o /app/publish 11 | 12 | FROM nginx:alpine AS final 13 | WORKDIR /usr/share/nginx/html 14 | COPY --from=publish /app/publish/wwwroot . 15 | COPY nginx.conf /etc/nginx/nginx.conf -------------------------------------------------------------------------------- /src/ClientApps/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.9' 2 | 3 | services: 4 | blazorwasmapp: 5 | image: cleanhrblazor 6 | build: 7 | context: . 8 | dockerfile: Dockerfile 9 | ports: ["7300:80"] -------------------------------------------------------------------------------- /src/ClientApps/nginx.conf: -------------------------------------------------------------------------------- 1 | events { } 2 | http { 3 | include mime.types; 4 | 5 | server { 6 | listen 80; 7 | 8 | location / { 9 | root /usr/share/nginx/html; 10 | try_files $uri $uri/ /index.html =404; 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /src/ServerApp/Core/CleanHr.Application/Caching/Handlers/IDepartmentCacheHandler.cs: -------------------------------------------------------------------------------- 1 | using TanvirArjel.Extensions.Microsoft.DependencyInjection; 2 | 3 | namespace CleanHr.Application.Caching.Handlers; 4 | 5 | [ScopedService] 6 | public interface IDepartmentCacheHandler 7 | { 8 | Task RemoveListAsync(); 9 | } 10 | -------------------------------------------------------------------------------- /src/ServerApp/Core/CleanHr.Application/Caching/Handlers/IEmployeeCacheHandler.cs: -------------------------------------------------------------------------------- 1 | namespace CleanHr.Application.Caching.Handlers; 2 | 3 | public interface IEmployeeCacheHandler 4 | { 5 | Task RemoveDetailsByIdAsync(Guid employeeId); 6 | } 7 | -------------------------------------------------------------------------------- /src/ServerApp/Core/CleanHr.Application/Caching/Repositories/IDepartmentCacheRepository.cs: -------------------------------------------------------------------------------- 1 | using CleanHr.Application.Queries.DepartmentQueries; 2 | using CleanHr.Domain.Aggregates.DepartmentAggregate; 3 | using TanvirArjel.Extensions.Microsoft.DependencyInjection; 4 | 5 | namespace CleanHr.Application.Caching.Repositories; 6 | 7 | [ScopedService] 8 | public interface IDepartmentCacheRepository 9 | { 10 | Task> GetListAsync(); 11 | 12 | Task GetByIdAsync(Guid departmentId); 13 | 14 | Task GetDetailsByIdAsync(Guid departmentId); 15 | } 16 | -------------------------------------------------------------------------------- /src/ServerApp/Core/CleanHr.Application/Caching/Repositories/IEmployeeCacheRepository.cs: -------------------------------------------------------------------------------- 1 | using CleanHr.Application.Queries.EmployeeQueries; 2 | using TanvirArjel.Extensions.Microsoft.DependencyInjection; 3 | 4 | namespace CleanHr.Application.Caching.Repositories; 5 | 6 | [ScopedService] 7 | public interface IEmployeeCacheRepository 8 | { 9 | Task GetDetailsByIdAsync(Guid employeeId); 10 | } 11 | -------------------------------------------------------------------------------- /src/ServerApp/Core/CleanHr.Application/CleanHr.Application.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net8.0 4 | true 5 | AllEnabledByDefault 6 | true 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | all 20 | runtime; build; native; contentfiles; analyzers; buildtransitive 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /src/ServerApp/Core/CleanHr.Application/Commands/DepartmentCommands/CreateDepartmentCommand.cs: -------------------------------------------------------------------------------- 1 | using CleanHr.Application.Caching.Handlers; 2 | using CleanHr.Domain.Aggregates.DepartmentAggregate; 3 | using CleanHr.Domain.ValueObjects; 4 | using MediatR; 5 | using TanvirArjel.ArgumentChecker; 6 | 7 | namespace CleanHr.Application.Commands.DepartmentCommands; 8 | 9 | public sealed record CreateDepartmentCommand(string Name, string Description) : IRequest; 10 | 11 | internal class CreateDepartmentCommandHandler : IRequestHandler 12 | { 13 | private readonly IDepartmentRepository _departmentRepository; 14 | private readonly IDepartmentCacheHandler _departmentCacheHandler; 15 | 16 | public CreateDepartmentCommandHandler( 17 | IDepartmentRepository departmentRepository, 18 | IDepartmentCacheHandler departmentCacheHandler) 19 | { 20 | ArgumentNullException.ThrowIfNull(departmentCacheHandler); 21 | ArgumentNullException.ThrowIfNull(departmentCacheHandler); 22 | 23 | _departmentRepository = departmentRepository; 24 | _departmentCacheHandler = departmentCacheHandler; 25 | } 26 | 27 | public async Task Handle(CreateDepartmentCommand request, CancellationToken cancellationToken) 28 | { 29 | _ = request.ThrowIfNull(nameof(request)); 30 | 31 | DepartmentName departmentName = new(request.Name); 32 | 33 | Department department = await Department.CreateAsync(_departmentRepository, departmentName, request.Description); 34 | 35 | // Persist to the database 36 | await _departmentRepository.InsertAsync(department); 37 | 38 | // Remove the cache 39 | await _departmentCacheHandler.RemoveListAsync(); 40 | 41 | return department.Id; 42 | } 43 | } -------------------------------------------------------------------------------- /src/ServerApp/Core/CleanHr.Application/Commands/DepartmentCommands/DeleteDepartmentCommand.cs: -------------------------------------------------------------------------------- 1 | using CleanHr.Application.Caching.Handlers; 2 | using CleanHr.Domain.Aggregates.DepartmentAggregate; 3 | using CleanHr.Domain.Exceptions; 4 | using MediatR; 5 | using TanvirArjel.ArgumentChecker; 6 | 7 | namespace CleanHr.Application.Commands.DepartmentCommands; 8 | 9 | public sealed class DeleteDepartmentCommand(Guid departmentId) : IRequest 10 | { 11 | public Guid Id { get; } = departmentId.ThrowIfEmpty(nameof(departmentId)); 12 | } 13 | 14 | internal class DeleteDepartmentCommandHandler : IRequestHandler 15 | { 16 | private readonly IDepartmentRepository _departmentRepository; 17 | private readonly IDepartmentCacheHandler _departmentCacheHandler; 18 | 19 | public DeleteDepartmentCommandHandler( 20 | IDepartmentRepository departmentRepository, 21 | IDepartmentCacheHandler departmentCacheHandler) 22 | { 23 | ArgumentNullException.ThrowIfNull(departmentCacheHandler); 24 | ArgumentNullException.ThrowIfNull(departmentCacheHandler); 25 | 26 | _departmentRepository = departmentRepository; 27 | _departmentCacheHandler = departmentCacheHandler; 28 | } 29 | 30 | public async Task Handle(DeleteDepartmentCommand request, CancellationToken cancellationToken) 31 | { 32 | _ = request.ThrowIfNull(nameof(request)); 33 | 34 | Department department = await _departmentRepository.GetByIdAsync(request.Id); 35 | 36 | if (department == null) 37 | { 38 | throw new EntityNotFoundException(typeof(Department), request.Id); 39 | } 40 | 41 | await _departmentRepository.DeleteAsync(department); 42 | await _departmentCacheHandler.RemoveListAsync(); 43 | } 44 | } -------------------------------------------------------------------------------- /src/ServerApp/Core/CleanHr.Application/Commands/DepartmentCommands/UpdateDepartmentCommand.cs: -------------------------------------------------------------------------------- 1 | using CleanHr.Application.Caching.Handlers; 2 | using CleanHr.Domain.Aggregates.DepartmentAggregate; 3 | using CleanHr.Domain.Exceptions; 4 | using CleanHr.Domain.ValueObjects; 5 | using MediatR; 6 | using TanvirArjel.ArgumentChecker; 7 | 8 | namespace CleanHr.Application.Commands.DepartmentCommands; 9 | 10 | public sealed record UpdateDepartmentCommand( 11 | Guid Id, 12 | string Name, 13 | string Description, 14 | bool IsActive) : IRequest; 15 | 16 | internal sealed class UpdateDepartmentCommandHandler( 17 | IDepartmentRepository departmentRepository, 18 | IDepartmentCacheHandler departmentCacheHandler) : IRequestHandler 19 | { 20 | public async Task Handle(UpdateDepartmentCommand request, CancellationToken cancellationToken) 21 | { 22 | request.ThrowIfNull(nameof(request)); 23 | 24 | Department departmentToBeUpdated = await departmentRepository.GetByIdAsync(request.Id); 25 | 26 | if (departmentToBeUpdated == null) 27 | { 28 | throw new EntityNotFoundException(typeof(Department), request.Id); 29 | } 30 | 31 | DepartmentName departmentName = new(request.Name); 32 | 33 | await departmentToBeUpdated.SetNameAsync(departmentRepository, departmentName); 34 | departmentToBeUpdated.SetDescription(request.Description); 35 | departmentToBeUpdated.IsActive = request.IsActive; 36 | 37 | await departmentRepository.UpdateAsync(departmentToBeUpdated); 38 | 39 | await departmentCacheHandler.RemoveListAsync(); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/ServerApp/Core/CleanHr.Application/Commands/EmployeeCommands/CreateEmployeeCommand.cs: -------------------------------------------------------------------------------- 1 | using CleanHr.Domain.Aggregates.EmployeeAggregate; 2 | using CleanHr.Domain.ValueObjects; 3 | using MediatR; 4 | using TanvirArjel.ArgumentChecker; 5 | 6 | namespace CleanHr.Application.Commands.EmployeeCommands; 7 | 8 | public record CreateEmployeeCommand( 9 | string FirstName, 10 | string LastName, 11 | Guid DepartmentId, 12 | DateTime DateOfBirth, 13 | string Email, 14 | string PhoneNumber) : IRequest; 15 | 16 | internal class CreateEmployeeCommandHandler( 17 | IEmployeeRepository employeeRepository, 18 | EmployeeFactory employeeFactory) : IRequestHandler 19 | { 20 | private readonly IEmployeeRepository _employeeRepository = employeeRepository ?? throw new ArgumentNullException(nameof(employeeRepository)); 21 | private readonly EmployeeFactory _employeeFactory = employeeFactory ?? throw new ArgumentNullException(nameof(employeeFactory)); 22 | 23 | public async Task Handle(CreateEmployeeCommand request, CancellationToken cancellationToken) 24 | { 25 | request.ThrowIfNull(nameof(request)); 26 | 27 | EmployeeName name = new(request.FirstName, request.LastName); 28 | DateOfBirth dateOfBirth = new(request.DateOfBirth); 29 | Email email = new(request.Email); 30 | PhoneNumber phoneNumber = new(request.PhoneNumber); 31 | 32 | Employee employee = _employeeFactory.Create(name, request.DepartmentId, dateOfBirth, email, phoneNumber); 33 | 34 | await _employeeRepository.InsertAsync(employee); 35 | return employee.Id; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/ServerApp/Core/CleanHr.Application/Commands/EmployeeCommands/DeleteEmployeeCommand.cs: -------------------------------------------------------------------------------- 1 | using CleanHr.Domain.Aggregates.EmployeeAggregate; 2 | using CleanHr.Domain.Exceptions; 3 | using MediatR; 4 | using TanvirArjel.ArgumentChecker; 5 | 6 | namespace CleanHr.Application.Commands.EmployeeCommands; 7 | 8 | public sealed record DeleteEmployeeCommand(Guid EmployeeId) : IRequest; 9 | 10 | internal class DeleteEmployeeCommandHandler( 11 | IEmployeeRepository employeeRepository) : IRequestHandler 12 | { 13 | private readonly IEmployeeRepository _employeeRepository = employeeRepository ?? throw new ArgumentNullException(nameof(employeeRepository)); 14 | 15 | public async Task Handle(DeleteEmployeeCommand request, CancellationToken cancellationToken) 16 | { 17 | request.ThrowIfNull(nameof(request)); 18 | 19 | Employee employeeToBeDeleted = await _employeeRepository.GetByIdAsync(request.EmployeeId); 20 | 21 | if (employeeToBeDeleted == null) 22 | { 23 | throw new EntityNotFoundException(typeof(Employee), request.EmployeeId); 24 | } 25 | 26 | await _employeeRepository.DeleteAsync(employeeToBeDeleted); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/ServerApp/Core/CleanHr.Application/Commands/IdentityCommands/UserCommands/StoreRefreshTokenCommand.cs: -------------------------------------------------------------------------------- 1 | using CleanHr.Domain.Aggregates.IdentityAggregate; 2 | using MediatR; 3 | using TanvirArjel.ArgumentChecker; 4 | using TanvirArjel.EFCore.GenericRepository; 5 | 6 | namespace CleanHr.Application.Commands.IdentityCommands.UserCommands; 7 | 8 | public sealed class StoreRefreshTokenCommand(Guid userId, string token) : IRequest 9 | { 10 | public Guid UserId { get; } = userId.ThrowIfEmpty(nameof(userId)); 11 | 12 | public string Token { get; } = token.ThrowIfNullOrEmpty(nameof(token)); 13 | } 14 | 15 | internal class StoreRefreshTokenCommandHandler : IRequestHandler 16 | { 17 | private readonly IRepository _repository; 18 | 19 | public StoreRefreshTokenCommandHandler(IRepository repository) 20 | { 21 | _repository = repository ?? throw new ArgumentNullException(nameof(repository)); 22 | } 23 | 24 | public async Task Handle(StoreRefreshTokenCommand request, CancellationToken cancellationToken) 25 | { 26 | request.ThrowIfNull(nameof(request)); 27 | 28 | RefreshToken refreshToken = new() 29 | { 30 | UserId = request.UserId, 31 | Token = request.Token, 32 | CreatedAtUtc = DateTime.UtcNow, 33 | ExpireAtUtc = DateTime.UtcNow.AddDays(30) 34 | }; 35 | 36 | _repository.Add(refreshToken); 37 | await _repository.SaveChangesAsync(cancellationToken); 38 | return refreshToken; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/ServerApp/Core/CleanHr.Application/Commands/IdentityCommands/UserCommands/StoreUserPasswordCommand.cs: -------------------------------------------------------------------------------- 1 | using CleanHr.Domain.Aggregates.IdentityAggregate; 2 | using MediatR; 3 | using Microsoft.AspNetCore.Identity; 4 | using TanvirArjel.ArgumentChecker; 5 | using TanvirArjel.EFCore.GenericRepository; 6 | 7 | namespace CleanHr.Application.Commands.IdentityCommands.UserCommands; 8 | 9 | public sealed class StoreUserPasswordCommand(ApplicationUser user, string password) : IRequest 10 | { 11 | public ApplicationUser User { get; } = user.ThrowIfNull(nameof(user)); 12 | 13 | public string Password { get; } = password.ThrowIfNullOrEmpty(nameof(password)); 14 | } 15 | 16 | internal class StoreUserPasswordCommandHandler : IRequestHandler 17 | { 18 | private readonly IPasswordHasher _passwordHasher; 19 | private readonly IRepository _repository; 20 | 21 | public StoreUserPasswordCommandHandler( 22 | IPasswordHasher passwordHasher, 23 | IRepository repository) 24 | { 25 | _passwordHasher = passwordHasher ?? throw new ArgumentNullException(nameof(passwordHasher)); 26 | _repository = repository ?? throw new ArgumentNullException(nameof(repository)); 27 | } 28 | 29 | public async Task Handle(StoreUserPasswordCommand request, CancellationToken cancellationToken) 30 | { 31 | request.ThrowIfNull(nameof(request)); 32 | 33 | string passwordHash = _passwordHasher.HashPassword(request.User, request.Password); 34 | 35 | UserOldPassword userOldPassword = new() 36 | { 37 | UserId = request.User.Id, 38 | PasswordHash = passwordHash, 39 | SetAtUtc = DateTime.UtcNow 40 | }; 41 | 42 | _repository.Add(userOldPassword); 43 | await _repository.SaveChangesAsync(cancellationToken); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/ServerApp/Core/CleanHr.Application/Commands/IdentityCommands/UserCommands/UpdateDialCodeCommand.cs: -------------------------------------------------------------------------------- 1 | using CleanHr.Domain.Aggregates.IdentityAggregate; 2 | using MediatR; 3 | using TanvirArjel.ArgumentChecker; 4 | using TanvirArjel.EFCore.GenericRepository; 5 | 6 | namespace CleanHr.Application.Commands.IdentityCommands.UserCommands; 7 | 8 | public sealed class UpdateDialCodeCommand : IRequest 9 | { 10 | public UpdateDialCodeCommand(Guid userId, string dialCode) 11 | { 12 | UserId = userId.ThrowIfEmpty(nameof(UserId)); 13 | DialCode = dialCode.ThrowIfNullOrEmpty(nameof(DialCode)); 14 | } 15 | 16 | public Guid UserId { get; } 17 | 18 | public string DialCode { get; } 19 | } 20 | 21 | internal class UpdateDialCodeCommandHandler : IRequestHandler 22 | { 23 | private readonly IRepository _repository; 24 | 25 | public UpdateDialCodeCommandHandler(IRepository repository) 26 | { 27 | _repository = repository ?? throw new ArgumentNullException(nameof(repository)); 28 | } 29 | 30 | public async Task Handle(UpdateDialCodeCommand request, CancellationToken cancellationToken) 31 | { 32 | request.ThrowIfNull(nameof(request)); 33 | 34 | ApplicationUser applicationUserToBeUpdated = await _repository.GetByIdAsync(request.UserId, cancellationToken); 35 | 36 | if (applicationUserToBeUpdated == null) 37 | { 38 | throw new InvalidOperationException($"The ApplicationUser does not exist with id value: {request.UserId}."); 39 | } 40 | 41 | applicationUserToBeUpdated.DialCode = request.DialCode.StartsWith('+') ? request.DialCode : $"+{request.DialCode}"; 42 | _repository.Update(applicationUserToBeUpdated); 43 | await _repository.SaveChangesAsync(cancellationToken); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/ServerApp/Core/CleanHr.Application/Commands/IdentityCommands/UserCommands/UpdateLanguageCultureCommand.cs: -------------------------------------------------------------------------------- 1 | using CleanHr.Domain.Aggregates.IdentityAggregate; 2 | using MediatR; 3 | using TanvirArjel.ArgumentChecker; 4 | using TanvirArjel.EFCore.GenericRepository; 5 | 6 | namespace CleanHr.Application.Commands.IdentityCommands.UserCommands; 7 | 8 | public sealed class UpdateLanguageCultureCommand(Guid userId, string languageCulture) : IRequest 9 | { 10 | public Guid UserId { get; } = userId.ThrowIfEmpty(nameof(userId)); 11 | 12 | public string LanguageCulture { get; } = languageCulture.ThrowIfNullOrEmpty(nameof(languageCulture)); 13 | } 14 | 15 | internal class UpdateLanguageCultureCommandHandler(IRepository repository) : IRequestHandler 16 | { 17 | private readonly IRepository _repository = repository ?? throw new ArgumentNullException(nameof(repository)); 18 | 19 | public async Task Handle(UpdateLanguageCultureCommand request, CancellationToken cancellationToken) 20 | { 21 | request.ThrowIfNull(nameof(request)); 22 | 23 | ApplicationUser userToBeUpdated = await _repository.GetByIdAsync(request.UserId, cancellationToken); 24 | 25 | if (userToBeUpdated == null) 26 | { 27 | throw new InvalidOperationException($"The ApplicationUser does not exist with id value: {request.UserId}."); 28 | } 29 | 30 | userToBeUpdated.LanguageCulture = request.LanguageCulture; 31 | 32 | _repository.Update(userToBeUpdated); 33 | await _repository.SaveChangesAsync(cancellationToken); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/ServerApp/Core/CleanHr.Application/Commands/IdentityCommands/UserCommands/UpdateRefreshTokenCommand.cs: -------------------------------------------------------------------------------- 1 | using CleanHr.Domain.Aggregates.IdentityAggregate; 2 | using MediatR; 3 | using TanvirArjel.ArgumentChecker; 4 | using TanvirArjel.EFCore.GenericRepository; 5 | 6 | namespace CleanHr.Application.Commands.IdentityCommands.UserCommands; 7 | 8 | public sealed class UpdateRefreshTokenCommand(Guid userId, string token) : IRequest 9 | { 10 | public Guid UserId { get; } = userId.ThrowIfEmpty(nameof(userId)); 11 | 12 | public string Token { get; } = token.ThrowIfNullOrEmpty(nameof(token)); 13 | } 14 | 15 | internal class UpdateRefreshTokenCommandHandler(IRepository repository) : IRequestHandler 16 | { 17 | private readonly IRepository _repository = repository ?? throw new ArgumentNullException(nameof(repository)); 18 | 19 | public async Task Handle(UpdateRefreshTokenCommand request, CancellationToken cancellationToken) 20 | { 21 | request.ThrowIfNull(nameof(request)); 22 | 23 | RefreshToken refreshTokenToBeUpdated = await _repository.GetAsync(rt => rt.UserId == request.UserId, cancellationToken); 24 | 25 | if (refreshTokenToBeUpdated == null) 26 | { 27 | throw new InvalidOperationException($"The RefreshToken does not exist with id value: {request.UserId}."); 28 | } 29 | 30 | refreshTokenToBeUpdated.Token = request.Token; 31 | refreshTokenToBeUpdated.ExpireAtUtc = DateTime.UtcNow; 32 | refreshTokenToBeUpdated.CreatedAtUtc = DateTime.UtcNow.AddDays(10); 33 | 34 | _repository.Update(refreshTokenToBeUpdated); 35 | await _repository.SaveChangesAsync(cancellationToken); 36 | 37 | return refreshTokenToBeUpdated; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/ServerApp/Core/CleanHr.Application/GlobalUsings.cs: -------------------------------------------------------------------------------- 1 | global using System; 2 | global using System.Collections.Generic; 3 | global using System.Threading; 4 | global using System.Threading.Tasks; -------------------------------------------------------------------------------- /src/ServerApp/Core/CleanHr.Application/Infrastructures/IEmailSender.cs: -------------------------------------------------------------------------------- 1 | using TanvirArjel.ArgumentChecker; 2 | using TanvirArjel.Extensions.Microsoft.DependencyInjection; 3 | 4 | namespace CleanHr.Application.Infrastructures; 5 | 6 | [SingletonService] 7 | public interface IEmailSender 8 | { 9 | Task SendAsync(EmailMessage emailMessage); 10 | } 11 | 12 | public sealed class EmailMessage( 13 | string receiverEmail, 14 | string receiverName, 15 | string senderEmail, 16 | string senderName, 17 | string subject, 18 | string mailBody) 19 | { 20 | public EmailMessage(string receiverEmail, string subject, string mailBody) 21 | : this(receiverEmail, receiverName: null, subject, mailBody) 22 | { 23 | } 24 | 25 | public EmailMessage(string receiverEmail, string receiverName, string subject, string mailBody) 26 | : this(receiverEmail, receiverName, senderEmail: null, senderName: null, subject, mailBody) 27 | { 28 | } 29 | 30 | public string ReceiverEmail { get; } = receiverEmail.ThrowIfNullOrEmpty(nameof(receiverEmail)); 31 | 32 | public string ReceiverName { get; } = receiverName; 33 | 34 | public string SenderEmail { get; } = senderEmail != null ? senderEmail.ThrowIfNotValidEmail(nameof(senderEmail)) : senderEmail; 35 | 36 | public string SenderName { get; } = senderName; 37 | 38 | public string Subject { get; } = subject.ThrowIfNullOrEmpty(nameof(subject)); 39 | 40 | public string MailBody { get; } = mailBody.ThrowIfNullOrEmpty(nameof(mailBody)); 41 | } 42 | -------------------------------------------------------------------------------- /src/ServerApp/Core/CleanHr.Application/Infrastructures/IExceptionLogger.cs: -------------------------------------------------------------------------------- 1 | using TanvirArjel.Extensions.Microsoft.DependencyInjection; 2 | 3 | namespace CleanHr.Application.Infrastructures; 4 | 5 | [SingletonService] 6 | public interface IExceptionLogger 7 | { 8 | Task LogAsync(Exception exception); 9 | 10 | Task LogAsync(Exception exception, object parameters); 11 | 12 | Task LogAsync(Exception exception, string requestPath, string requestBody); 13 | } 14 | -------------------------------------------------------------------------------- /src/ServerApp/Core/CleanHr.Application/Queries/DepartmentQueries/GetDepartmentByIdQuery.cs: -------------------------------------------------------------------------------- 1 | using System.Linq.Expressions; 2 | using CleanHr.Domain.Aggregates.DepartmentAggregate; 3 | using MediatR; 4 | using TanvirArjel.ArgumentChecker; 5 | using TanvirArjel.EFCore.GenericRepository; 6 | 7 | namespace CleanHr.Application.Queries.DepartmentQueries; 8 | 9 | public sealed class GetDepartmentByIdQuery(Guid id) : IRequest 10 | { 11 | public Guid Id { get; } = id.ThrowIfEmpty(nameof(id)); 12 | } 13 | 14 | public class DepartmentDetailsDto 15 | { 16 | public Guid Id { get; set; } 17 | 18 | public string Name { get; set; } 19 | 20 | public string Description { get; set; } 21 | 22 | public bool IsActive { get; set; } 23 | 24 | public DateTime CreatedAtUtc { get; set; } 25 | 26 | public DateTime? LastModifiedAtUtc { get; set; } 27 | } 28 | 29 | // Handler 30 | internal class GetDepartmentByIdQueryHandler(IQueryRepository repository) : IRequestHandler 31 | { 32 | private readonly IQueryRepository _repository = repository ?? throw new ArgumentNullException(nameof(repository)); 33 | 34 | public async Task Handle(GetDepartmentByIdQuery request, CancellationToken cancellationToken) 35 | { 36 | request.ThrowIfNull(nameof(request)); 37 | 38 | Expression> selectExp = d => new DepartmentDetailsDto 39 | { 40 | Id = d.Id, 41 | Name = d.Name.Value, 42 | Description = d.Description, 43 | IsActive = d.IsActive, 44 | CreatedAtUtc = d.CreatedAtUtc, 45 | LastModifiedAtUtc = d.LastModifiedAtUtc 46 | }; 47 | 48 | DepartmentDetailsDto departmentDetailsDto = await _repository.GetByIdAsync(request.Id, selectExp, cancellationToken); 49 | 50 | return departmentDetailsDto; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/ServerApp/Core/CleanHr.Application/Queries/DepartmentQueries/GetDepartmentListQuery.cs: -------------------------------------------------------------------------------- 1 | using CleanHr.Application.Caching.Repositories; 2 | using MediatR; 3 | using TanvirArjel.ArgumentChecker; 4 | 5 | namespace CleanHr.Application.Queries.DepartmentQueries; 6 | 7 | public sealed class GetDepartmentListQuery : IRequest> 8 | { 9 | } 10 | 11 | public class DepartmentDto 12 | { 13 | public Guid Id { get; set; } 14 | 15 | public string Name { get; set; } 16 | 17 | public string Description { get; set; } 18 | 19 | public bool IsActive { get; set; } 20 | 21 | public DateTime CreatedAtUtc { get; set; } 22 | 23 | public DateTime? LastModifiedAtUtc { get; set; } 24 | } 25 | 26 | internal class GetDepartmentListQueryHandler(IDepartmentCacheRepository departmentCacheRepository) : IRequestHandler> 27 | { 28 | private readonly IDepartmentCacheRepository _departmentCacheRepository = departmentCacheRepository ?? throw new ArgumentNullException(nameof(departmentCacheRepository)); 29 | 30 | public async Task> Handle(GetDepartmentListQuery request, CancellationToken cancellationToken) 31 | { 32 | request.ThrowIfNull(nameof(request)); 33 | 34 | List departmentDtos = await _departmentCacheRepository.GetListAsync(); 35 | return departmentDtos; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/ServerApp/Core/CleanHr.Application/Queries/DepartmentQueries/IsDepartmentExistentByIdQuery.cs: -------------------------------------------------------------------------------- 1 | using CleanHr.Domain.Aggregates.DepartmentAggregate; 2 | using MediatR; 3 | using TanvirArjel.ArgumentChecker; 4 | using TanvirArjel.EFCore.GenericRepository; 5 | 6 | namespace CleanHr.Application.Queries.DepartmentQueries; 7 | 8 | public sealed class IsDepartmentExistentByIdQuery(Guid departmentId) : IRequest 9 | { 10 | public Guid Id { get; } = departmentId.ThrowIfEmpty(nameof(departmentId)); 11 | } 12 | 13 | internal class IsDepartmentExistentByIdQueryHandler(IQueryRepository repository) : IRequestHandler 14 | { 15 | private readonly IQueryRepository _repository = repository ?? throw new ArgumentNullException(nameof(repository)); 16 | 17 | public async Task Handle(IsDepartmentExistentByIdQuery request, CancellationToken cancellationToken) 18 | { 19 | request.ThrowIfNull(nameof(request)); 20 | 21 | bool isExists = await _repository.ExistsAsync(d => d.Id == request.Id, cancellationToken); 22 | return isExists; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/ServerApp/Core/CleanHr.Application/Queries/DepartmentQueries/IsDepartmentExistentByNameQuery.cs: -------------------------------------------------------------------------------- 1 | using CleanHr.Domain.Aggregates.DepartmentAggregate; 2 | using MediatR; 3 | using TanvirArjel.ArgumentChecker; 4 | using TanvirArjel.EFCore.GenericRepository; 5 | 6 | namespace CleanHr.Application.Queries.DepartmentQueries; 7 | 8 | public sealed class IsDepartmentExistentByNameQuery(string name) : IRequest 9 | { 10 | public string Name { get; set; } = name.ThrowIfNullOrEmpty(nameof(name)); 11 | } 12 | 13 | internal class IsDepartmentExistentByNameQueryHandler(IQueryRepository repository) : IRequestHandler 14 | { 15 | private readonly IQueryRepository _repository = repository ?? throw new ArgumentNullException(nameof(repository)); 16 | 17 | public async Task Handle(IsDepartmentExistentByNameQuery request, CancellationToken cancellationToken) 18 | { 19 | request.ThrowIfNull(nameof(request)); 20 | bool isExists = await _repository.ExistsAsync(d => d.Name.Value == request.Name, cancellationToken); 21 | return isExists; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/ServerApp/Core/CleanHr.Application/Queries/DepartmentQueries/IsDepartmentNameUniqueQuery.cs: -------------------------------------------------------------------------------- 1 | using CleanHr.Domain.Aggregates.DepartmentAggregate; 2 | using MediatR; 3 | using TanvirArjel.ArgumentChecker; 4 | using TanvirArjel.EFCore.GenericRepository; 5 | 6 | namespace CleanHr.Application.Queries.DepartmentQueries; 7 | 8 | public sealed class IsDepartmentNameUniqueQuery(Guid departmentId, string name) : IRequest 9 | { 10 | public Guid Id { get; } = departmentId; 11 | 12 | public string Name { get; } = name.ThrowIfNullOrEmpty(nameof(name)); 13 | } 14 | 15 | internal class IsDepartmentNameUniqueQueryHandler( 16 | IQueryRepository repository) : IRequestHandler 17 | { 18 | private readonly IQueryRepository _repository = repository ?? throw new ArgumentNullException(nameof(repository)); 19 | 20 | public async Task Handle(IsDepartmentNameUniqueQuery request, CancellationToken cancellationToken) 21 | { 22 | request.ThrowIfNull(nameof(request)); 23 | 24 | bool isExistent = await _repository.ExistsAsync(d => d.Id != request.Id && d.Name.Value == request.Name, cancellationToken); 25 | return !isExistent; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/ServerApp/Core/CleanHr.Application/Queries/EmployeeQueries/GetEmployeeByIdQuery.cs: -------------------------------------------------------------------------------- 1 | using CleanHr.Application.Caching.Repositories; 2 | using MediatR; 3 | using TanvirArjel.ArgumentChecker; 4 | 5 | namespace CleanHr.Application.Queries.EmployeeQueries; 6 | 7 | public sealed class GetEmployeeByIdQuery(Guid employeeId) : IRequest 8 | { 9 | public Guid Id { get; } = employeeId.ThrowIfEmpty(nameof(employeeId)); 10 | } 11 | 12 | internal class GetEmployeeByIdQueryHandler(IEmployeeCacheRepository employeeCacheRepository) : IRequestHandler 13 | { 14 | private readonly IEmployeeCacheRepository _employeeCacheRepository = employeeCacheRepository ?? throw new ArgumentNullException(nameof(employeeCacheRepository)); 15 | 16 | public async Task Handle(GetEmployeeByIdQuery request, CancellationToken cancellationToken) 17 | { 18 | request.ThrowIfNull(nameof(request)); 19 | 20 | EmployeeDetailsDto employeeDetailsDto = await _employeeCacheRepository.GetDetailsByIdAsync(request.Id); 21 | return employeeDetailsDto; 22 | } 23 | } 24 | 25 | public class EmployeeDetailsDto 26 | { 27 | public Guid Id { get; set; } 28 | 29 | public string Name { get; set; } 30 | 31 | public Guid DepartmentId { get; set; } 32 | 33 | public string DepartmentName { get; set; } 34 | 35 | public DateTime DateOfBirth { get; set; } 36 | 37 | public string Email { get; set; } 38 | 39 | public string PhoneNumber { get; set; } 40 | 41 | public bool IsActive { get; set; } 42 | 43 | public DateTime CreatedAtUtc { get; set; } 44 | 45 | public DateTime? LastModifiedAtUtc { get; set; } 46 | } 47 | -------------------------------------------------------------------------------- /src/ServerApp/Core/CleanHr.Application/Queries/EmployeeQueries/IsEmployeeExistentByIdQuery.cs: -------------------------------------------------------------------------------- 1 | using CleanHr.Domain.Aggregates.EmployeeAggregate; 2 | using MediatR; 3 | using TanvirArjel.ArgumentChecker; 4 | 5 | namespace CleanHr.Application.Queries.EmployeeQueries; 6 | 7 | public sealed class IsEmployeeExistentByIdQuery(Guid employeeId) : IRequest 8 | { 9 | public Guid Id { get; } = employeeId.ThrowIfEmpty(nameof(employeeId)); 10 | } 11 | 12 | internal class IsEmployeeExistentByIdQueryHandler(IEmployeeRepository employeeRepository) : IRequestHandler 13 | { 14 | private readonly IEmployeeRepository _employeeRepository = employeeRepository ?? throw new ArgumentNullException(nameof(employeeRepository)); 15 | 16 | public async Task Handle(IsEmployeeExistentByIdQuery request, CancellationToken cancellationToken) 17 | { 18 | request.ThrowIfNull(nameof(request)); 19 | 20 | bool isExistent = await _employeeRepository.ExistsAsync(e => e.Id == request.Id); 21 | return isExistent; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/ServerApp/Core/CleanHr.Application/Queries/IdentityQueries/UserQueries/CheckIfOldPasswordQuery.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using CleanHr.Domain.Aggregates.IdentityAggregate; 3 | using MediatR; 4 | using Microsoft.AspNetCore.Identity; 5 | using Microsoft.EntityFrameworkCore; 6 | using TanvirArjel.ArgumentChecker; 7 | using TanvirArjel.EFCore.GenericRepository; 8 | 9 | namespace CleanHr.Application.Queries.IdentityQueries.UserQueries; 10 | 11 | public sealed class CheckIfOldPasswordQuery(ApplicationUser user, string password) : IRequest 12 | { 13 | public ApplicationUser User { get; } = user.ThrowIfNull(nameof(user)); 14 | 15 | public string Password { get; } = password.ThrowIfNullOrEmpty(nameof(password)); 16 | } 17 | 18 | internal class CheckIfOldPasswordQueryHandler( 19 | IRepository repository, 20 | IPasswordHasher passwordHasher) : IRequestHandler 21 | { 22 | private readonly IRepository _repository = repository ?? throw new ArgumentNullException(nameof(repository)); 23 | private readonly IPasswordHasher _passwordHasher = passwordHasher ?? throw new ArgumentNullException(nameof(passwordHasher)); 24 | 25 | public async Task Handle(CheckIfOldPasswordQuery request, CancellationToken cancellationToken) 26 | { 27 | request.ThrowIfNull(nameof(request)); 28 | 29 | List userOldPasswords = await _repository.GetQueryable() 30 | .Where(uop => uop.UserId == request.User.Id).ToListAsync(cancellationToken); 31 | 32 | if (userOldPasswords.Count == 0) 33 | { 34 | return false; 35 | } 36 | 37 | foreach (UserOldPassword item in userOldPasswords) 38 | { 39 | PasswordVerificationResult passwordVerificationResult = _passwordHasher.VerifyHashedPassword(request.User, item.PasswordHash, request.Password); 40 | if (passwordVerificationResult == PasswordVerificationResult.Success) 41 | { 42 | return true; 43 | } 44 | } 45 | 46 | return false; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/ServerApp/Core/CleanHr.Application/Queries/IdentityQueries/UserQueries/GetEmailVerificationCodeQuery.cs: -------------------------------------------------------------------------------- 1 | using CleanHr.Domain.Aggregates.IdentityAggregate; 2 | using MediatR; 3 | using TanvirArjel.ArgumentChecker; 4 | using TanvirArjel.EFCore.GenericRepository; 5 | 6 | namespace CleanHr.Application.Queries.IdentityQueries.UserQueries; 7 | 8 | public sealed class GetEmailVerificationCodeQuery(string email, string code) : IRequest 9 | { 10 | public string Email { get; } = email.ThrowIfNotValidEmail(nameof(email)); 11 | 12 | public string Code { get; } = code.ThrowIfNullOrEmpty(nameof(code)); 13 | } 14 | 15 | internal class GetEmailVerificationCodeQueryHandler( 16 | IRepository repository) : IRequestHandler 17 | { 18 | private readonly IRepository _repository = repository ?? throw new ArgumentNullException(nameof(repository)); 19 | 20 | public async Task Handle( 21 | GetEmailVerificationCodeQuery request, 22 | CancellationToken cancellationToken) 23 | { 24 | request.ThrowIfNull(nameof(request)); 25 | 26 | EmailVerificationCode emailVerificationCode = await _repository 27 | .GetAsync(evc => evc.Email == request.Email && evc.Code == request.Code && evc.UsedAtUtc == null, cancellationToken); 28 | 29 | return emailVerificationCode; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/ServerApp/Core/CleanHr.Application/Queries/IdentityQueries/UserQueries/GetLanguageCultureQuery.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using CleanHr.Domain.Aggregates.IdentityAggregate; 3 | using MediatR; 4 | using Microsoft.EntityFrameworkCore; 5 | using TanvirArjel.ArgumentChecker; 6 | using TanvirArjel.EFCore.GenericRepository; 7 | 8 | namespace CleanHr.Application.Queries.IdentityQueries.UserQueries; 9 | 10 | public sealed class GetLanguageCultureQuery(Guid userId) : IRequest 11 | { 12 | public Guid UserId { get; } = userId.ThrowIfEmpty(nameof(userId)); 13 | } 14 | 15 | internal class GetLanguageCultureQueryHandler(IRepository repository) : IRequestHandler 16 | { 17 | private readonly IRepository _repository = repository ?? throw new ArgumentNullException(nameof(repository)); 18 | 19 | public async Task Handle(GetLanguageCultureQuery request, CancellationToken cancellationToken) 20 | { 21 | request.ThrowIfNull(nameof(request)); 22 | 23 | string userLanguageCulture = await _repository.GetQueryable().Where(u => u.Id == request.UserId) 24 | .Select(u => u.LanguageCulture).FirstOrDefaultAsync(cancellationToken); 25 | 26 | return userLanguageCulture; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/ServerApp/Core/CleanHr.Application/Queries/IdentityQueries/UserQueries/GetPasswordResetCodeQuery.cs: -------------------------------------------------------------------------------- 1 | using CleanHr.Domain.Aggregates.IdentityAggregate; 2 | using MediatR; 3 | using TanvirArjel.ArgumentChecker; 4 | using TanvirArjel.EFCore.GenericRepository; 5 | 6 | namespace CleanHr.Application.Queries.IdentityQueries.UserQueries; 7 | 8 | public sealed class GetPasswordResetCodeQuery(string email, string code) : IRequest 9 | { 10 | public string Email { get; } = email.ThrowIfNotValidEmail(nameof(email)); 11 | 12 | public string Code { get; } = code.ThrowIfNullOrEmpty(nameof(code)); 13 | 14 | private class GetPasswordResetCodeQueryHandler(IRepository repository) : IRequestHandler 15 | { 16 | public async Task Handle(GetPasswordResetCodeQuery request, CancellationToken cancellationToken) 17 | { 18 | request.ThrowIfNull(nameof(request)); 19 | 20 | PasswordResetCode passwordResetCode = await repository 21 | .GetAsync(evc => evc.Email == request.Email && evc.Code == request.Code && evc.UsedAtUtc == null, cancellationToken); 22 | 23 | return passwordResetCode; 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/ServerApp/Core/CleanHr.Application/Queries/IdentityQueries/UserQueries/GetRefreshTokenQuery.cs: -------------------------------------------------------------------------------- 1 | using CleanHr.Domain.Aggregates.IdentityAggregate; 2 | using MediatR; 3 | using TanvirArjel.ArgumentChecker; 4 | using TanvirArjel.EFCore.GenericRepository; 5 | 6 | namespace CleanHr.Application.Queries.IdentityQueries.UserQueries; 7 | 8 | public sealed class GetRefreshTokenQuery(Guid userId) : IRequest 9 | { 10 | public Guid UserId { get; } = userId.ThrowIfEmpty(nameof(userId)); 11 | 12 | private class GetRefreshTokenQueryHanlder(IRepository repository) : IRequestHandler 13 | { 14 | public async Task Handle(GetRefreshTokenQuery request, CancellationToken cancellationToken) 15 | { 16 | request.ThrowIfNull(nameof(request)); 17 | 18 | RefreshToken refreshToken = await repository.GetAsync(rt => rt.UserId == request.UserId, cancellationToken); 19 | 20 | return refreshToken; 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/ServerApp/Core/CleanHr.Application/Queries/IdentityQueries/UserQueries/HasUserActiveEmailVerificationCodeQuery.cs: -------------------------------------------------------------------------------- 1 | using CleanHr.Domain.Aggregates.IdentityAggregate; 2 | using MediatR; 3 | using TanvirArjel.ArgumentChecker; 4 | using TanvirArjel.EFCore.GenericRepository; 5 | 6 | namespace CleanHr.Application.Queries.IdentityQueries.UserQueries; 7 | 8 | public sealed class HasUserActiveEmailVerificationCodeQuery(string email) : IRequest 9 | { 10 | public string Email { get; } = email.ThrowIfNotValidEmail(nameof(email)); 11 | 12 | private class HasUserActiveEmailVerificationCodeQueryHandler(IRepository repository) : IRequestHandler 13 | { 14 | public async Task Handle(HasUserActiveEmailVerificationCodeQuery request, CancellationToken cancellationToken) 15 | { 16 | request.ThrowIfNull(nameof(request)); 17 | 18 | bool isExists = await repository 19 | .ExistsAsync(evc => evc.Email == request.Email && evc.SentAtUtc.AddMinutes(5) > DateTime.UtcNow, cancellationToken); 20 | return isExists; 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/ServerApp/Core/CleanHr.Application/Queries/IdentityQueries/UserQueries/IsRefreshTokenValidQuery.cs: -------------------------------------------------------------------------------- 1 | using CleanHr.Domain.Aggregates.IdentityAggregate; 2 | using MediatR; 3 | using TanvirArjel.ArgumentChecker; 4 | using TanvirArjel.EFCore.GenericRepository; 5 | 6 | namespace CleanHr.Application.Queries.IdentityQueries.UserQueries; 7 | 8 | public sealed class IsRefreshTokenValidQuery(Guid userId, string refreshToken) : IRequest 9 | { 10 | public Guid UserId { get; } = userId; 11 | 12 | public string RefreshToken { get; } = refreshToken; 13 | } 14 | 15 | internal class IsRefreshTokenValidQueryHandler(IRepository repository) : IRequestHandler 16 | { 17 | private readonly IRepository _repository = repository ?? throw new ArgumentNullException(nameof(repository)); 18 | 19 | public async Task Handle(IsRefreshTokenValidQuery request, CancellationToken cancellationToken) 20 | { 21 | request.ThrowIfNull(nameof(request)); 22 | 23 | bool isRefreshTokenValid = await _repository.ExistsAsync(rt => rt.UserId == request.UserId && rt.Token == request.RefreshToken, cancellationToken); 24 | 25 | return isRefreshTokenValid; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/ServerApp/Core/CleanHr.Domain/Aggregates/DepartmentAggregate/DepartmentDomainErrors.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace CleanHr.Domain; 4 | 5 | public static class DepartmentDomainErrors 6 | { 7 | public static string NameNullOrEmpty => "The DepartmentName value cannot be null or empty."; 8 | 9 | public static string DescriptionNullOrEmpty => "The Department description cannot be null or empty."; 10 | 11 | public static string GetDescriptionLengthOutOfRangeMessage(int minLength, int maxLength) 12 | { 13 | return $"The Department description must be in between {minLength} and {maxLength} characters."; 14 | } 15 | 16 | public static string GetNameLengthOutOfRangeErrorMessage(int minLength, int maxLength) 17 | { 18 | return $"The DepartmentName value must be in between {minLength} to {maxLength} characters."; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/ServerApp/Core/CleanHr.Domain/Aggregates/DepartmentAggregate/IDepartmentRepository.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq.Expressions; 3 | using System.Threading.Tasks; 4 | 5 | namespace CleanHr.Domain.Aggregates.DepartmentAggregate; 6 | 7 | public interface IDepartmentRepository 8 | { 9 | Task GetByIdAsync(Guid departmentId); 10 | 11 | Task ExistsAsync(Expression> condition); 12 | 13 | Task InsertAsync(Department department); 14 | 15 | Task UpdateAsync(Department department); 16 | 17 | Task DeleteAsync(Department department); 18 | } 19 | -------------------------------------------------------------------------------- /src/ServerApp/Core/CleanHr.Domain/Aggregates/EmployeeAggregate/EmployeeFactory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using CleanHr.Domain.Aggregates.DepartmentAggregate; 3 | using CleanHr.Domain.ValueObjects; 4 | using TanvirArjel.Extensions.Microsoft.DependencyInjection; 5 | 6 | namespace CleanHr.Domain.Aggregates.EmployeeAggregate; 7 | 8 | [ScopedService] 9 | public sealed class EmployeeFactory(IDepartmentRepository departmentRepository, IEmployeeRepository employeeRepository) 10 | { 11 | public Employee Create( 12 | EmployeeName name, 13 | Guid departmentId, 14 | DateOfBirth dateOfBirth, 15 | Email email, 16 | PhoneNumber phoneNumber) 17 | { 18 | Employee employee = new(departmentRepository, employeeRepository, name, departmentId, dateOfBirth, email, phoneNumber); 19 | 20 | return employee; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/ServerApp/Core/CleanHr.Domain/Aggregates/EmployeeAggregate/IEmployeeRepository.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq.Expressions; 3 | using System.Threading.Tasks; 4 | 5 | namespace CleanHr.Domain.Aggregates.EmployeeAggregate; 6 | 7 | public interface IEmployeeRepository 8 | { 9 | Task GetByIdAsync(Guid employeeId); 10 | 11 | Task ExistsAsync(Expression> condition); 12 | 13 | Task InsertAsync(Employee employee); 14 | 15 | Task UpdateAsync(Employee employeeToBeUpdated); 16 | 17 | Task DeleteAsync(Employee employeeToBeDeleted); 18 | } 19 | -------------------------------------------------------------------------------- /src/ServerApp/Core/CleanHr.Domain/Aggregates/ITimeFields.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace CleanHr.Domain.Aggregates; 4 | 5 | public interface ITimeFields 6 | { 7 | public DateTime CreatedAtUtc { get; set; } 8 | 9 | public DateTime? LastModifiedAtUtc { get; set; } 10 | } 11 | -------------------------------------------------------------------------------- /src/ServerApp/Core/CleanHr.Domain/Aggregates/IdentityAggregate/ApplicationRole.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.AspNetCore.Identity; 3 | 4 | namespace CleanHr.Domain.Aggregates.IdentityAggregate; 5 | 6 | public class ApplicationRole : IdentityRole 7 | { 8 | } 9 | -------------------------------------------------------------------------------- /src/ServerApp/Core/CleanHr.Domain/Aggregates/IdentityAggregate/ApplicationUser.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.AspNetCore.Identity; 3 | 4 | namespace CleanHr.Domain.Aggregates.IdentityAggregate; 5 | 6 | public class ApplicationUser : IdentityUser 7 | { 8 | public string FullName { get; set; } 9 | 10 | public string DialCode { get; set; } 11 | 12 | public string LanguageCulture { get; set; } 13 | 14 | public bool IsDisabled { get; set; } 15 | 16 | public DateTime? LastLoggedInAtUtc { get; set; } 17 | 18 | public RefreshToken RefreshToken { get; set; } 19 | } 20 | -------------------------------------------------------------------------------- /src/ServerApp/Core/CleanHr.Domain/Aggregates/IdentityAggregate/EmailVerificationCode.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace CleanHr.Domain.Aggregates.IdentityAggregate; 4 | 5 | public class EmailVerificationCode 6 | { 7 | public Guid Id { get; set; } 8 | 9 | public string Email { get; set; } 10 | 11 | public string Code { get; set; } 12 | 13 | public DateTime SentAtUtc { get; set; } 14 | 15 | public DateTime? UsedAtUtc { get; set; } 16 | } 17 | -------------------------------------------------------------------------------- /src/ServerApp/Core/CleanHr.Domain/Aggregates/IdentityAggregate/PasswordResetCode.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace CleanHr.Domain.Aggregates.IdentityAggregate; 4 | 5 | public class PasswordResetCode 6 | { 7 | public Guid Id { get; set; } 8 | 9 | public string Email { get; set; } 10 | 11 | public string Code { get; set; } 12 | 13 | public DateTime SentAtUtc { get; set; } 14 | 15 | public DateTime? UsedAtUtc { get; set; } 16 | } 17 | -------------------------------------------------------------------------------- /src/ServerApp/Core/CleanHr.Domain/Aggregates/IdentityAggregate/RefreshToken.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace CleanHr.Domain.Aggregates.IdentityAggregate; 4 | 5 | public class RefreshToken 6 | { 7 | public Guid UserId { get; set; } 8 | 9 | public string Token { get; set; } 10 | 11 | public DateTime CreatedAtUtc { get; set; } 12 | 13 | public DateTime ExpireAtUtc { get; set; } 14 | 15 | // Navigation properties 16 | public ApplicationUser ApplicationUser { get; set; } 17 | } 18 | -------------------------------------------------------------------------------- /src/ServerApp/Core/CleanHr.Domain/Aggregates/IdentityAggregate/UserOldPassword.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace CleanHr.Domain.Aggregates.IdentityAggregate; 4 | 5 | public class UserOldPassword 6 | { 7 | public Guid Id { get; set; } 8 | 9 | public Guid UserId { get; set; } 10 | 11 | public string PasswordHash { get; set; } 12 | 13 | public DateTime SetAtUtc { get; set; } 14 | 15 | public ApplicationUser User { get; set; } 16 | } 17 | -------------------------------------------------------------------------------- /src/ServerApp/Core/CleanHr.Domain/CleanHr.Domain.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | true 6 | AllEnabledByDefault 7 | true 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | all 16 | runtime; build; native; contentfiles; analyzers; buildtransitive 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/ServerApp/Core/CleanHr.Domain/Exceptions/DomainValidationException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.Serialization; 3 | 4 | namespace CleanHr.Domain.Exceptions; 5 | 6 | [Serializable] 7 | public sealed class DomainValidationException : Exception 8 | { 9 | public DomainValidationException(string message) 10 | : base(message) 11 | { 12 | } 13 | 14 | public DomainValidationException(string message, Exception innerException) 15 | : base(message, innerException) 16 | { 17 | } 18 | 19 | private DomainValidationException() 20 | { 21 | } 22 | 23 | private DomainValidationException(SerializationInfo serializationInfo, StreamingContext streamingContext) 24 | { 25 | throw new NotImplementedException(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/ServerApp/Core/CleanHr.Domain/Exceptions/EntityNotFoundException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.Serialization; 3 | 4 | namespace CleanHr.Domain.Exceptions; 5 | 6 | [Serializable] 7 | public sealed class EntityNotFoundException : Exception 8 | { 9 | public EntityNotFoundException(string message) 10 | : base(message) 11 | { 12 | } 13 | 14 | public EntityNotFoundException(string message, Exception inner) 15 | : base(message, inner) 16 | { 17 | } 18 | 19 | public EntityNotFoundException(Type entityType) 20 | : base(GetExceptionMessage(entityType, null)) 21 | { 22 | } 23 | 24 | public EntityNotFoundException(Type entityType, object key) 25 | : base(GetExceptionMessage(entityType, key)) 26 | { 27 | } 28 | 29 | private EntityNotFoundException() 30 | : base() 31 | { 32 | } 33 | 34 | private EntityNotFoundException(SerializationInfo serializationInfo, StreamingContext streamingContext) 35 | { 36 | throw new NotImplementedException(); 37 | } 38 | 39 | private static string GetExceptionMessage(Type entityType, object key) 40 | { 41 | if (key == null) 42 | { 43 | return $"Entity \'{entityType.Name}\' was not found."; 44 | } 45 | 46 | return $"Entity \'{entityType.Name}\' with the id/key value \'{key}\' was not found."; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/ServerApp/Core/CleanHr.Domain/Primitives/AggregateRoot.cs: -------------------------------------------------------------------------------- 1 | namespace CleanHr.Domain.Primitives; 2 | 3 | public abstract class AggregateRoot : Entity 4 | { 5 | } 6 | -------------------------------------------------------------------------------- /src/ServerApp/Core/CleanHr.Domain/Primitives/Entity.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace CleanHr.Domain.Primitives; 4 | 5 | public abstract class Entity : IEquatable 6 | { 7 | public Guid Id { get; protected init; } 8 | 9 | public static bool operator ==(Entity first, Entity second) 10 | { 11 | if (first is null && second is null) 12 | { 13 | return true; 14 | } 15 | 16 | if (first is null || second is null) 17 | { 18 | return false; 19 | } 20 | 21 | return first.Equals(second); 22 | } 23 | 24 | public static bool operator !=(Entity first, Entity second) 25 | { 26 | return !(first == second); 27 | } 28 | 29 | public bool Equals(Entity other) 30 | { 31 | if (other is null || other.GetType() != GetType()) 32 | { 33 | return false; 34 | } 35 | 36 | return other.Id == Id; 37 | } 38 | 39 | public override bool Equals(object obj) 40 | { 41 | // Check if the two have same type. 42 | if (obj is null || obj.GetType() != GetType()) 43 | { 44 | return false; 45 | } 46 | 47 | // Check If the obj if of type Entity. 48 | if (obj is not Entity entity) 49 | { 50 | return false; 51 | } 52 | 53 | return entity.Id == Id; 54 | } 55 | 56 | public override int GetHashCode() 57 | { 58 | return Id.GetHashCode(); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/ServerApp/Core/CleanHr.Domain/Primitives/ValueObject.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace CleanHr.Domain.Primitives; 6 | 7 | public abstract class ValueObject : IEquatable 8 | { 9 | public static bool operator ==(ValueObject one, ValueObject two) 10 | { 11 | return EqualOperator(one, two); 12 | } 13 | 14 | public static bool operator !=(ValueObject one, ValueObject two) 15 | { 16 | return !EqualOperator(one, two); 17 | } 18 | 19 | public bool Equals(ValueObject other) 20 | { 21 | return other is not null && ValuesAreEqual(other); 22 | } 23 | 24 | public override bool Equals(object obj) 25 | { 26 | if (obj is null || obj.GetType() != GetType()) 27 | { 28 | return false; 29 | } 30 | 31 | return obj is ValueObject other && ValuesAreEqual(other); 32 | } 33 | 34 | public override int GetHashCode() 35 | { 36 | return GetEqualityComponents() 37 | .Select(x => x != null ? x.GetHashCode() : 0) 38 | .Aggregate((x, y) => x ^ y); 39 | } 40 | 41 | protected static bool EqualOperator(ValueObject left, ValueObject right) 42 | { 43 | if (left is null && right is null) 44 | { 45 | return true; 46 | } 47 | 48 | if (left is null || right is null) 49 | { 50 | return false; 51 | } 52 | 53 | return ReferenceEquals(left, right) || left.Equals(right); 54 | } 55 | 56 | protected abstract IEnumerable GetEqualityComponents(); 57 | 58 | private bool ValuesAreEqual(ValueObject other) 59 | { 60 | return other is not null && GetEqualityComponents().SequenceEqual(other.GetEqualityComponents()); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/ServerApp/Core/CleanHr.Domain/ValueObjects/DateOfBirth.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using CleanHr.Domain.Exceptions; 4 | using CleanHr.Domain.Primitives; 5 | 6 | namespace CleanHr.Domain.ValueObjects; 7 | 8 | public sealed class DateOfBirth : ValueObject 9 | { 10 | private readonly DateTime _minDateOfBirth = DateTime.UtcNow.AddYears(-115); 11 | 12 | private readonly DateTime _maxDateOfBirth = DateTime.UtcNow.AddYears(-15); 13 | 14 | public DateOfBirth(DateTime value) 15 | { 16 | SetValue(value); 17 | } 18 | 19 | public DateTime Value { get; private set; } 20 | 21 | protected override IEnumerable GetEqualityComponents() 22 | { 23 | yield return Value; 24 | } 25 | 26 | private void SetValue(DateTime value) 27 | { 28 | if (value < _minDateOfBirth || value > _maxDateOfBirth) 29 | { 30 | throw new DomainValidationException("The minimum age has to be 15 years."); 31 | } 32 | 33 | Value = value; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/ServerApp/Core/CleanHr.Domain/ValueObjects/DepartmentName.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using CleanHr.Domain.Exceptions; 3 | using CleanHr.Domain.Primitives; 4 | 5 | namespace CleanHr.Domain.ValueObjects; 6 | 7 | public sealed class DepartmentName : ValueObject 8 | { 9 | private const int _minLength = 5; 10 | 11 | private const int _maxLength = 50; 12 | 13 | public DepartmentName(string value) 14 | { 15 | SetValue(value); 16 | } 17 | 18 | public string Value { get; private set; } 19 | 20 | protected override IEnumerable GetEqualityComponents() 21 | { 22 | yield return Value; 23 | } 24 | 25 | private void SetValue(string value) 26 | { 27 | if (string.IsNullOrWhiteSpace(value)) 28 | { 29 | throw new DomainValidationException(DepartmentDomainErrors.NameNullOrEmpty); 30 | } 31 | 32 | if (value.Length < _minLength || value.Length > _maxLength) 33 | { 34 | string errorMessage = DepartmentDomainErrors.GetNameLengthOutOfRangeErrorMessage(_minLength, _maxLength); 35 | throw new DomainValidationException(errorMessage); 36 | } 37 | 38 | Value = value; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/ServerApp/Core/CleanHr.Domain/ValueObjects/Email.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Text.RegularExpressions; 3 | using CleanHr.Domain.Exceptions; 4 | using CleanHr.Domain.Primitives; 5 | 6 | namespace CleanHr.Domain.ValueObjects; 7 | 8 | public sealed class Email : ValueObject 9 | { 10 | private const int _maxLength = 50; 11 | 12 | public Email(string value) 13 | { 14 | SetValue(value); 15 | } 16 | 17 | public string Value { get; private set; } 18 | 19 | protected override IEnumerable GetEqualityComponents() 20 | { 21 | yield return Value; 22 | } 23 | 24 | private void SetValue(string value) 25 | { 26 | if (string.IsNullOrWhiteSpace(value)) 27 | { 28 | throw new DomainValidationException("The Email cannot be null or empty."); 29 | } 30 | 31 | if (value.Length > _maxLength) 32 | { 33 | throw new DomainValidationException($"The Email length must be less than {_maxLength + 1} characters."); 34 | } 35 | 36 | Regex emailRegex = new(@"^([\w\.\-]+)@([\w\-]+)((\.(\w){2,3})+)$"); 37 | 38 | Match match = emailRegex.Match(value); 39 | 40 | if (match.Success == false) 41 | { 42 | throw new DomainValidationException("The Email value is not a valid email."); 43 | } 44 | 45 | Value = value; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/ServerApp/Core/CleanHr.Domain/ValueObjects/EmployeeName.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using CleanHr.Domain.Exceptions; 3 | using CleanHr.Domain.Primitives; 4 | 5 | namespace CleanHr.Domain.ValueObjects; 6 | 7 | public sealed class EmployeeName : ValueObject 8 | { 9 | private const int _minLength = 2; 10 | private const int _maxLength = 50; 11 | 12 | public EmployeeName(string firstName, string lastName) 13 | { 14 | SetFirstName(firstName); 15 | SetLastName(lastName); 16 | } 17 | 18 | public string FirstName { get; private set; } 19 | 20 | public string LastName { get; private set; } 21 | 22 | protected override IEnumerable GetEqualityComponents() 23 | { 24 | yield return FirstName; 25 | yield return LastName; 26 | } 27 | 28 | private void SetFirstName(string firstName) 29 | { 30 | if (string.IsNullOrWhiteSpace(firstName)) 31 | { 32 | throw new DomainValidationException("The FirstName cannot be null or empty."); 33 | } 34 | 35 | if (firstName.Length < _minLength || firstName.Length > _maxLength) 36 | { 37 | throw new DomainValidationException($"The FirstName must be in between {_minLength} to {_maxLength} characters."); 38 | } 39 | 40 | FirstName = firstName; 41 | } 42 | 43 | private void SetLastName(string lastName) 44 | { 45 | if (string.IsNullOrWhiteSpace(lastName)) 46 | { 47 | throw new DomainValidationException("The LastName cannot be null or empty."); 48 | } 49 | 50 | if (lastName.Length < _minLength || lastName.Length > _maxLength) 51 | { 52 | throw new DomainValidationException($"The LastName must be in between {_minLength} to {_maxLength} characters."); 53 | } 54 | 55 | LastName = lastName; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/ServerApp/Core/CleanHr.Domain/ValueObjects/PhoneNumber.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using CleanHr.Domain.Exceptions; 3 | using CleanHr.Domain.Primitives; 4 | 5 | namespace CleanHr.Domain.ValueObjects; 6 | 7 | public sealed class PhoneNumber : ValueObject 8 | { 9 | private const int _minLength = 10; 10 | private const int _maxLength = 20; 11 | 12 | public PhoneNumber(string value) 13 | { 14 | SetValue(value); 15 | } 16 | 17 | public string Value { get; private set; } 18 | 19 | protected override IEnumerable GetEqualityComponents() 20 | { 21 | yield return Value; 22 | } 23 | 24 | private void SetValue(string value) 25 | { 26 | if (string.IsNullOrWhiteSpace(value)) 27 | { 28 | throw new DomainValidationException("The PhoneNumber value cannot be null or empty."); 29 | } 30 | 31 | if (value.Length < _minLength || value.Length > _maxLength) 32 | { 33 | throw new DomainValidationException($"The PhoneNumber value must be in between {_minLength} && {_maxLength} characters."); 34 | } 35 | 36 | Value = value; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/ServerApp/Infrastructure/CleanHr.Infrastructure.Services/CleanHr.Infrastructure.Services.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | true 6 | AllEnabledByDefault 7 | true 8 | 9 | 10 | 11 | 12 | 13 | all 14 | runtime; build; native; contentfiles; analyzers; buildtransitive 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/ServerApp/Infrastructure/CleanHr.Infrastructure.Services/Configs/SendGridConfig.cs: -------------------------------------------------------------------------------- 1 | using TanvirArjel.ArgumentChecker; 2 | 3 | namespace CleanHr.Infrastructure.Services.Configs; 4 | 5 | public class SendGridConfig(string apiKey) 6 | { 7 | public string ApiKey { get; set; } = apiKey.ThrowIfNullOrEmpty(nameof(apiKey)); 8 | } 9 | -------------------------------------------------------------------------------- /src/ServerApp/Infrastructure/CleanHr.Infrastructure.Services/EmailSender.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using CleanHr.Application.Infrastructures; 4 | using CleanHr.Infrastructure.Services.Configs; 5 | using SendGrid; 6 | using SendGrid.Helpers.Mail; 7 | 8 | namespace CleanHr.Infrastructure.Services; 9 | 10 | public sealed class EmailSender(SendGridConfig sendGridConfig, IExceptionLogger exceptionLogger) : IEmailSender 11 | { 12 | private readonly SendGridConfig _sendGridConfig = sendGridConfig ?? throw new ArgumentNullException(nameof(sendGridConfig)); 13 | private readonly IExceptionLogger _exceptionLogger = exceptionLogger ?? throw new ArgumentNullException(nameof(exceptionLogger)); 14 | 15 | private SendGridClient SendGridClient => new(_sendGridConfig.ApiKey); 16 | 17 | public async Task SendAsync(EmailMessage emailMessage) 18 | { 19 | try 20 | { 21 | ArgumentNullException.ThrowIfNull(emailMessage); 22 | 23 | SendGridMessage message = new() 24 | { 25 | Subject = emailMessage.Subject, 26 | HtmlContent = emailMessage.MailBody, 27 | }; 28 | 29 | message.AddTo(new EmailAddress(emailMessage.ReceiverEmail, emailMessage.ReceiverName)); 30 | 31 | if (!string.IsNullOrWhiteSpace(emailMessage.SenderEmail)) 32 | { 33 | message.From = new EmailAddress(emailMessage.SenderEmail, emailMessage.SenderName); 34 | message.ReplyTo = new EmailAddress(emailMessage.SenderEmail, emailMessage.SenderName); 35 | } 36 | 37 | Response response = await SendGridClient.SendEmailAsync(message); 38 | } 39 | catch (Exception exception) 40 | { 41 | await _exceptionLogger.LogAsync(exception, emailMessage); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/ServerApp/Infrastructure/CleanHr.Infrastructure.Services/ExceptionLogger.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text.Json; 3 | using System.Threading.Tasks; 4 | using CleanHr.Application.Infrastructures; 5 | using Microsoft.Extensions.Logging; 6 | 7 | namespace CleanHr.Infrastructure.Services; 8 | 9 | internal sealed class ExceptionLogger(ILogger logger) : IExceptionLogger 10 | { 11 | private readonly ILogger _logger = logger ?? throw new ArgumentNullException(nameof(logger)); 12 | 13 | public async Task LogAsync(Exception exception) 14 | { 15 | await LogAsync(exception, null); 16 | } 17 | 18 | public async Task LogAsync(Exception exception, object parameters) 19 | { 20 | try 21 | { 22 | ArgumentNullException.ThrowIfNull(exception); 23 | 24 | string jsonParameters = parameters != null ? JsonSerializer.Serialize(parameters) : "No parameter."; 25 | _logger.LogCritical(exception, "Parameters: {P1}", jsonParameters); 26 | 27 | await Task.CompletedTask; 28 | } 29 | catch (Exception loggerException) 30 | { 31 | _logger.LogCritical(loggerException, "Exception thrown in exception logger."); 32 | } 33 | } 34 | 35 | public async Task LogAsync( 36 | Exception exception, 37 | string requestPath, 38 | string requestBody) 39 | { 40 | try 41 | { 42 | ArgumentNullException.ThrowIfNull(exception); 43 | 44 | _logger.LogCritical(exception, "RequestedPath: {P1} and RequestBody: {P2}", requestPath, requestBody); 45 | 46 | await Task.CompletedTask; 47 | } 48 | catch (Exception loggerException) 49 | { 50 | _logger.LogCritical(loggerException, "Exception thrown in exception logger."); 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/ServerApp/Infrastructure/CleanHr.Infrastructure.Services/ServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using CleanHr.Infrastructure.Services.Configs; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using TanvirArjel.ArgumentChecker; 4 | 5 | namespace CleanHr.Infrastructure.Services; 6 | 7 | public static class ServiceCollectionExtensions 8 | { 9 | public static void AddSendGrid(this IServiceCollection services, string apiKey) 10 | { 11 | services.ThrowIfNull(nameof(services)); 12 | apiKey.ThrowIfNull(nameof(apiKey)); 13 | 14 | services.AddSingleton(fact => 15 | { 16 | SendGridConfig sendGridConfig = new(apiKey); 17 | return sendGridConfig; 18 | }); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/ServerApp/Infrastructure/CleanHr.Persistence.Cache/CleanHr.Persistence.Cache.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | true 6 | AllEnabledByDefault 7 | true 8 | 9 | 10 | 11 | 12 | all 13 | runtime; build; native; contentfiles; analyzers; buildtransitive 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/ServerApp/Infrastructure/CleanHr.Persistence.Cache/Handlers/DepartmentCacheHandler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using CleanHr.Application.Caching.Handlers; 4 | using CleanHr.Persistence.Cache.Keys; 5 | using Microsoft.Extensions.Caching.Distributed; 6 | 7 | namespace CleanHr.Persistence.Cache.Handlers; 8 | 9 | internal sealed class DepartmentCacheHandler(IDistributedCache distributedCache) : IDepartmentCacheHandler 10 | { 11 | private readonly IDistributedCache _distributedCache = distributedCache ?? throw new ArgumentNullException(nameof(distributedCache)); 12 | 13 | public async Task RemoveListAsync() 14 | { 15 | string departmentListKey = DepartmentCacheKeys.ListKey; 16 | await _distributedCache.RemoveAsync(departmentListKey); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/ServerApp/Infrastructure/CleanHr.Persistence.Cache/Handlers/EmployeeCacheHandler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using CleanHr.Application.Caching.Handlers; 4 | using CleanHr.Persistence.Cache.Keys; 5 | using Microsoft.Extensions.Caching.Distributed; 6 | 7 | namespace CleanHr.Persistence.Cache.Handlers; 8 | 9 | internal sealed class EmployeeCacheHandler(IDistributedCache distributedCache) : IEmployeeCacheHandler 10 | { 11 | private readonly IDistributedCache _distributedCache = distributedCache ?? throw new ArgumentNullException(nameof(distributedCache)); 12 | 13 | public async Task RemoveDetailsByIdAsync(Guid employeeId) 14 | { 15 | string detailsKey = EmployeeCacheKeys.GetDetailsKey(employeeId); 16 | await _distributedCache.RemoveAsync(detailsKey); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/ServerApp/Infrastructure/CleanHr.Persistence.Cache/Keys/DepartmentCacheKeys.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace CleanHr.Persistence.Cache.Keys; 4 | 5 | internal static class DepartmentCacheKeys 6 | { 7 | public static string ListKey => "DepartmentList"; 8 | 9 | public static string SelectListKey => "DepartmentSelectList"; 10 | 11 | public static string GetKey(Guid departmentId) 12 | { 13 | return $"Department-{departmentId}"; 14 | } 15 | 16 | public static string GetDetailsKey(Guid departmentId) 17 | { 18 | return $"DepartmentDetails-{departmentId}"; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/ServerApp/Infrastructure/CleanHr.Persistence.Cache/Keys/EmployeeCacheKeys.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace CleanHr.Persistence.Cache.Keys; 4 | 5 | public static class EmployeeCacheKeys 6 | { 7 | public static string GetKey(Guid employeeId) 8 | { 9 | return $"Employee-{employeeId}"; 10 | } 11 | 12 | public static string GetDetailsKey(Guid employeeId) 13 | { 14 | return $"EmployeeDetails-{employeeId}"; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/ServerApp/Infrastructure/CleanHr.Persistence.Cache/Repositories/EmployeeCacheRepository.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq.Expressions; 3 | using System.Threading.Tasks; 4 | using CleanHr.Application.Caching.Repositories; 5 | using CleanHr.Application.Queries.EmployeeQueries; 6 | using CleanHr.Domain.Aggregates.EmployeeAggregate; 7 | using CleanHr.Persistence.Cache.Keys; 8 | using Microsoft.Extensions.Caching.Distributed; 9 | using TanvirArjel.EFCore.GenericRepository; 10 | using TanvirArjel.Extensions.Microsoft.Caching; 11 | 12 | namespace CleanHr.Persistence.Cache.Repositories; 13 | 14 | internal sealed class EmployeeCacheRepository(IDistributedCache distributedCache, IQueryRepository repository) : IEmployeeCacheRepository 15 | { 16 | public async Task GetDetailsByIdAsync(Guid employeeId) 17 | { 18 | string cacheKey = EmployeeCacheKeys.GetDetailsKey(employeeId); 19 | EmployeeDetailsDto employeeDetails = await distributedCache.GetAsync(cacheKey); 20 | 21 | if (employeeDetails == null) 22 | { 23 | Expression> selectExp = e => new EmployeeDetailsDto 24 | { 25 | Id = e.Id, 26 | Name = e.Name.FirstName + " " + e.Name.LastName, 27 | DepartmentId = e.DepartmentId, 28 | DepartmentName = e.Department.Name.Value, 29 | DateOfBirth = e.DateOfBirth.Value, 30 | Email = e.Email.Value, 31 | PhoneNumber = e.PhoneNumber.Value, 32 | IsActive = e.IsActive, 33 | CreatedAtUtc = e.CreatedAtUtc, 34 | LastModifiedAtUtc = e.LastModifiedAtUtc 35 | }; 36 | 37 | employeeDetails = await repository.GetByIdAsync(employeeId, selectExp); 38 | 39 | await distributedCache.SetAsync(cacheKey, employeeDetails); 40 | } 41 | 42 | return employeeDetails; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/ServerApp/Infrastructure/CleanHr.Persistence.Cache/ServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using CleanHr.Application.Caching.Handlers; 2 | using CleanHr.Persistence.Cache.Handlers; 3 | using Microsoft.Extensions.DependencyInjection; 4 | using TanvirArjel.ArgumentChecker; 5 | 6 | namespace CleanHr.Persistence.Cache; 7 | 8 | public static class ServiceCollectionExtensions 9 | { 10 | public static void AddCaching(this IServiceCollection services) 11 | { 12 | services.ThrowIfNull(nameof(services)); 13 | 14 | services.AddDistributedMemoryCache(); 15 | 16 | services.AddScoped(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/ServerApp/Infrastructure/CleanHr.Persistence.RelationalDB/CleanHr.Persistence.RelationalDB.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | true 6 | AllEnabledByDefault 7 | true 8 | 9 | 10 | 11 | 12 | 13 | 14 | all 15 | runtime; build; native; contentfiles; analyzers; buildtransitive 16 | 17 | 18 | 19 | all 20 | runtime; build; native; contentfiles; analyzers; buildtransitive 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/ServerApp/Infrastructure/CleanHr.Persistence.RelationalDB/CleanHrDbContext.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using CleanHr.Domain.Aggregates.IdentityAggregate; 5 | using CleanHr.Persistence.RelationalDB.EntityConfigurations.EmployeeAggregate; 6 | using Microsoft.AspNetCore.Identity.EntityFrameworkCore; 7 | using Microsoft.EntityFrameworkCore; 8 | 9 | namespace CleanHr.Persistence.RelationalDB; 10 | 11 | internal sealed class CleanHrDbContext(DbContextOptions options) : IdentityDbContext(options) 12 | { 13 | public override Task SaveChangesAsync(CancellationToken cancellationToken = default) 14 | { 15 | ////ChangeTracker.ApplyValueGenerationOnUpdate(); 16 | return base.SaveChangesAsync(cancellationToken); 17 | } 18 | 19 | public override int SaveChanges() 20 | { 21 | ////ChangeTracker.ApplyValueGenerationOnUpdate(); 22 | return base.SaveChanges(); 23 | } 24 | 25 | protected override void OnModelCreating(ModelBuilder modelBuilder) 26 | { 27 | ArgumentNullException.ThrowIfNull(modelBuilder); 28 | 29 | base.OnModelCreating(modelBuilder); 30 | modelBuilder.ApplyConfigurationsFromAssembly(typeof(EmployeeConfiguration).Assembly); 31 | ////modelBuilder.ApplyBaseEntityConfiguration(); // This should be called after calling the derived entity configurations 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/ServerApp/Infrastructure/CleanHr.Persistence.RelationalDB/EntityConfigurations/BaseEntityConfiguration.cs: -------------------------------------------------------------------------------- 1 | ////using System; 2 | ////using EmployeeManagement.Domain.Entities; 3 | ////using Microsoft.EntityFrameworkCore; 4 | ////using Microsoft.EntityFrameworkCore.Metadata; 5 | 6 | ////namespace EmployeeManagement.Persistence.RelationalDB.EntityConfigurations.DomainEntities; 7 | 8 | ////public static class BaseEntityConfiguration 9 | ////{ 10 | //// public static void Configure(ModelBuilder modelBuilder) 11 | //// where TEntity : Entity 12 | //// { 13 | //// if (modelBuilder == null) 14 | //// { 15 | //// throw new ArgumentNullException(nameof(modelBuilder)); 16 | //// } 17 | 18 | //// modelBuilder.Entity(builder => 19 | //// { 20 | //// builder.Property("IdentityKey").ValueGeneratedOnAdd(); 21 | 22 | //// builder.HasKey(cr => cr.Id); 23 | //// builder.Property(cr => cr.CreatedAtUtc).HasDefaultValueSql("getutcdate()"); 24 | //// ////builder.Property(cr => cr.CreatedAtUtc).Metadata.SetBeforeSaveBehavior(PropertySaveBehavior.Throw); 25 | //// builder.Property(cr => cr.CreatedAtUtc).Metadata.SetAfterSaveBehavior(PropertySaveBehavior.Throw); 26 | 27 | //// builder.Property(cr => cr.LastModifiedAtUtc).Metadata.SetBeforeSaveBehavior(PropertySaveBehavior.Throw); 28 | //// }); 29 | //// } 30 | ////} 31 | -------------------------------------------------------------------------------- /src/ServerApp/Infrastructure/CleanHr.Persistence.RelationalDB/EntityConfigurations/DepartmentAggregate/DepartmentConfiguration.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using CleanHr.Domain.Aggregates.DepartmentAggregate; 3 | using Microsoft.EntityFrameworkCore; 4 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 5 | 6 | namespace CleanHr.Persistence.RelationalDB.EntityConfigurations.DepartmentAggregate; 7 | 8 | public class DepartmentConfiguration : IEntityTypeConfiguration 9 | { 10 | public void Configure(EntityTypeBuilder builder) 11 | { 12 | ArgumentNullException.ThrowIfNull(builder); 13 | 14 | builder.ToTable("Departments"); 15 | builder.HasKey(d => d.Id); 16 | 17 | builder.OwnsOne(d => d.Name, navBuilder => 18 | { 19 | navBuilder.Property(n => n.Value).HasMaxLength(50).HasColumnName("Name").IsRequired(); 20 | navBuilder.HasIndex(n => n.Value).IsUnique(); 21 | }); 22 | 23 | builder.Navigation(d => d.Name).IsRequired(); 24 | 25 | builder.Property(d => d.Description).HasMaxLength(200).IsRequired(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/ServerApp/Infrastructure/CleanHr.Persistence.RelationalDB/EntityConfigurations/EmployeeAggregate/EmployeeConfiguration.cs: -------------------------------------------------------------------------------- 1 | using CleanHr.Domain.Aggregates.EmployeeAggregate; 2 | using Microsoft.EntityFrameworkCore; 3 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 4 | 5 | namespace CleanHr.Persistence.RelationalDB.EntityConfigurations.EmployeeAggregate; 6 | 7 | public class EmployeeConfiguration : IEntityTypeConfiguration 8 | { 9 | public void Configure(EntityTypeBuilder builder) 10 | { 11 | builder.ToTable("Employees"); 12 | builder.HasKey(emp => emp.Id); 13 | 14 | builder.OwnsOne(emp => emp.Name).Property(n => n.FirstName) 15 | .HasColumnName("FirstName").HasMaxLength(50).IsRequired(); 16 | 17 | builder.OwnsOne(emp => emp.Name).Property(n => n.LastName) 18 | .HasColumnName("LastName").HasMaxLength(50).IsRequired(); 19 | 20 | builder.Navigation(emp => emp.Name).IsRequired(); 21 | 22 | builder.HasOne(emp => emp.Department).WithMany().HasForeignKey(emp => emp.DepartmentId).IsRequired(); 23 | 24 | builder.OwnsOne(emp => emp.DateOfBirth) 25 | .Property(d => d.Value).HasColumnName("DateOfBirth").HasColumnType("date"); 26 | builder.Navigation(emp => emp.DateOfBirth).IsRequired(); 27 | 28 | builder.OwnsOne(emp => emp.Email) 29 | .Property(e => e.Value).HasColumnName("Email").HasMaxLength(50).IsRequired(); 30 | builder.Navigation(emp => emp.Email).IsRequired(); 31 | 32 | builder.OwnsOne(emp => emp.PhoneNumber) 33 | .Property(p => p.Value).HasColumnName("PhoneNumber").HasMaxLength(15).IsRequired(); 34 | builder.Navigation(emp => emp.PhoneNumber).IsRequired(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/ServerApp/Infrastructure/CleanHr.Persistence.RelationalDB/EntityConfigurations/IdentityAggregate/ApplicationRoleConfiguration.cs: -------------------------------------------------------------------------------- 1 | using CleanHr.Domain.Aggregates.IdentityAggregate; 2 | using Microsoft.EntityFrameworkCore; 3 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 4 | 5 | namespace CleanHr.Persistence.RelationalDB.EntityConfigurations.IdentityAggregate; 6 | 7 | internal sealed class ApplicationRoleConfiguration : IEntityTypeConfiguration 8 | { 9 | public void Configure(EntityTypeBuilder builder) 10 | { 11 | ////builder.Property("IdentityKey").ValueGeneratedOnAdd(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/ServerApp/Infrastructure/CleanHr.Persistence.RelationalDB/EntityConfigurations/IdentityAggregate/ApplicationUserConfiguration.cs: -------------------------------------------------------------------------------- 1 | using CleanHr.Domain.Aggregates.IdentityAggregate; 2 | using Microsoft.EntityFrameworkCore; 3 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 4 | 5 | namespace CleanHr.Persistence.RelationalDB.EntityConfigurations.IdentityAggregate; 6 | 7 | internal sealed class ApplicationUserConfiguration : IEntityTypeConfiguration 8 | { 9 | public void Configure(EntityTypeBuilder builder) 10 | { 11 | ////builder.Property("IdentityKey").ValueGeneratedOnAdd(); 12 | 13 | builder.Property(au => au.FullName).HasMaxLength(100).IsRequired(false); 14 | builder.Property(au => au.UserName).HasMaxLength(50).IsRequired(); 15 | builder.Property(au => au.NormalizedUserName).HasMaxLength(50).IsRequired(); 16 | builder.Property(au => au.Email).HasMaxLength(50).IsRequired(); 17 | builder.Property(au => au.NormalizedEmail).HasMaxLength(50).IsRequired(); 18 | builder.Property(au => au.PhoneNumber).HasMaxLength(15).IsRequired(false); 19 | builder.Property(au => au.DialCode).HasMaxLength(4).IsRequired(false); 20 | builder.Property(au => au.LanguageCulture).HasMaxLength(4).IsRequired(false); 21 | 22 | builder.HasIndex(au => au.Email).IsUnique(true); 23 | builder.HasIndex(au => au.NormalizedEmail).IsUnique(true); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/ServerApp/Infrastructure/CleanHr.Persistence.RelationalDB/EntityConfigurations/IdentityAggregate/EmailVerificationCodeConfiguration.cs: -------------------------------------------------------------------------------- 1 | using CleanHr.Domain.Aggregates.IdentityAggregate; 2 | using Microsoft.EntityFrameworkCore; 3 | using Microsoft.EntityFrameworkCore.Metadata; 4 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 5 | 6 | namespace CleanHr.Persistence.RelationalDB.EntityConfigurations.IdentityAggregate; 7 | 8 | public class EmailVerificationCodeConfiguration : IEntityTypeConfiguration 9 | { 10 | public void Configure(EntityTypeBuilder builder) 11 | { 12 | builder.ToTable("EmailVerificationCodes"); 13 | builder.HasKey(evc => evc.Id); 14 | builder.Property(evc => evc.Id).ValueGeneratedOnAdd(); 15 | 16 | builder.Property(evc => evc.Email).HasMaxLength(50).IsRequired(); 17 | builder.Property(evc => evc.Code).HasMaxLength(6).IsFixedLength().IsRequired(); 18 | 19 | builder.Property(eu => eu.SentAtUtc).Metadata.SetAfterSaveBehavior(PropertySaveBehavior.Throw); 20 | builder.Property(eu => eu.UsedAtUtc).Metadata.SetBeforeSaveBehavior(PropertySaveBehavior.Throw); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/ServerApp/Infrastructure/CleanHr.Persistence.RelationalDB/EntityConfigurations/IdentityAggregate/PasswordResetCodeConfiguration.cs: -------------------------------------------------------------------------------- 1 | using CleanHr.Domain.Aggregates.IdentityAggregate; 2 | using Microsoft.EntityFrameworkCore; 3 | using Microsoft.EntityFrameworkCore.Metadata; 4 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 5 | 6 | namespace CleanHr.Persistence.RelationalDB.EntityConfigurations.IdentityAggregate; 7 | 8 | public class PasswordResetCodeConfiguration : IEntityTypeConfiguration 9 | { 10 | public void Configure(EntityTypeBuilder builder) 11 | { 12 | builder.ToTable("PasswordResetCodes"); 13 | builder.HasKey(evc => evc.Id); 14 | builder.Property(evc => evc.Id).ValueGeneratedOnAdd(); 15 | 16 | builder.Property(evc => evc.Email).HasMaxLength(50).IsRequired(); 17 | builder.Property(evc => evc.Code).HasMaxLength(6).IsFixedLength().IsRequired(); 18 | 19 | builder.Property(eu => eu.SentAtUtc).Metadata.SetAfterSaveBehavior(PropertySaveBehavior.Throw); 20 | builder.Property(eu => eu.UsedAtUtc).Metadata.SetBeforeSaveBehavior(PropertySaveBehavior.Throw); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/ServerApp/Infrastructure/CleanHr.Persistence.RelationalDB/EntityConfigurations/IdentityAggregate/RefreshTokenConfiguration.cs: -------------------------------------------------------------------------------- 1 | using CleanHr.Domain.Aggregates.IdentityAggregate; 2 | using Microsoft.EntityFrameworkCore; 3 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 4 | 5 | namespace CleanHr.Persistence.RelationalDB.EntityConfigurations.IdentityAggregate; 6 | 7 | public class RefreshTokenConfiguration : IEntityTypeConfiguration 8 | { 9 | public void Configure(EntityTypeBuilder builder) 10 | { 11 | builder.ToTable("RefreshTokens"); 12 | builder.HasKey(rt => rt.UserId); 13 | 14 | builder.HasOne(rt => rt.ApplicationUser).WithOne(au => au.RefreshToken).HasForeignKey(rt => rt.UserId); 15 | builder.Property(rt => rt.Token).HasMaxLength(100); 16 | builder.Property(rt => rt.CreatedAtUtc).HasDefaultValueSql("getutcdate()"); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/ServerApp/Infrastructure/CleanHr.Persistence.RelationalDB/EntityConfigurations/IdentityAggregate/UserOldPasswordConfiguration.cs: -------------------------------------------------------------------------------- 1 | using CleanHr.Domain.Aggregates.IdentityAggregate; 2 | using Microsoft.EntityFrameworkCore; 3 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 4 | 5 | namespace CleanHr.Persistence.RelationalDB.EntityConfigurations.IdentityAggregate; 6 | 7 | internal sealed class UserOldPasswordConfiguration : IEntityTypeConfiguration 8 | { 9 | public void Configure(EntityTypeBuilder builder) 10 | { 11 | builder.ToTable("UserOldPasswords"); 12 | builder.HasKey(uop => uop.Id); 13 | builder.Property(uop => uop.Id).ValueGeneratedOnAdd(); 14 | 15 | builder.HasOne(uop => uop.User).WithMany().HasForeignKey(uop => uop.UserId); 16 | builder.Property(uop => uop.PasswordHash).HasMaxLength(500).IsRequired(); 17 | builder.Property(uop => uop.SetAtUtc).IsRequired(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/ServerApp/Infrastructure/CleanHr.Persistence.RelationalDB/Extensions/ApplicationBuilderExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Builder; 2 | using Microsoft.EntityFrameworkCore; 3 | using Microsoft.Extensions.DependencyInjection; 4 | using TanvirArjel.ArgumentChecker; 5 | 6 | namespace CleanHr.Persistence.RelationalDB.Extensions; 7 | 8 | public static class ApplicationBuilderExtensions 9 | { 10 | public static void ApplyDatabaseMigrations(this WebApplication app) 11 | { 12 | app.ThrowIfNull(nameof(app)); 13 | 14 | using IServiceScope serviceScope = app.Services.CreateScope(); 15 | CleanHrDbContext dbContext = serviceScope.ServiceProvider.GetRequiredService(); 16 | 17 | // TODO: Comment out this if you have SQL server installed on your machine. 18 | dbContext.Database.Migrate(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/ServerApp/Infrastructure/CleanHr.Persistence.RelationalDB/Extensions/QueryableExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.EntityFrameworkCore; 6 | using TanvirArjel.EFCore.GenericRepository; 7 | 8 | namespace CleanHr.Persistence.RelationalDB.Extensions; 9 | 10 | internal static class QueryableExtensions 11 | { 12 | public static async Task> ToPaginatedListAsync(this IQueryable source, int pageIndex, int pageSize) 13 | where T : class 14 | { 15 | ArgumentNullException.ThrowIfNull(source); 16 | 17 | long count = await source.LongCountAsync(); 18 | pageIndex = pageIndex <= 0 ? 1 : pageIndex; 19 | List items = await source.Skip((pageIndex - 1) * pageSize).Take(pageSize).ToListAsync(); 20 | return new PaginatedList(items, count, pageIndex, pageSize); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/ServerApp/Infrastructure/CleanHr.Persistence.RelationalDB/GlobalSuppressions.cs: -------------------------------------------------------------------------------- 1 | // This file is used by Code Analysis to maintain SuppressMessage 2 | // attributes that are applied to this project. 3 | // Project-level suppressions either have no target or are given 4 | // a specific target and scoped to a namespace, type, member, etc. 5 | 6 | using System.Diagnostics.CodeAnalysis; 7 | 8 | [assembly: SuppressMessage("Design", "CA1062:Validate arguments of public methods", Justification = "Not applicable here", Scope = "namespaceanddescendants", Target = "~N:CleanHr.Persistence.RelationalDB.Migrations")] 9 | [assembly: SuppressMessage("Design", "CA1062:Validate arguments of public methods", Justification = "Not applicable here", Scope = "namespaceanddescendants", Target = "~N:CleanHr.Persistence.RelationalDB.EntityConfigurations")] 10 | -------------------------------------------------------------------------------- /src/ServerApp/Presentation/CleanHr.Api/.config/dotnet-tools.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "isRoot": true, 4 | "tools": { 5 | "dotnet-ef": { 6 | "version": "5.0.7", 7 | "commands": [ 8 | "dotnet-ef" 9 | ] 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /src/ServerApp/Presentation/CleanHr.Api/Configs/JwtConfig.cs: -------------------------------------------------------------------------------- 1 | using TanvirArjel.ArgumentChecker; 2 | 3 | namespace CleanHr.Api.Configs; 4 | 5 | public class JwtConfig(string issuer, string key, int tokenLifeTime) 6 | { 7 | public string Issuer { get; private set; } = issuer.ThrowIfNullOrEmpty(nameof(issuer)); 8 | 9 | public string Key { get; private set; } = key.ThrowIfNullOrEmpty(nameof(key)); 10 | 11 | public int TokenLifeTime { get; private set; } = tokenLifeTime; 12 | } 13 | -------------------------------------------------------------------------------- /src/ServerApp/Presentation/CleanHr.Api/Constants/HealthCheckTags.cs: -------------------------------------------------------------------------------- 1 | namespace CleanHr.Api.Constants; 2 | 3 | public static class HealthCheckTags 4 | { 5 | public static string Database => "database"; 6 | 7 | public static string Ready => "ready"; 8 | } 9 | -------------------------------------------------------------------------------- /src/ServerApp/Presentation/CleanHr.Api/Dockerfile: -------------------------------------------------------------------------------- 1 | #See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging. 2 | 3 | FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base 4 | WORKDIR /app 5 | EXPOSE 80 6 | EXPOSE 443 7 | 8 | FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build 9 | WORKDIR /src 10 | COPY ["src/ServerApp/Presentation/EmployeeManagement.Api/EmployeeManagement.Api.csproj", "src/ServerApp/Presentation/EmployeeManagement.Api/"] 11 | COPY ["src/ServerApp/Infrastructure/EmployeeManagement.Infrastructure.Services/EmployeeManagement.Infrastructure.Services.csproj", "src/ServerApp/Infrastructure/EmployeeManagement.Infrastructure.Services/"] 12 | COPY ["src/ServerApp/Core/EmployeeManagement.Application/EmployeeManagement.Application.csproj", "src/ServerApp/Core/EmployeeManagement.Application/"] 13 | COPY ["src/ServerApp/Core/EmployeeManagement.Domain/EmployeeManagement.Domain.csproj", "src/ServerApp/Core/EmployeeManagement.Domain/"] 14 | COPY ["src/ServerApp/Infrastructure/EmployeeManagement.Persistence.Cache/EmployeeManagement.Persistence.Cache.csproj", "src/ServerApp/Infrastructure/EmployeeManagement.Persistence.Cache/"] 15 | COPY ["src/ServerApp/Infrastructure/EmployeeManagement.Persistence.RelationalDB/EmployeeManagement.Persistence.RelationalDB.csproj", "src/ServerApp/Infrastructure/EmployeeManagement.Persistence.RelationalDB/"] 16 | RUN dotnet restore "src/ServerApp/Presentation/EmployeeManagement.Api/EmployeeManagement.Api.csproj" 17 | COPY . . 18 | WORKDIR "/src/src/ServerApp/Presentation/EmployeeManagement.Api" 19 | RUN dotnet build "EmployeeManagement.Api.csproj" -c Release -o /app/build 20 | 21 | FROM build AS publish 22 | RUN dotnet publish "EmployeeManagement.Api.csproj" -c Release -o /app/publish 23 | 24 | FROM base AS final 25 | WORKDIR /app 26 | COPY --from=publish /app/publish . 27 | ENTRYPOINT ["dotnet", "EmployeeManagement.Api.dll"] -------------------------------------------------------------------------------- /src/ServerApp/Presentation/CleanHr.Api/Extensions/FluentValidationServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using CleanHr.Api.Features.Department.Validators; 2 | using FluentValidation; 3 | using SharpGrip.FluentValidation.AutoValidation.Mvc.Extensions; 4 | 5 | namespace CleanHr.Api; 6 | 7 | public static class FluentValidationServiceCollectionExtensions 8 | { 9 | public static void AddFluentValidation(this IServiceCollection services) 10 | { 11 | ValidatorOptions.Global.DefaultRuleLevelCascadeMode = CascadeMode.Stop; 12 | services.AddValidatorsFromAssemblyContaining(); 13 | 14 | // Make sure this is from SharpGrip.FluentValidation.AutoValidation.Mvc.Extensions; 15 | // Otherwise Async validation would not work. 16 | services.AddFluentValidationAutoValidation(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/ServerApp/Presentation/CleanHr.Api/Extensions/WebApplicationExtensions.cs: -------------------------------------------------------------------------------- 1 | using HealthChecks.UI.Client; 2 | using Microsoft.AspNetCore.Diagnostics.HealthChecks; 3 | 4 | namespace CleanHr.Api; 5 | 6 | public static class WebApplicationExtensions 7 | { 8 | public static void AddHealthCheckEndpoints(this WebApplication app) 9 | { 10 | ArgumentNullException.ThrowIfNull(app, nameof(app)); 11 | 12 | app.MapHealthChecks("/healthz", new HealthCheckOptions() 13 | { 14 | ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse 15 | }); 16 | 17 | app.MapHealthChecks("/healthz/database", new HealthCheckOptions() 18 | { 19 | Predicate = hc => hc.Tags.Contains("database"), 20 | ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse 21 | }); 22 | 23 | app.MapHealthChecks("/healthz/ready", new HealthCheckOptions() 24 | { 25 | Predicate = hc => hc.Tags.Contains("ready"), 26 | ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse 27 | }); 28 | 29 | app.MapHealthChecks("/healthz/live", new HealthCheckOptions 30 | { 31 | Predicate = _ => false 32 | }); 33 | 34 | app.MapHealthChecksUI(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/ServerApp/Presentation/CleanHr.Api/Features/Department/Endpoints/CreateDepartmentEndpoint.cs: -------------------------------------------------------------------------------- 1 | using CleanHr.Api.Features.Department.Models; 2 | using CleanHr.Application.Commands.DepartmentCommands; 3 | using MediatR; 4 | using Microsoft.AspNetCore.Mvc; 5 | using Swashbuckle.AspNetCore.Annotations; 6 | 7 | namespace CleanHr.Api.Features.Department.Endpoints; 8 | 9 | public sealed class CreateDepartmentEndpoint( 10 | IMediator mediator) : DepartmentEndpointBase 11 | { 12 | private readonly IMediator _mediator = mediator ?? throw new ArgumentNullException(nameof(mediator)); 13 | 14 | [HttpPost] 15 | [ProducesResponseType(StatusCodes.Status201Created)] 16 | [ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status400BadRequest)] 17 | [ProducesResponseType(StatusCodes.Status500InternalServerError)] 18 | [ProducesDefaultResponseType] 19 | [SwaggerOperation(Summary = "Create a new department by posting the required data.")] 20 | public async Task Post(CreateDepartmentModel model) 21 | { 22 | CreateDepartmentCommand command = new(model.Name, model.Description); 23 | Guid departmentId = await _mediator.Send(command, HttpContext.RequestAborted); 24 | return Created($"/api/v1/departments/{departmentId}", model); 25 | } 26 | } -------------------------------------------------------------------------------- /src/ServerApp/Presentation/CleanHr.Api/Features/Department/Endpoints/DeleteDepartmentEndpoint.cs: -------------------------------------------------------------------------------- 1 | using CleanHr.Application.Commands.DepartmentCommands; 2 | using MediatR; 3 | using Microsoft.AspNetCore.Mvc; 4 | using Swashbuckle.AspNetCore.Annotations; 5 | 6 | namespace CleanHr.Api.Features.Department.Endpoints; 7 | 8 | public sealed class DeleteDepartmentEndpoint( 9 | IMediator mediator) : DepartmentEndpointBase 10 | { 11 | private readonly IMediator _mediator = mediator ?? throw new ArgumentNullException(nameof(mediator)); 12 | 13 | [HttpDelete("{departmentId}")] 14 | [ProducesResponseType(StatusCodes.Status204NoContent)] 15 | [ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status400BadRequest)] 16 | [ProducesResponseType(StatusCodes.Status404NotFound)] 17 | [ProducesDefaultResponseType] 18 | [SwaggerOperation(Summary = "Delete an existing department by department id.")] 19 | public async Task Delete(Guid departmentId) 20 | { 21 | if (departmentId == Guid.Empty) 22 | { 23 | ModelState.AddModelError(string.Empty, $"The value of {nameof(departmentId)} must be not empty."); 24 | return ValidationProblem(ModelState); 25 | } 26 | 27 | DeleteDepartmentCommand command = new(departmentId); 28 | await _mediator.Send(command, HttpContext.RequestAborted); 29 | 30 | return NoContent(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/ServerApp/Presentation/CleanHr.Api/Features/Department/Endpoints/DepartmentEndpointBase.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authorization; 2 | using Microsoft.AspNetCore.Mvc; 3 | 4 | namespace CleanHr.Api.Features.Department.Endpoints; 5 | 6 | [ApiVersion("1.0")] 7 | [Route("api/v{version:apiVersion}/departments")] 8 | [ApiController] 9 | ////[Authorize] 10 | [ApiExplorerSettings(GroupName = "Department Endpoints")] 11 | public abstract class DepartmentEndpointBase : ControllerBase 12 | { 13 | } 14 | -------------------------------------------------------------------------------- /src/ServerApp/Presentation/CleanHr.Api/Features/Department/Endpoints/GetDepartmentByIdEndpoint.cs: -------------------------------------------------------------------------------- 1 | using CleanHr.Application.Queries.DepartmentQueries; 2 | using MediatR; 3 | using Microsoft.AspNetCore.Mvc; 4 | using Swashbuckle.AspNetCore.Annotations; 5 | 6 | namespace CleanHr.Api.Features.Department.Endpoints; 7 | 8 | public sealed class GetDepartmentByIdEndpoint(IMediator mediator) : DepartmentEndpointBase 9 | { 10 | private readonly IMediator _mediator = mediator ?? throw new ArgumentNullException(nameof(mediator)); 11 | 12 | [HttpGet("{departmentId}")] 13 | [ProducesResponseType(StatusCodes.Status200OK)] 14 | [ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status400BadRequest)] 15 | [ProducesResponseType(StatusCodes.Status404NotFound)] 16 | [ProducesDefaultResponseType] 17 | [SwaggerOperation(Summary = "Get the details of a department by department id.")] 18 | public async Task> GetDepartment(Guid departmentId) 19 | { 20 | if (departmentId == Guid.Empty) 21 | { 22 | ModelState.AddModelError(nameof(departmentId), $"The value of {nameof(departmentId)} can't be empty."); 23 | return ValidationProblem(ModelState); 24 | } 25 | 26 | GetDepartmentByIdQuery query = new(departmentId); 27 | 28 | DepartmentDetailsDto departmentDetailsDto = await _mediator.Send(query, HttpContext.RequestAborted); 29 | return departmentDetailsDto; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/ServerApp/Presentation/CleanHr.Api/Features/Department/Endpoints/GetDepartmentListEndpoint.cs: -------------------------------------------------------------------------------- 1 | using CleanHr.Application.Queries.DepartmentQueries; 2 | using MediatR; 3 | using Microsoft.AspNetCore.Mvc; 4 | using Swashbuckle.AspNetCore.Annotations; 5 | 6 | namespace CleanHr.Api.Features.Department.Endpoints; 7 | 8 | public sealed class GetDepartmentListEndpoint(IMediator mediator) : DepartmentEndpointBase 9 | { 10 | private readonly IMediator _mediator = mediator ?? throw new ArgumentNullException(nameof(mediator)); 11 | 12 | [HttpGet] 13 | [ProducesResponseType(StatusCodes.Status200OK)] 14 | [ProducesResponseType(StatusCodes.Status500InternalServerError)] 15 | [ProducesDefaultResponseType] 16 | [SwaggerOperation(Summary = "Get the list of all departments.")] 17 | public async Task>> Get() 18 | { 19 | GetDepartmentListQuery departmentListQuery = new(); 20 | List departmentDetailsDtos = await _mediator.Send(departmentListQuery, HttpContext.RequestAborted); 21 | return departmentDetailsDtos; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/ServerApp/Presentation/CleanHr.Api/Features/Department/Endpoints/GetDepartmentSelectListEndpoint.cs: -------------------------------------------------------------------------------- 1 | using CleanHr.Application.Queries.DepartmentQueries; 2 | using MediatR; 3 | using Microsoft.AspNetCore.Mvc; 4 | using Microsoft.AspNetCore.Mvc.Rendering; 5 | using Swashbuckle.AspNetCore.Annotations; 6 | 7 | namespace CleanHr.Api.Features.Department.Endpoints; 8 | 9 | public sealed class GetDepartmentSelectListEndpoint(IMediator mediator) : DepartmentEndpointBase 10 | { 11 | private readonly IMediator _mediator = mediator ?? throw new ArgumentNullException(nameof(mediator)); 12 | 13 | [HttpGet("select-list")] 14 | [ProducesResponseType(StatusCodes.Status200OK)] 15 | [ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status400BadRequest)] 16 | [ProducesResponseType(StatusCodes.Status500InternalServerError)] 17 | [ProducesDefaultResponseType] 18 | [SwaggerOperation(Summary = "Get the department select list.")] 19 | public async Task> Get(Guid? selectedDepartment) 20 | { 21 | if (selectedDepartment == Guid.Empty) 22 | { 23 | ModelState.AddModelError(nameof(selectedDepartment), $"The value of {nameof(selectedDepartment)} can't be empty."); 24 | return ValidationProblem(ModelState); 25 | } 26 | 27 | GetDepartmentListQuery departmentListQuery = new(); 28 | List departmentDtos = await _mediator.Send(departmentListQuery, HttpContext.RequestAborted); 29 | 30 | SelectList selectList = new(departmentDtos, "Id", "Name", selectedDepartment); 31 | return selectList; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/ServerApp/Presentation/CleanHr.Api/Features/Department/Endpoints/UpdateDepartmentEndpoint.cs: -------------------------------------------------------------------------------- 1 | using CleanHr.Api.Features.Department.Models; 2 | using CleanHr.Application.Commands.DepartmentCommands; 3 | using MediatR; 4 | using Microsoft.AspNetCore.Mvc; 5 | using Swashbuckle.AspNetCore.Annotations; 6 | 7 | namespace CleanHr.Api.Features.Department.Endpoints; 8 | 9 | public sealed class UpdateDepartmentEndpoint( 10 | IMediator mediator) : DepartmentEndpointBase 11 | { 12 | private readonly IMediator _mediator = mediator ?? throw new ArgumentNullException(nameof(mediator)); 13 | 14 | [HttpPut("{departmentId}")] 15 | [ProducesResponseType(StatusCodes.Status200OK)] 16 | [ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status400BadRequest)] 17 | [ProducesResponseType(StatusCodes.Status404NotFound)] 18 | [ProducesDefaultResponseType] 19 | [SwaggerOperation(Summary = "Update an existing employee by employee id and posting updated data.")] 20 | public async Task Put(Guid departmentId, UpdateDepartmentModel model) 21 | { 22 | if (departmentId != model.Id) 23 | { 24 | ModelState.AddModelError(nameof(model.Id), "The DepartmentId does not match with route value."); 25 | return ValidationProblem(ModelState); 26 | } 27 | 28 | UpdateDepartmentCommand command = new(departmentId, model.Name, model.Description, true); 29 | 30 | await _mediator.Send(command, HttpContext.RequestAborted); 31 | return Ok(); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/ServerApp/Presentation/CleanHr.Api/Features/Department/Models/CreateDepartmentModel.cs: -------------------------------------------------------------------------------- 1 | namespace CleanHr.Api.Features.Department.Models; 2 | 3 | public sealed class CreateDepartmentModel : DepartmentBaseModel 4 | { 5 | } 6 | -------------------------------------------------------------------------------- /src/ServerApp/Presentation/CleanHr.Api/Features/Department/Models/DepartmentBaseModel.cs: -------------------------------------------------------------------------------- 1 | namespace CleanHr.Api.Features.Department.Models; 2 | 3 | public abstract class DepartmentBaseModel 4 | { 5 | public string Name { get; set; } 6 | 7 | public string Description { get; set; } 8 | } 9 | -------------------------------------------------------------------------------- /src/ServerApp/Presentation/CleanHr.Api/Features/Department/Models/UpdateDepartmentModel.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace CleanHr.Api.Features.Department.Models; 4 | 5 | public sealed class UpdateDepartmentModel : DepartmentBaseModel 6 | { 7 | public Guid Id { get; set; } 8 | } 9 | -------------------------------------------------------------------------------- /src/ServerApp/Presentation/CleanHr.Api/Features/Department/Validators/CreateDepartmentModelValidator.cs: -------------------------------------------------------------------------------- 1 | using CleanHr.Api.Features.Department.Models; 2 | using FluentValidation; 3 | using MediatR; 4 | 5 | namespace CleanHr.Api.Features.Department.Validators; 6 | 7 | public sealed class CreateDepartmentModelValidator : DepartmentBaseModelValidator 8 | { 9 | public CreateDepartmentModelValidator(IMediator mediator) 10 | : base(mediator) 11 | { 12 | RuleFor(d => d.Name) 13 | .ValidName() 14 | .MustAsync(async (model, name, token) => await IsUniqueNameAsync(Guid.Empty, name, token)) 15 | .WithMessage("The DepartmentName is already existent."); 16 | } 17 | } -------------------------------------------------------------------------------- /src/ServerApp/Presentation/CleanHr.Api/Features/Department/Validators/DepartmentBaseModelValidator.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using CleanHr.Api.Features.Department.Models; 3 | using CleanHr.Application.Queries.DepartmentQueries; 4 | using FluentValidation; 5 | using MediatR; 6 | 7 | namespace CleanHr.Api.Features.Department.Validators; 8 | 9 | public abstract class DepartmentBaseModelValidator : AbstractValidator 10 | where T : DepartmentBaseModel 11 | { 12 | private readonly IMediator _mediator; 13 | 14 | protected DepartmentBaseModelValidator(IMediator mediator) 15 | { 16 | _mediator = mediator ?? throw new ArgumentNullException(nameof(mediator)); 17 | 18 | RuleFor(d => d.Description) 19 | .NotEmpty() 20 | .WithMessage("The {PropertyName} is required.") 21 | .MinimumLength(20) 22 | .WithMessage("The {PropertyName} must be at least {MinLength} characters.") 23 | .MaximumLength(200) 24 | .WithMessage("The {PropertyName} can't be more than {MaxLength} characters."); 25 | } 26 | 27 | protected async Task IsUniqueNameAsync(Guid id, string name, CancellationToken cancellationToken) 28 | { 29 | IsDepartmentNameUniqueQuery nameUniqueQuery = new(id, name); 30 | bool isUnique = await _mediator.Send(nameUniqueQuery, cancellationToken); 31 | return isUnique; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/ServerApp/Presentation/CleanHr.Api/Features/Department/Validators/DepartmentRuleBuilderExtensions.cs: -------------------------------------------------------------------------------- 1 | using CleanHr.Api.Features.Department.Models; 2 | using FluentValidation; 3 | 4 | namespace CleanHr.Api; 5 | 6 | public static class DepartmentRuleBuilderExtensions 7 | { 8 | public static IRuleBuilderOptions ValidName( 9 | this IRuleBuilder rule) 10 | where T : DepartmentBaseModel 11 | { 12 | return rule.NotEmpty() 13 | .WithMessage("The {PropertyName} is required.") 14 | .MinimumLength(2) 15 | .WithMessage("The {PropertyName} must be at least {MinLength} characters.") 16 | .MaximumLength(20) 17 | .WithMessage("The {PropertyName} can't be more than {MaxLength} characters."); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/ServerApp/Presentation/CleanHr.Api/Features/Department/Validators/UpdateDepartmentModelValidator.cs: -------------------------------------------------------------------------------- 1 | using CleanHr.Api.Features.Department.Models; 2 | using FluentValidation; 3 | using MediatR; 4 | 5 | namespace CleanHr.Api.Features.Department.Validators; 6 | 7 | public sealed class UpdateDepartmentModelValidator : DepartmentBaseModelValidator 8 | { 9 | public UpdateDepartmentModelValidator(IMediator mediator) 10 | : base(mediator) 11 | { 12 | RuleFor(d => d.Id) 13 | .NotEmpty() 14 | .WithMessage("The Id is required."); 15 | 16 | RuleFor(d => d.Name) 17 | .Cascade(CascadeMode.Stop) 18 | .ValidName() 19 | .MustAsync(async (model, name, token) => await IsUniqueNameAsync(model.Id, name, token)) 20 | .WithMessage("The DepartmentName is already existent."); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/ServerApp/Presentation/CleanHr.Api/Features/Employee/Endpoints/CreateEmployeeEndpoint.cs: -------------------------------------------------------------------------------- 1 | using CleanHr.Api.Features.Employee.Models; 2 | using CleanHr.Application.Commands.EmployeeCommands; 3 | using MediatR; 4 | using Microsoft.AspNetCore.Mvc; 5 | using Swashbuckle.AspNetCore.Annotations; 6 | 7 | namespace CleanHr.Api.Features.Employee.Endpoints; 8 | 9 | public class CreateEmployeeEndpoint(IMediator mediator) : EmployeeEndpointBase 10 | { 11 | private readonly IMediator _mediator = mediator ?? throw new ArgumentNullException(nameof(mediator)); 12 | 13 | // POST: api/employees 14 | [HttpPost] 15 | [ProducesResponseType(StatusCodes.Status201Created)] 16 | [ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status400BadRequest)] 17 | [ProducesResponseType(StatusCodes.Status500InternalServerError)] 18 | [ProducesDefaultResponseType] 19 | [SwaggerOperation(Summary = "Create a new employee by posting the required data.")] 20 | public async Task Post([FromBody] CreateEmployeeModel model) 21 | { 22 | CreateEmployeeCommand createEmployeeCommand = new( 23 | model.Name, 24 | model.Name, 25 | model.DepartmentId, 26 | model.DateOfBirth, 27 | model.Email, 28 | model.PhoneNumber); 29 | 30 | await _mediator.Send(createEmployeeCommand, HttpContext.RequestAborted); 31 | return StatusCode(StatusCodes.Status201Created); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/ServerApp/Presentation/CleanHr.Api/Features/Employee/Endpoints/DeleteEmployeeEndpoint.cs: -------------------------------------------------------------------------------- 1 | using CleanHr.Application.Commands.EmployeeCommands; 2 | using MediatR; 3 | using Microsoft.AspNetCore.Mvc; 4 | using Swashbuckle.AspNetCore.Annotations; 5 | 6 | namespace CleanHr.Api.Features.Employee.Endpoints; 7 | 8 | public class DeleteEmployeeEndpoint(IMediator mediator) : EmployeeEndpointBase 9 | { 10 | private readonly IMediator _mediator = mediator ?? throw new ArgumentNullException(nameof(mediator)); 11 | 12 | // DELETE: api/employees/5 13 | [HttpDelete("{employeeId}")] 14 | [ProducesResponseType(StatusCodes.Status204NoContent)] 15 | [ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status400BadRequest)] 16 | [ProducesResponseType(StatusCodes.Status500InternalServerError)] 17 | [ProducesDefaultResponseType] 18 | [SwaggerOperation(Summary = "Delete an existing employee by employee id.")] 19 | public async Task Delete(Guid employeeId) 20 | { 21 | if (employeeId == Guid.Empty) 22 | { 23 | ModelState.AddModelError(nameof(employeeId), $"The value of {nameof(employeeId)} can't be empty."); 24 | return ValidationProblem(ModelState); 25 | } 26 | 27 | DeleteEmployeeCommand command = new(employeeId); 28 | 29 | await _mediator.Send(command, HttpContext.RequestAborted); 30 | return NoContent(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/ServerApp/Presentation/CleanHr.Api/Features/Employee/Endpoints/EmployeeEndpointBase.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authorization; 2 | using Microsoft.AspNetCore.Mvc; 3 | 4 | namespace CleanHr.Api.Features.Employee.Endpoints; 5 | 6 | [ApiController] 7 | [ApiVersion("1.0")] 8 | [Route("api/v{version:apiVersion}/employees")] 9 | [Authorize] 10 | [ApiExplorerSettings(GroupName = "Employee Endpoints")] 11 | public abstract class EmployeeEndpointBase : ControllerBase 12 | { 13 | } 14 | -------------------------------------------------------------------------------- /src/ServerApp/Presentation/CleanHr.Api/Features/Employee/Endpoints/GetEmployeeDetailsByIdEndpoint.cs: -------------------------------------------------------------------------------- 1 | using CleanHr.Application.Queries.EmployeeQueries; 2 | using MediatR; 3 | using Microsoft.AspNetCore.Mvc; 4 | using Swashbuckle.AspNetCore.Annotations; 5 | 6 | namespace CleanHr.Api.Features.Employee.Endpoints; 7 | 8 | public class GetEmployeeDetailsByIdEndpoint(IMediator mediator) : EmployeeEndpointBase 9 | { 10 | private readonly IMediator _mediator = mediator ?? throw new ArgumentNullException(nameof(mediator)); 11 | 12 | // GET: api/employees/5 13 | [HttpGet("{employeeId}")] 14 | [ProducesResponseType(StatusCodes.Status200OK)] 15 | [ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status400BadRequest)] 16 | [ProducesResponseType(StatusCodes.Status500InternalServerError)] 17 | [ProducesDefaultResponseType] 18 | [SwaggerOperation(Summary = "Get details of an employee by employee id.")] 19 | public async Task> Get(Guid employeeId) 20 | { 21 | if (employeeId == Guid.Empty) 22 | { 23 | ModelState.AddModelError(nameof(employeeId), $"The value of {nameof(employeeId)} can't be empty."); 24 | return ValidationProblem(ModelState); 25 | } 26 | 27 | GetEmployeeByIdQuery getEmployeeByIdQuery = new(employeeId); 28 | 29 | EmployeeDetailsDto employeeDetailsDto = await _mediator.Send(getEmployeeByIdQuery, HttpContext.RequestAborted); 30 | return employeeDetailsDto; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/ServerApp/Presentation/CleanHr.Api/Features/Employee/Endpoints/GetEmployeeListEndpoint.cs: -------------------------------------------------------------------------------- 1 | using CleanHr.Application.Queries.EmployeeQueries; 2 | using MediatR; 3 | using Microsoft.AspNetCore.Mvc; 4 | using Swashbuckle.AspNetCore.Annotations; 5 | using TanvirArjel.EFCore.GenericRepository; 6 | 7 | namespace CleanHr.Api.Features.Employee.Endpoints; 8 | 9 | public class GetEmployeeListEndpoint(IMediator mediator) : EmployeeEndpointBase 10 | { 11 | private readonly IMediator _mediator = mediator ?? throw new ArgumentNullException(nameof(mediator)); 12 | 13 | // GET: api/employees 14 | [HttpGet] 15 | [ProducesResponseType(StatusCodes.Status200OK)] 16 | [ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status400BadRequest)] 17 | [ProducesResponseType(StatusCodes.Status500InternalServerError)] 18 | [ProducesDefaultResponseType] 19 | [SwaggerOperation(Summary = "Get the employee paginated list by page number and page size.")] 20 | public async Task>> Get(int pageNumber, int pageSize) 21 | { 22 | if (pageNumber < 1) 23 | { 24 | ModelState.AddModelError(nameof(pageNumber), $"The {nameof(pageNumber)} must be greater than 0."); 25 | return ValidationProblem(ModelState); 26 | } 27 | 28 | if (pageSize < 1 || pageSize > 50) 29 | { 30 | ModelState.AddModelError(nameof(pageSize), $"The {nameof(pageSize)} must be in between 1 and 50."); 31 | return ValidationProblem(ModelState); 32 | } 33 | 34 | GetEmployeeListQuery getEmployeeListQuery = new(pageNumber, pageSize); 35 | PaginatedList employeeList = await _mediator.Send(getEmployeeListQuery, HttpContext.RequestAborted); 36 | return employeeList; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/ServerApp/Presentation/CleanHr.Api/Features/Employee/Endpoints/UpdateEmployeeEndpoint.cs: -------------------------------------------------------------------------------- 1 | using CleanHr.Api.Features.Employee.Models; 2 | using CleanHr.Application.Commands.EmployeeCommands; 3 | using MediatR; 4 | using Microsoft.AspNetCore.Mvc; 5 | using Swashbuckle.AspNetCore.Annotations; 6 | 7 | namespace CleanHr.Api.Features.Employee.Endpoints; 8 | 9 | public class UpdateEmployeeEndpoint(IMediator mediator) : EmployeeEndpointBase 10 | { 11 | private readonly IMediator _mediator = mediator ?? throw new ArgumentNullException(nameof(mediator)); 12 | 13 | // PUT: api/v1/employees/{Guid} 14 | [HttpPut("{employeeId}")] 15 | [ProducesResponseType(StatusCodes.Status200OK)] 16 | [ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status400BadRequest)] 17 | [ProducesResponseType(StatusCodes.Status500InternalServerError)] 18 | [ProducesDefaultResponseType] 19 | [SwaggerOperation(Summary = "Update an existing employee by employee id and posting the updated data.")] 20 | public async Task Put(Guid employeeId, UpdateEmployeeModel model) 21 | { 22 | if (employeeId == Guid.Empty) 23 | { 24 | ModelState.AddModelError("employeeId", "The employeeId cannot be empty guid."); 25 | return ValidationProblem(ModelState); 26 | } 27 | 28 | UpdateEmployeeCommand command = new( 29 | employeeId, 30 | model.Name, 31 | model.DepartmentId, 32 | model.DateOfBirth, 33 | model.Email, 34 | model.PhoneNumber); 35 | 36 | await _mediator.Send(command, HttpContext.RequestAborted); 37 | return Ok(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/ServerApp/Presentation/CleanHr.Api/Features/Employee/Models/CreateEmployeeModel.cs: -------------------------------------------------------------------------------- 1 | namespace CleanHr.Api.Features.Employee.Models; 2 | 3 | public class CreateEmployeeModel : EmployeeBaseModel 4 | { 5 | } -------------------------------------------------------------------------------- /src/ServerApp/Presentation/CleanHr.Api/Features/Employee/Models/EmployeeBaseModel.cs: -------------------------------------------------------------------------------- 1 | namespace CleanHr.Api; 2 | 3 | public abstract class EmployeeBaseModel 4 | { 5 | public string Name { get; set; } 6 | 7 | public Guid DepartmentId { get; set; } 8 | 9 | ////[MinAge(15, 0, 0, ErrorMessage = "The minimum age has to be 15 years.")] 10 | public DateTime DateOfBirth { get; set; } 11 | 12 | public string Email { get; set; } 13 | 14 | public string PhoneNumber { get; set; } 15 | } 16 | -------------------------------------------------------------------------------- /src/ServerApp/Presentation/CleanHr.Api/Features/Employee/Models/UpdateEmployeeModel.cs: -------------------------------------------------------------------------------- 1 | namespace CleanHr.Api.Features.Employee.Models; 2 | 3 | public class UpdateEmployeeModel : EmployeeBaseModel 4 | { 5 | } 6 | -------------------------------------------------------------------------------- /src/ServerApp/Presentation/CleanHr.Api/Features/Employee/Validators/CreateEmployeeModelValidator.cs: -------------------------------------------------------------------------------- 1 | using CleanHr.Api.Features.Employee.Models; 2 | 3 | namespace CleanHr.Api.Features.Employee.Validators; 4 | 5 | public sealed class CreateEmployeeModelValidator : EmployeeBaseModelValidator 6 | { 7 | } 8 | -------------------------------------------------------------------------------- /src/ServerApp/Presentation/CleanHr.Api/Features/Employee/Validators/EmployeeBaseModelValidator.cs: -------------------------------------------------------------------------------- 1 | using FluentValidation; 2 | 3 | namespace CleanHr.Api.Features.Employee.Validators; 4 | 5 | public abstract class EmployeeBaseModelValidator : AbstractValidator 6 | where T : EmployeeBaseModel 7 | { 8 | protected EmployeeBaseModelValidator() 9 | { 10 | RuleFor(e => e.Name).NotEmpty() 11 | .MinimumLength(5).WithMessage("The Name must be at least 5 characters.") 12 | .MaximumLength(50).WithMessage("The Name can't be more than 50 characters."); 13 | 14 | RuleFor(e => e.DepartmentId).NotEmpty(); 15 | 16 | RuleFor(e => e.DateOfBirth).Must(IsAdult).WithMessage("The minimum age has to be 15 years."); 17 | 18 | RuleFor(e => e.Email).NotEmpty() 19 | .EmailAddress() 20 | .MinimumLength(8).WithMessage("The Email must be at least 5 characters.") 21 | .MaximumLength(50).WithMessage("The Email can't be more than 50 characters."); 22 | 23 | RuleFor(e => e.PhoneNumber).NotEmpty() 24 | .MinimumLength(8).WithMessage("The PhoneNumber must be at least 5 characters.") 25 | .MaximumLength(50).WithMessage("The PhoneNumber can't be more than 50 characters."); 26 | } 27 | 28 | private bool IsAdult(DateTime dateOfBirth) 29 | { 30 | return true; 31 | } 32 | } -------------------------------------------------------------------------------- /src/ServerApp/Presentation/CleanHr.Api/Features/Employee/Validators/UpdateEmployeeModelValidator.cs: -------------------------------------------------------------------------------- 1 | using CleanHr.Api.Features.Employee.Models; 2 | 3 | namespace CleanHr.Api.Features.Employee.Validators; 4 | 5 | public sealed class UpdateEmployeeModelValidator : EmployeeBaseModelValidator 6 | { 7 | } 8 | -------------------------------------------------------------------------------- /src/ServerApp/Presentation/CleanHr.Api/Features/ExternalLogin/Endpoints/ExternalLoginEndpointBase.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | 3 | namespace CleanHr.Api.Features.ExternalLogin.Endpoints; 4 | 5 | [Route("api/v{version:apiVersion}/external-login")] 6 | [ApiExplorerSettings(GroupName = "External Login Endpoints")] 7 | [ApiController] 8 | public abstract class ExternalLoginEndpointBase : ControllerBase 9 | { 10 | } 11 | -------------------------------------------------------------------------------- /src/ServerApp/Presentation/CleanHr.Api/Features/ExternalLogin/Endpoints/ExternalLoginSignInEndpoint.cs: -------------------------------------------------------------------------------- 1 | using CleanHr.Domain.Aggregates.IdentityAggregate; 2 | using Microsoft.AspNetCore.Authentication; 3 | using Microsoft.AspNetCore.Identity; 4 | using Microsoft.AspNetCore.Mvc; 5 | 6 | namespace CleanHr.Api.Features.ExternalLogin.Endpoints; 7 | 8 | [ApiVersion("1.0")] 9 | public class ExternalLoginSignInEndpoint( 10 | SignInManager signInManager) : ExternalLoginEndpointBase 11 | { 12 | [HttpGet("sign-in")] 13 | public IActionResult Get(string provider) 14 | { 15 | // Request a redirect to the external login provider. 16 | string redirectUrl = "api/v1/external-login/sign-in-callback"; 17 | AuthenticationProperties properties = signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl); 18 | return Challenge(properties, provider); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/ServerApp/Presentation/CleanHr.Api/Features/ExternalLogin/Endpoints/ExternalLoginSignUpEndpoint.cs: -------------------------------------------------------------------------------- 1 | using CleanHr.Domain.Aggregates.IdentityAggregate; 2 | using Microsoft.AspNetCore.Authentication; 3 | using Microsoft.AspNetCore.Identity; 4 | using Microsoft.AspNetCore.Mvc; 5 | 6 | namespace CleanHr.Api.Features.ExternalLogin.Endpoints; 7 | 8 | [ApiVersion("1.0")] 9 | public class ExternalLoginSignUpEndpoint( 10 | SignInManager signInManager) : ExternalLoginEndpointBase 11 | { 12 | [HttpGet("sign-up")] 13 | public IActionResult Get(string provider, string returnUrl = null) 14 | { 15 | // Request a redirect to the external login provider. 16 | string redirectUrl = $"api/v1/external-login/sign-up-callback?returnUrl={returnUrl}"; 17 | AuthenticationProperties properties = signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl); 18 | return Challenge(properties, provider); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/ServerApp/Presentation/CleanHr.Api/Features/ExternalLogin/Models/TestModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace CleanHr.Api; 4 | 5 | public class TestModel 6 | { 7 | } -------------------------------------------------------------------------------- /src/ServerApp/Presentation/CleanHr.Api/Features/ExternalLogin/Validators/TestModelValidator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace CleanHr.Api; 4 | 5 | public class TestModelValidator 6 | { 7 | } -------------------------------------------------------------------------------- /src/ServerApp/Presentation/CleanHr.Api/Features/User/Endpoints/ConfirmUserEmailEndpoint.cs: -------------------------------------------------------------------------------- 1 | using CleanHr.Api.Features.User.Models; 2 | using CleanHr.Application.Commands.IdentityCommands.UserCommands; 3 | using MediatR; 4 | using Microsoft.AspNetCore.Authorization; 5 | using Microsoft.AspNetCore.Mvc; 6 | using Swashbuckle.AspNetCore.Annotations; 7 | 8 | namespace CleanHr.Api.Features.User.Endpoints; 9 | 10 | [ApiVersion("1.0")] 11 | public class ConfirmUserEmailEndpoint( 12 | IMediator mediator) : UserEndpointBase 13 | { 14 | [HttpPost("confirm-email")] 15 | [AllowAnonymous] 16 | [ProducesResponseType(StatusCodes.Status200OK)] 17 | [ProducesResponseType(StatusCodes.Status400BadRequest)] 18 | [ProducesResponseType(StatusCodes.Status500InternalServerError)] 19 | [ProducesDefaultResponseType] 20 | [SwaggerOperation(Summary = "Confirm the newly registered user's email by posting the required data.")] 21 | public async Task Post(EmailConfirmationModel model) 22 | { 23 | VerifyUserEmailCommand verifyUserEmailCommand = new(model.Email, model.Code); 24 | await mediator.Send(verifyUserEmailCommand); 25 | return Ok(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/ServerApp/Presentation/CleanHr.Api/Features/User/Endpoints/GetRefreshedAccessTokenEndpoint.cs: -------------------------------------------------------------------------------- 1 | using CleanHr.Api.Features.User.Models; 2 | using CleanHr.Api.Helpers; 3 | using Microsoft.AspNetCore.Authorization; 4 | using Microsoft.AspNetCore.Mvc; 5 | using Swashbuckle.AspNetCore.Annotations; 6 | 7 | namespace CleanHr.Api.Features.User.Endpoints; 8 | 9 | [ApiVersion("1.0")] 10 | public class GetRefreshedAccessTokenEndpoint( 11 | TokenManager tokenManager) : UserEndpointBase 12 | { 13 | [HttpPost("refresh-token")] 14 | [AllowAnonymous] 15 | [ProducesResponseType(StatusCodes.Status200OK)] 16 | [ProducesResponseType(StatusCodes.Status400BadRequest)] 17 | [ProducesDefaultResponseType] 18 | [SwaggerOperation(Summary = "Get a new access token for user by posting user's expired access token and refresh token.")] 19 | public async Task> Post(TokenRefreshModel model) 20 | { 21 | string jsonWebToken = await tokenManager.GetJwtTokenAsync(model.AccessToken); 22 | return Ok(jsonWebToken); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/ServerApp/Presentation/CleanHr.Api/Features/User/Endpoints/ResendUserEmailConfirmationCodeEndpoint.cs: -------------------------------------------------------------------------------- 1 | using CleanHr.Api.Features.User.Models; 2 | using CleanHr.Application.Commands.IdentityCommands.UserCommands; 3 | using MediatR; 4 | using Microsoft.AspNetCore.Authorization; 5 | using Microsoft.AspNetCore.Mvc; 6 | using Swashbuckle.AspNetCore.Annotations; 7 | 8 | namespace CleanHr.Api.Features.User.Endpoints; 9 | 10 | [ApiVersion("1.0")] 11 | public class ResendUserEmailConfirmationCodeEndpoint( 12 | IMediator mediator) : UserEndpointBase 13 | { 14 | [HttpPost("resend-email-confirmation-code")] 15 | [AllowAnonymous] 16 | [ProducesResponseType(StatusCodes.Status200OK)] 17 | [ProducesResponseType(StatusCodes.Status400BadRequest)] 18 | [ProducesResponseType(StatusCodes.Status500InternalServerError)] 19 | [ProducesDefaultResponseType] 20 | [SwaggerOperation(Summary = "Resend email confirmation code to the newly registered user's email.")] 21 | public async Task Post(ResendEmailConfirmationCodeModel model) 22 | { 23 | SendEmailVerificationCodeCommand command = new(model.Email); 24 | await mediator.Send(command); 25 | return Ok(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/ServerApp/Presentation/CleanHr.Api/Features/User/Endpoints/ResetUserPasswordEndpoint.cs: -------------------------------------------------------------------------------- 1 | using CleanHr.Api.Features.User.Models; 2 | using CleanHr.Application.Commands.IdentityCommands.UserCommands; 3 | using CleanHr.Application.Infrastructures; 4 | using MediatR; 5 | using Microsoft.AspNetCore.Authorization; 6 | using Microsoft.AspNetCore.Mvc; 7 | using Swashbuckle.AspNetCore.Annotations; 8 | 9 | namespace CleanHr.Api.Features.User.Endpoints; 10 | 11 | [ApiVersion("1.0")] 12 | public class ResetUserPasswordEndpoint( 13 | IMediator mediator, 14 | IExceptionLogger exceptionLogger) : UserEndpointBase 15 | { 16 | [AllowAnonymous] 17 | [HttpPost("reset-password")] 18 | [ProducesResponseType(StatusCodes.Status200OK)] 19 | [ProducesResponseType(StatusCodes.Status400BadRequest)] 20 | [ProducesResponseType(StatusCodes.Status500InternalServerError)] 21 | [ProducesDefaultResponseType] 22 | [SwaggerOperation(Summary = "Reset a new password for an user by posting the password reset code and the new password.")] 23 | public async Task Post(ResetPasswordModel model) 24 | { 25 | try 26 | { 27 | ResetPasswordCommand resetPasswordCommand = new(model.Email, model.Code, model.Password); 28 | await mediator.Send(resetPasswordCommand); 29 | return Ok(); 30 | } 31 | catch (Exception exception) 32 | { 33 | model.Password = string.Empty; 34 | model.ConfirmPassword = string.Empty; 35 | await exceptionLogger.LogAsync(exception, model); 36 | return StatusCode(StatusCodes.Status500InternalServerError); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/ServerApp/Presentation/CleanHr.Api/Features/User/Endpoints/SendUserPasswordResetCodeEndpoint.cs: -------------------------------------------------------------------------------- 1 | using CleanHr.Api.Features.User.Models; 2 | using CleanHr.Application.Commands.IdentityCommands.UserCommands; 3 | using MediatR; 4 | using Microsoft.AspNetCore.Authorization; 5 | using Microsoft.AspNetCore.Mvc; 6 | using Swashbuckle.AspNetCore.Annotations; 7 | 8 | namespace CleanHr.Api.Features.User.Endpoints; 9 | 10 | [ApiVersion("1.0")] 11 | public class SendUserPasswordResetCodeEndpoint( 12 | IMediator mediator) : UserEndpointBase 13 | { 14 | [AllowAnonymous] 15 | [HttpPost("send-password-reset-code")] 16 | [ProducesResponseType(StatusCodes.Status200OK)] 17 | [ProducesResponseType(StatusCodes.Status500InternalServerError)] 18 | [ProducesDefaultResponseType] 19 | [SwaggerOperation(Summary = "Send password reset code to reset user's password.")] 20 | public async Task Post(ForgotPasswordModel model) 21 | { 22 | SendPasswordResetCodeCommand command = new(model.Email); 23 | await mediator.Send(command); 24 | return Ok(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/ServerApp/Presentation/CleanHr.Api/Features/User/Endpoints/UserEndpointBase.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authorization; 2 | using Microsoft.AspNetCore.Mvc; 3 | 4 | namespace CleanHr.Api.Features.User.Endpoints; 5 | 6 | [Authorize] 7 | [Route("api/v{version:apiVersion}/user")] 8 | [ApiController] 9 | [ApiExplorerSettings(GroupName = "User Endpoints")] 10 | public abstract class UserEndpointBase : ControllerBase 11 | { 12 | } 13 | -------------------------------------------------------------------------------- /src/ServerApp/Presentation/CleanHr.Api/Features/User/Endpoints/UserLogoutEndpoint.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authentication; 2 | using Microsoft.AspNetCore.Mvc; 3 | 4 | namespace CleanHr.Api.Features.User.Endpoints; 5 | 6 | [ApiVersion("1.0")] 7 | public class UserLogoutEndpoint : UserEndpointBase 8 | { 9 | [HttpPost("logout")] 10 | public async Task Post() 11 | { 12 | await HttpContext.SignOutAsync(); 13 | return Ok(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/ServerApp/Presentation/CleanHr.Api/Features/User/Models/EmailConfirmationModel.cs: -------------------------------------------------------------------------------- 1 | namespace CleanHr.Api.Features.User.Models; 2 | 3 | public class EmailConfirmationModel 4 | { 5 | public string Email { get; set; } 6 | 7 | public string Code { get; set; } 8 | } 9 | -------------------------------------------------------------------------------- /src/ServerApp/Presentation/CleanHr.Api/Features/User/Models/ForgotPasswordModel.cs: -------------------------------------------------------------------------------- 1 | namespace CleanHr.Api.Features.User.Models; 2 | 3 | public class ForgotPasswordModel 4 | { 5 | public string Email { get; set; } 6 | } 7 | -------------------------------------------------------------------------------- /src/ServerApp/Presentation/CleanHr.Api/Features/User/Models/LoginModel.cs: -------------------------------------------------------------------------------- 1 | namespace CleanHr.Api.Features.User.Models; 2 | 3 | public class LoginModel 4 | { 5 | public string EmailOrUserName { get; set; } 6 | 7 | public string Password { get; set; } 8 | 9 | public bool RememberMe { get; set; } 10 | } 11 | -------------------------------------------------------------------------------- /src/ServerApp/Presentation/CleanHr.Api/Features/User/Models/RegistrationModel.cs: -------------------------------------------------------------------------------- 1 | namespace CleanHr.Api.Features.User.Models; 2 | 3 | public class RegistrationModel 4 | { 5 | public string FirstName { get; set; } 6 | 7 | public string LastName { get; set; } 8 | 9 | public string Email { get; set; } 10 | 11 | public string Password { get; set; } 12 | 13 | public string ConfirmPassword { get; set; } 14 | } 15 | -------------------------------------------------------------------------------- /src/ServerApp/Presentation/CleanHr.Api/Features/User/Models/ResendEmailConfirmationCodeModel.cs: -------------------------------------------------------------------------------- 1 | namespace CleanHr.Api.Features.User.Models; 2 | 3 | public class ResendEmailConfirmationCodeModel 4 | { 5 | public string Email { get; set; } 6 | } 7 | -------------------------------------------------------------------------------- /src/ServerApp/Presentation/CleanHr.Api/Features/User/Models/ResetPasswordModel.cs: -------------------------------------------------------------------------------- 1 | namespace CleanHr.Api.Features.User.Models; 2 | 3 | public class ResetPasswordModel 4 | { 5 | public string Email { get; set; } 6 | 7 | public string Password { get; set; } 8 | 9 | public string ConfirmPassword { get; set; } 10 | 11 | public string Code { get; set; } 12 | } 13 | -------------------------------------------------------------------------------- /src/ServerApp/Presentation/CleanHr.Api/Features/User/Models/TokenRefreshModel.cs: -------------------------------------------------------------------------------- 1 | namespace CleanHr.Api.Features.User.Models; 2 | 3 | public class TokenRefreshModel 4 | { 5 | public string AccessToken { get; set; } 6 | 7 | public string RefreshToken { get; set; } 8 | } 9 | -------------------------------------------------------------------------------- /src/ServerApp/Presentation/CleanHr.Api/Features/User/Validators/ForgotPasswordModelValidator.cs: -------------------------------------------------------------------------------- 1 | using System.Data; 2 | using System.Threading; 3 | using CleanHr.Api.Features.User.Models; 4 | using CleanHr.Domain.Aggregates.IdentityAggregate; 5 | using FluentValidation; 6 | using Microsoft.AspNetCore.Identity; 7 | using Microsoft.EntityFrameworkCore; 8 | 9 | namespace CleanHr.Api.Features.User.Validators; 10 | 11 | public sealed class ForgotPasswordModelValidator : AbstractValidator 12 | { 13 | private readonly UserManager _userManager; 14 | 15 | public ForgotPasswordModelValidator(UserManager userManager) 16 | { 17 | _userManager = userManager; 18 | 19 | RuleFor(fpm => fpm.Email).NotEmpty() 20 | .WithMessage("The email is required.") 21 | .EmailAddress() 22 | .WithMessage("The email is not a valid email.") 23 | .MustAsync(IsEmailExistentAsync) 24 | .WithMessage("The email does not belong to any user account."); 25 | } 26 | 27 | private async Task IsEmailExistentAsync(string email, CancellationToken cancellationToken) 28 | { 29 | bool isExistent = await _userManager.Users 30 | .Where(u => u.Email == email) 31 | .AnyAsync(cancellationToken); 32 | return isExistent; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/ServerApp/Presentation/CleanHr.Api/Features/User/Validators/LoginModelValidator.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using CleanHr.Api.Features.User.Models; 3 | using CleanHr.Domain.Aggregates.IdentityAggregate; 4 | using FluentValidation; 5 | using Microsoft.AspNetCore.Identity; 6 | using Microsoft.EntityFrameworkCore; 7 | 8 | namespace CleanHr.Api.Features.User.Validators; 9 | 10 | public sealed class LoginModelValidator : AbstractValidator 11 | { 12 | private readonly UserManager _userManager; 13 | 14 | public LoginModelValidator(UserManager userManager) 15 | { 16 | _userManager = userManager; 17 | 18 | RuleFor(lm => lm.EmailOrUserName).NotEmpty() 19 | .WithMessage("The emailOrUserName is required.") 20 | .MinimumLength(5) 21 | .MaximumLength(50) 22 | .MustAsync(IsEmailOrUserNameExistentAsync) 23 | .WithMessage("The emailOrUserName does not exist."); 24 | 25 | RuleFor(lm => lm.Password).NotEmpty() 26 | .WithMessage("The Password is required.") 27 | .MinimumLength(6) 28 | .MaximumLength(50); 29 | } 30 | 31 | private async Task IsEmailOrUserNameExistentAsync( 32 | string emailOrUserName, 33 | CancellationToken cancellationToken) 34 | { 35 | string emailUpper = emailOrUserName.ToUpperInvariant(); 36 | var isExistent = await _userManager.Users 37 | .Where(u => u.NormalizedEmail == emailUpper || u.NormalizedUserName == emailUpper) 38 | .AnyAsync(cancellationToken); 39 | return isExistent; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/ServerApp/Presentation/CleanHr.Api/Filters/BadRequestResultFilter.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | using Microsoft.AspNetCore.Mvc.Filters; 3 | 4 | namespace CleanHr.Api.Filters; 5 | 6 | public class BadRequestResultFilter : IResultFilter 7 | { 8 | public void OnResultExecuting(ResultExecutingContext context) 9 | { 10 | if (context != null && context.Result is BadRequestObjectResult) 11 | { 12 | context.Result = new BadRequestObjectResult(new ValidationProblemDetails(context.ModelState)); 13 | } 14 | } 15 | 16 | public void OnResultExecuted(ResultExecutedContext context) 17 | { 18 | // Do nothing 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/ServerApp/Presentation/CleanHr.Api/GlobalSuppressions.cs: -------------------------------------------------------------------------------- 1 | // This file is used by Code Analysis to maintain SuppressMessage 2 | // attributes that are applied to this project. 3 | // Project-level suppressions either have no target or are given 4 | // a specific target and scoped to a namespace, type, member, etc. 5 | 6 | using System.Diagnostics.CodeAnalysis; 7 | 8 | [assembly: SuppressMessage("Design", "CA1062:Validate arguments of public methods", Justification = "Not Applicable here.", Scope = "namespaceanddescendants", Target = "~N:CleanHr.Api.Features.User.Endpoints")] 9 | [assembly: SuppressMessage("Design", "CA1062:Validate arguments of public methods", Justification = "Not Applicable here.", Scope = "namespaceanddescendants", Target = "~N:CleanHr.Api.Features.Employee.Endpoints")] 10 | [assembly: SuppressMessage("Design", "CA1062:Validate arguments of public methods", Justification = "Not Applicable here.", Scope = "namespaceanddescendants", Target = "~N:CleanHr.Api.Features.Department.Endpoints")] 11 | [assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "Not Applicable here.", Scope = "namespaceanddescendants", Target = "~N:CleanHr.Api.Endpoints")] 12 | -------------------------------------------------------------------------------- /src/ServerApp/Presentation/CleanHr.Api/GlobalUsings.cs: -------------------------------------------------------------------------------- 1 | global using System; 2 | global using System.Collections.Generic; 3 | global using System.IO; 4 | global using System.Linq; 5 | global using System.Threading.Tasks; 6 | global using Microsoft.AspNetCore.Builder; 7 | global using Microsoft.AspNetCore.Hosting; 8 | global using Microsoft.AspNetCore.Http; 9 | global using Microsoft.AspNetCore.Routing; 10 | global using Microsoft.Extensions.Configuration; 11 | global using Microsoft.Extensions.DependencyInjection; 12 | global using Microsoft.Extensions.Hosting; 13 | -------------------------------------------------------------------------------- /src/ServerApp/Presentation/CleanHr.Api/Health/DbConnectionHealthCheck.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using Microsoft.Data.SqlClient; 3 | using Microsoft.Extensions.Diagnostics.HealthChecks; 4 | using Microsoft.Extensions.Logging; 5 | 6 | namespace CleanHr.Api; 7 | 8 | public class DbConnectionHealthCheck( 9 | string connectionString, 10 | ILogger logger) : IHealthCheck 11 | { 12 | private readonly string _connectionString = connectionString; 13 | private readonly ILogger _logger = logger ?? throw new ArgumentNullException(nameof(logger)); 14 | 15 | public async Task CheckHealthAsync( 16 | HealthCheckContext context, 17 | CancellationToken cancellationToken = default) 18 | { 19 | try 20 | { 21 | _logger.LogInformation("ConnectionString in DbHealthCheck is: {ConnectionString}", _connectionString); 22 | using SqlConnection sqlConnection = new(_connectionString); 23 | using SqlCommand sqlCommand = sqlConnection.CreateCommand(); 24 | sqlCommand.CommandText = "SELECT 1"; 25 | 26 | await sqlConnection.OpenAsync(cancellationToken); 27 | await sqlCommand.ExecuteScalarAsync(cancellationToken); 28 | await sqlConnection.CloseAsync(); 29 | return HealthCheckResult.Healthy(description: "The database connection is fine."); 30 | } 31 | catch (Exception exception) 32 | { 33 | _logger.LogCritical(exception: exception, "The exception happened with connection string : {ConnectionString}", _connectionString); 34 | return HealthCheckResult.Unhealthy(description: exception.Message); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/ServerApp/Presentation/CleanHr.Api/Health/ReadinessHealthCheck.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using Microsoft.Extensions.Diagnostics.HealthChecks; 4 | 5 | namespace CleanHr.Api; 6 | 7 | public class ReadinessHealthCheck : IHealthCheck 8 | { 9 | public bool IsStartupCompleted { get; set; } 10 | 11 | public Task CheckHealthAsync( 12 | HealthCheckContext context, 13 | CancellationToken cancellationToken = default) 14 | { 15 | if (IsStartupCompleted) 16 | { 17 | return Task.FromResult(HealthCheckResult.Healthy(description: "The startup task has completed.")); 18 | } 19 | 20 | return Task.FromResult(HealthCheckResult.Unhealthy(description: "The startup task is still running.")); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/ServerApp/Presentation/CleanHr.Api/Health/SendGridConnectionHealthCheck.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using Microsoft.Extensions.Diagnostics.HealthChecks; 4 | 5 | namespace CleanHr.Api; 6 | 7 | public class SendGridConnectionHealthCheck : IHealthCheck 8 | { 9 | public Task CheckHealthAsync( 10 | HealthCheckContext context, 11 | CancellationToken cancellationToken = default) 12 | { 13 | throw new NotImplementedException(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/ServerApp/Presentation/CleanHr.Api/Helpers/ConfigurationHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace CleanHr.Api; 4 | 5 | public static class ConfigurationHelper 6 | { 7 | public static string GetDbConnectionString(this WebApplicationBuilder builder) 8 | { 9 | ArgumentNullException.ThrowIfNull(builder); 10 | 11 | bool isUnixLikeSystem = Environment.OSVersion.Platform == PlatformID.Unix 12 | || Environment.OSVersion.Platform == PlatformID.MacOSX; 13 | 14 | string connectionString; 15 | 16 | if (isUnixLikeSystem) 17 | { 18 | connectionString = builder.Configuration.GetConnectionString("DockerDbConnection"); 19 | } 20 | else 21 | { 22 | string connectionName = builder.Environment.IsDevelopment() ? "EmployeeDbConnection" : "EmployeeDbConnection"; 23 | connectionString = builder.Configuration.GetConnectionString(connectionName); 24 | } 25 | 26 | return connectionString; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/ServerApp/Presentation/CleanHr.Api/Pages/ExternalLoginConfirmationPage.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @using CleanHr.Api.Pages; 3 | @model ExternalLoginConfirmationPage -------------------------------------------------------------------------------- /src/ServerApp/Presentation/CleanHr.Api/Pages/ExternalLoginConfirmationPage.cshtml.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc.RazorPages; 2 | 3 | namespace CleanHr.Api.Pages; 4 | 5 | public class ExternalLoginConfirmationPage : PageModel 6 | { 7 | public void OnGet() 8 | { 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/ServerApp/Presentation/CleanHr.Api/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/launchsettings.json", 3 | "iisSettings": { 4 | "windowsAuthentication": false, 5 | "anonymousAuthentication": true, 6 | "iisExpress": { 7 | "applicationUrl": "http://localhost:9175/", 8 | "sslPort": 44381 9 | } 10 | }, 11 | "profiles": { 12 | "CleanHr.Api": { 13 | "commandName": "Project", 14 | "launchBrowser": true, 15 | "launchUrl": "swagger", 16 | "applicationUrl": "http://localhost:5100;https://localhost:5101", 17 | "environmentVariables": { 18 | "ASPNETCORE_ENVIRONMENT": "Development" 19 | } 20 | }, 21 | "IIS Express": { 22 | "commandName": "IISExpress", 23 | "launchBrowser": true, 24 | "launchUrl": "swagger", 25 | "environmentVariables": { 26 | "ASPNETCORE_ENVIRONMENT": "Development" 27 | } 28 | }, 29 | "Docker": { 30 | "commandName": "Docker", 31 | "launchBrowser": true, 32 | "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}/swagger", 33 | "environmentVariables": {}, 34 | "publishAllPorts": true, 35 | "useSSL": true 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /src/ServerApp/Presentation/CleanHr.Api/Utilities/SlugifyParameterTransformer.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | using System.Text.RegularExpressions; 3 | 4 | namespace CleanHr.Api.Utilities; 5 | 6 | public partial class SlugifyParameterTransformer : IOutboundParameterTransformer 7 | { 8 | public string TransformOutbound(object value) 9 | { 10 | // Slugify value 11 | return value == null ? null : MyRegex().Replace(value.ToString(), "$1-$2").ToLower(CultureInfo.InvariantCulture); 12 | } 13 | 14 | [GeneratedRegex("([a-z])([A-Z])")] 15 | private static partial Regex MyRegex(); 16 | } 17 | -------------------------------------------------------------------------------- /src/ServerApp/Presentation/CleanHr.Api/Workers/ConfigurationLoadingBackgroundService.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using Microsoft.Extensions.Logging; 3 | 4 | namespace CleanHr.Api; 5 | 6 | public class ConfigurationLoadingBackgroundService( 7 | ReadinessHealthCheck readinessHealthCheck, 8 | ILogger logger) : BackgroundService 9 | { 10 | protected override async Task ExecuteAsync(CancellationToken stoppingToken) 11 | { 12 | logger.LogInformation("Configuration loading has been started."); 13 | 14 | // Simulate the effect of a long-running task. 15 | await Task.Delay(TimeSpan.FromSeconds(30), stoppingToken); 16 | 17 | readinessHealthCheck.IsStartupCompleted = true; 18 | 19 | logger.LogInformation("Configuration loading has been completed successfully."); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/ServerApp/Presentation/CleanHr.Api/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /test/CleanHr.Application.Tests/CleanHr.Application.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | false 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | runtime; build; native; contentfiles; analyzers; buildtransitive 17 | all 18 | 19 | 20 | runtime; build; native; contentfiles; analyzers; buildtransitive 21 | all 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /test/CleanHr.Application.Tests/DepartmentCommandTests/CreateDepartmentCommandHandlerTests.cs: -------------------------------------------------------------------------------- 1 | using CleanHr.Application.Caching.Handlers; 2 | using CleanHr.Application.Commands.DepartmentCommands; 3 | using CleanHr.Domain.Aggregates.DepartmentAggregate; 4 | using CleanHr.Domain.Exceptions; 5 | using Moq; 6 | 7 | namespace CleanHr.Application.Tests; 8 | 9 | public class CreateDepartmentCommandHandlerTests 10 | { 11 | // Arrange 12 | private readonly Mock mockDepartmentRepository = new(); 13 | private readonly Mock mockDepartmentCacheHandler = new(); 14 | 15 | [Fact] 16 | public async Task Handle_WithValidCommand_ReturnsDepartmentId() 17 | { 18 | // Act 19 | CreateDepartmentCommandHandler handler = new( 20 | mockDepartmentRepository.Object, mockDepartmentCacheHandler.Object); 21 | 22 | CreateDepartmentCommand createDepartmentRequest = new("Human Resource", "This is human resource department."); 23 | Guid departmentId = await handler.Handle(createDepartmentRequest, CancellationToken.None); 24 | 25 | // Assert 26 | Assert.NotEqual(Guid.Empty, departmentId); 27 | mockDepartmentRepository.Verify(dr => dr.InsertAsync(It.IsAny()), Times.Once()); 28 | mockDepartmentCacheHandler.Verify(dr => dr.RemoveListAsync(), Times.Once()); 29 | } 30 | 31 | [Theory] 32 | [InlineData("", "This is human resource department.")] 33 | [InlineData("HR", "This is human resource department.")] 34 | [InlineData(null, "This is human resource department.")] 35 | public async Task Handle_WithInvalidCommand_ReturnsDepartmentId(string departmentName, string description) 36 | { 37 | // Act 38 | CreateDepartmentCommandHandler handler = new( 39 | mockDepartmentRepository.Object, mockDepartmentCacheHandler.Object); 40 | 41 | CreateDepartmentCommand createDepartmentRequest = new(departmentName, description); 42 | await Assert.ThrowsAsync(() => handler.Handle(createDepartmentRequest, CancellationToken.None)); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /test/CleanHr.Application.Tests/UnitTest1.cs: -------------------------------------------------------------------------------- 1 | namespace CleanHr.Application.Tests; 2 | 3 | public class UnitTest1 4 | { 5 | [Fact] 6 | public void Test1() 7 | { 8 | 9 | } 10 | } -------------------------------------------------------------------------------- /test/CleanHr.Application.Tests/Usings.cs: -------------------------------------------------------------------------------- 1 | global using Xunit; --------------------------------------------------------------------------------
Sorry, there's nothing at this address.
7 | You are not authorized to see the requested page. Please login by clicking the following button to move forward. 8 |