├── .gitattributes ├── .github └── workflows │ └── dotnet.yml ├── .gitignore ├── Application ├── Application.csproj ├── Common │ ├── Error │ │ ├── ExceptionHandling.cs │ │ └── IErrorMessageLog.cs │ ├── Interface │ │ ├── IApplicationDBContext.cs │ │ ├── IConfigurationExtension.cs │ │ ├── ICurrentUserService.cs │ │ ├── IRepository.cs │ │ ├── IUnitOfWork.cs │ │ └── IUserService.cs │ ├── Mapping │ │ ├── AutoMapperProfile.cs │ │ └── ManualViewMapping.cs │ └── Model │ │ ├── GlobalDeclaration.cs │ │ ├── ResponseModel.cs │ │ ├── UserModel.cs │ │ └── Video.cs ├── DependencyInjection.cs └── Master │ ├── Dto │ └── AppSettingVm.cs │ ├── IMasterServices.cs │ └── MasterServices.cs ├── CleanBlazorWeb.sln ├── CleanBlazorWeb ├── CleanBlazorWeb.Client │ ├── CleanBlazorWeb.Client.csproj │ ├── Pages │ │ ├── Auth.razor │ │ └── Counter.razor │ ├── PersistentAuthenticationStateProvider.cs │ ├── Program.cs │ ├── RedirectToLogin.razor │ ├── UserInfo.cs │ ├── _Imports.razor │ └── wwwroot │ │ ├── appsettings.Development.json │ │ └── appsettings.json └── CleanBlazorWeb │ ├── CleanBlazorWeb.csproj │ ├── Components │ ├── Account │ │ ├── IdentityComponentsEndpointRouteBuilderExtensions.cs │ │ ├── IdentityNoOpEmailSender.cs │ │ ├── IdentityRedirectManager.cs │ │ ├── IdentityUserAccessor.cs │ │ ├── Pages │ │ │ ├── AccessDenied.razor │ │ │ ├── ConfirmEmail.razor │ │ │ ├── ConfirmEmailChange.razor │ │ │ ├── ExternalLogin.razor │ │ │ ├── ForgotPassword.razor │ │ │ ├── ForgotPasswordConfirmation.razor │ │ │ ├── InvalidPasswordReset.razor │ │ │ ├── InvalidUser.razor │ │ │ ├── Lockout.razor │ │ │ ├── Login.razor │ │ │ ├── LoginWith2fa.razor │ │ │ ├── LoginWithRecoveryCode.razor │ │ │ ├── Manage │ │ │ │ ├── ChangePassword.razor │ │ │ │ ├── DeletePersonalData.razor │ │ │ │ ├── Disable2fa.razor │ │ │ │ ├── Email.razor │ │ │ │ ├── EnableAuthenticator.razor │ │ │ │ ├── ExternalLogins.razor │ │ │ │ ├── GenerateRecoveryCodes.razor │ │ │ │ ├── Index.razor │ │ │ │ ├── PersonalData.razor │ │ │ │ ├── ResetAuthenticator.razor │ │ │ │ ├── SetPassword.razor │ │ │ │ ├── TwoFactorAuthentication.razor │ │ │ │ └── _Imports.razor │ │ │ ├── Register.razor │ │ │ ├── RegisterConfirmation.razor │ │ │ ├── ResendEmailConfirmation.razor │ │ │ ├── ResetPassword.razor │ │ │ ├── ResetPasswordConfirmation.razor │ │ │ └── _Imports.razor │ │ ├── PersistingRevalidatingAuthenticationStateProvider.cs │ │ └── Shared │ │ │ ├── AccountLayout.razor │ │ │ ├── ExternalLoginPicker.razor │ │ │ ├── ManageLayout.razor │ │ │ ├── ManageNavMenu.razor │ │ │ ├── ShowRecoveryCodes.razor │ │ │ └── StatusMessage.razor │ ├── App.razor │ ├── Layout │ │ ├── MainLayout.razor │ │ ├── MainLayout.razor.css │ │ ├── NavMenu.razor │ │ └── NavMenu.razor.css │ ├── Pages │ │ ├── Error.razor │ │ ├── Home.razor │ │ └── Weather.razor │ ├── Routes.razor │ └── _Imports.razor │ ├── Data │ ├── ApplicationDbContext.cs │ ├── ApplicationUser.cs │ └── Migrations │ │ ├── 00000000000000_CreateIdentitySchema.Designer.cs │ │ ├── 00000000000000_CreateIdentitySchema.cs │ │ └── ApplicationDbContextModelSnapshot.cs │ ├── Program.cs │ ├── Properties │ ├── launchSettings.json │ ├── serviceDependencies.json │ └── serviceDependencies.local.json │ ├── appsettings.Development.json │ ├── appsettings.json │ └── wwwroot │ ├── app.css │ ├── bootstrap │ ├── bootstrap.min.css │ └── bootstrap.min.css.map │ └── favicon.png ├── Domain ├── Common │ ├── AppConstants.cs │ ├── AuditableWithBaseEntity.cs │ ├── BaseEntity.cs │ ├── IAuditableEntity.cs │ ├── IEntity.cs │ └── PaginatedList.cs ├── Domain.csproj └── Master │ ├── AppSetting.cs │ ├── ReferenceField.cs │ └── UserAccess.cs ├── Infrastructure ├── DependencyInjection.cs ├── Identity │ ├── ApplicationUser.cs │ └── UserService.cs ├── Infrastructure.csproj ├── LogCapture │ └── ErrorMessageLog.cs └── Persistence │ ├── ApplicationDBContext.cs │ ├── Configuration │ └── Master │ │ └── AppSettingConfiguration.cs │ ├── EfRepository.cs │ ├── Migrations │ ├── 00000000000000_CreateIdentitySchema.Designer.cs │ ├── 00000000000000_CreateIdentitySchema.cs │ ├── 20240202065228_initializeDb.Designer.cs │ ├── 20240202065228_initializeDb.cs │ └── ApplicationDbContextModelSnapshot.cs │ └── UnitOfWork.cs ├── LICENSE.txt └── README.md /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /.github/workflows/dotnet.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a .NET project 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-net 3 | 4 | name: .NET 5 | 6 | on: 7 | push: 8 | branches: [ "master" ] 9 | pull_request: 10 | branches: [ "master" ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | - name: Setup .NET 20 | uses: actions/setup-dotnet@v4 21 | with: 22 | dotnet-version: 8.0.x 23 | - name: Restore dependencies 24 | run: dotnet restore 25 | - name: Build 26 | run: dotnet build --no-restore 27 | - name: Test 28 | run: dotnet test --no-build --verbosity normal 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Ww][Ii][Nn]32/ 27 | [Aa][Rr][Mm]/ 28 | [Aa][Rr][Mm]64/ 29 | bld/ 30 | [Bb]in/ 31 | [Oo]bj/ 32 | [Oo]ut/ 33 | [Ll]og/ 34 | [Ll]ogs/ 35 | 36 | # Visual Studio 2015/2017 cache/options directory 37 | .vs/ 38 | # Uncomment if you have tasks that create the project's static files in wwwroot 39 | #wwwroot/ 40 | 41 | # Visual Studio 2017 auto generated files 42 | Generated\ Files/ 43 | 44 | # MSTest test Results 45 | [Tt]est[Rr]esult*/ 46 | [Bb]uild[Ll]og.* 47 | 48 | # NUnit 49 | *.VisualState.xml 50 | TestResult.xml 51 | nunit-*.xml 52 | 53 | # Build Results of an ATL Project 54 | [Dd]ebugPS/ 55 | [Rr]eleasePS/ 56 | dlldata.c 57 | 58 | # Benchmark Results 59 | BenchmarkDotNet.Artifacts/ 60 | 61 | # .NET Core 62 | project.lock.json 63 | project.fragment.lock.json 64 | artifacts/ 65 | 66 | # ASP.NET Scaffolding 67 | ScaffoldingReadMe.txt 68 | 69 | # StyleCop 70 | StyleCopReport.xml 71 | 72 | # Files built by Visual Studio 73 | *_i.c 74 | *_p.c 75 | *_h.h 76 | *.ilk 77 | *.meta 78 | *.obj 79 | *.iobj 80 | *.pch 81 | *.pdb 82 | *.ipdb 83 | *.pgc 84 | *.pgd 85 | *.rsp 86 | *.sbr 87 | *.tlb 88 | *.tli 89 | *.tlh 90 | *.tmp 91 | *.tmp_proj 92 | *_wpftmp.csproj 93 | *.log 94 | *.vspscc 95 | *.vssscc 96 | .builds 97 | *.pidb 98 | *.svclog 99 | *.scc 100 | 101 | # Chutzpah Test files 102 | _Chutzpah* 103 | 104 | # Visual C++ cache files 105 | ipch/ 106 | *.aps 107 | *.ncb 108 | *.opendb 109 | *.opensdf 110 | *.sdf 111 | *.cachefile 112 | *.VC.db 113 | *.VC.VC.opendb 114 | 115 | # Visual Studio profiler 116 | *.psess 117 | *.vsp 118 | *.vspx 119 | *.sap 120 | 121 | # Visual Studio Trace Files 122 | *.e2e 123 | 124 | # TFS 2012 Local Workspace 125 | $tf/ 126 | 127 | # Guidance Automation Toolkit 128 | *.gpState 129 | 130 | # ReSharper is a .NET coding add-in 131 | _ReSharper*/ 132 | *.[Rr]e[Ss]harper 133 | *.DotSettings.user 134 | 135 | # TeamCity is a build add-in 136 | _TeamCity* 137 | 138 | # DotCover is a Code Coverage Tool 139 | *.dotCover 140 | 141 | # AxoCover is a Code Coverage Tool 142 | .axoCover/* 143 | !.axoCover/settings.json 144 | 145 | # Coverlet is a free, cross platform Code Coverage Tool 146 | coverage*.json 147 | coverage*.xml 148 | coverage*.info 149 | 150 | # Visual Studio code coverage results 151 | *.coverage 152 | *.coveragexml 153 | 154 | # NCrunch 155 | _NCrunch_* 156 | .*crunch*.local.xml 157 | nCrunchTemp_* 158 | 159 | # MightyMoose 160 | *.mm.* 161 | AutoTest.Net/ 162 | 163 | # Web workbench (sass) 164 | .sass-cache/ 165 | 166 | # Installshield output folder 167 | [Ee]xpress/ 168 | 169 | # DocProject is a documentation generator add-in 170 | DocProject/buildhelp/ 171 | DocProject/Help/*.HxT 172 | DocProject/Help/*.HxC 173 | DocProject/Help/*.hhc 174 | DocProject/Help/*.hhk 175 | DocProject/Help/*.hhp 176 | DocProject/Help/Html2 177 | DocProject/Help/html 178 | 179 | # Click-Once directory 180 | publish/ 181 | 182 | # Publish Web Output 183 | *.[Pp]ublish.xml 184 | *.azurePubxml 185 | # Note: Comment the next line if you want to checkin your web deploy settings, 186 | # but database connection strings (with potential passwords) will be unencrypted 187 | *.pubxml 188 | *.publishproj 189 | 190 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 191 | # checkin your Azure Web App publish settings, but sensitive information contained 192 | # in these scripts will be unencrypted 193 | PublishScripts/ 194 | 195 | # NuGet Packages 196 | *.nupkg 197 | # NuGet Symbol Packages 198 | *.snupkg 199 | # The packages folder can be ignored because of Package Restore 200 | **/[Pp]ackages/* 201 | # except build/, which is used as an MSBuild target. 202 | !**/[Pp]ackages/build/ 203 | # Uncomment if necessary however generally it will be regenerated when needed 204 | #!**/[Pp]ackages/repositories.config 205 | # NuGet v3's project.json files produces more ignorable files 206 | *.nuget.props 207 | *.nuget.targets 208 | 209 | # Microsoft Azure Build Output 210 | csx/ 211 | *.build.csdef 212 | 213 | # Microsoft Azure Emulator 214 | ecf/ 215 | rcf/ 216 | 217 | # Windows Store app package directories and files 218 | AppPackages/ 219 | BundleArtifacts/ 220 | Package.StoreAssociation.xml 221 | _pkginfo.txt 222 | *.appx 223 | *.appxbundle 224 | *.appxupload 225 | 226 | # Visual Studio cache files 227 | # files ending in .cache can be ignored 228 | *.[Cc]ache 229 | # but keep track of directories ending in .cache 230 | !?*.[Cc]ache/ 231 | 232 | # Others 233 | ClientBin/ 234 | ~$* 235 | *~ 236 | *.dbmdl 237 | *.dbproj.schemaview 238 | *.jfm 239 | *.pfx 240 | *.publishsettings 241 | orleans.codegen.cs 242 | 243 | # Including strong name files can present a security risk 244 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 245 | #*.snk 246 | 247 | # Since there are multiple workflows, uncomment next line to ignore bower_components 248 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 249 | #bower_components/ 250 | 251 | # RIA/Silverlight projects 252 | Generated_Code/ 253 | 254 | # Backup & report files from converting an old project file 255 | # to a newer Visual Studio version. Backup files are not needed, 256 | # because we have git ;-) 257 | _UpgradeReport_Files/ 258 | Backup*/ 259 | UpgradeLog*.XML 260 | UpgradeLog*.htm 261 | ServiceFabricBackup/ 262 | *.rptproj.bak 263 | 264 | # SQL Server files 265 | *.mdf 266 | *.ldf 267 | *.ndf 268 | 269 | # Business Intelligence projects 270 | *.rdl.data 271 | *.bim.layout 272 | *.bim_*.settings 273 | *.rptproj.rsuser 274 | *- [Bb]ackup.rdl 275 | *- [Bb]ackup ([0-9]).rdl 276 | *- [Bb]ackup ([0-9][0-9]).rdl 277 | 278 | # Microsoft Fakes 279 | FakesAssemblies/ 280 | 281 | # GhostDoc plugin setting file 282 | *.GhostDoc.xml 283 | 284 | # Node.js Tools for Visual Studio 285 | .ntvs_analysis.dat 286 | node_modules/ 287 | 288 | # Visual Studio 6 build log 289 | *.plg 290 | 291 | # Visual Studio 6 workspace options file 292 | *.opt 293 | 294 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 295 | *.vbw 296 | 297 | # Visual Studio LightSwitch build output 298 | **/*.HTMLClient/GeneratedArtifacts 299 | **/*.DesktopClient/GeneratedArtifacts 300 | **/*.DesktopClient/ModelManifest.xml 301 | **/*.Server/GeneratedArtifacts 302 | **/*.Server/ModelManifest.xml 303 | _Pvt_Extensions 304 | 305 | # Paket dependency manager 306 | .paket/paket.exe 307 | paket-files/ 308 | 309 | # FAKE - F# Make 310 | .fake/ 311 | 312 | # CodeRush personal settings 313 | .cr/personal 314 | 315 | # Python Tools for Visual Studio (PTVS) 316 | __pycache__/ 317 | *.pyc 318 | 319 | # Cake - Uncomment if you are using it 320 | # tools/** 321 | # !tools/packages.config 322 | 323 | # Tabs Studio 324 | *.tss 325 | 326 | # Telerik's JustMock configuration file 327 | *.jmconfig 328 | 329 | # BizTalk build output 330 | *.btp.cs 331 | *.btm.cs 332 | *.odx.cs 333 | *.xsd.cs 334 | 335 | # OpenCover UI analysis results 336 | OpenCover/ 337 | 338 | # Azure Stream Analytics local run output 339 | ASALocalRun/ 340 | 341 | # MSBuild Binary and Structured Log 342 | *.binlog 343 | 344 | # NVidia Nsight GPU debugger configuration file 345 | *.nvuser 346 | 347 | # MFractors (Xamarin productivity tool) working folder 348 | .mfractor/ 349 | 350 | # Local History for Visual Studio 351 | .localhistory/ 352 | 353 | # BeatPulse healthcheck temp database 354 | healthchecksdb 355 | 356 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 357 | MigrationBackup/ 358 | 359 | # Ionide (cross platform F# VS Code tools) working folder 360 | .ionide/ 361 | 362 | # Fody - auto-generated XML schema 363 | FodyWeavers.xsd -------------------------------------------------------------------------------- /Application/Application.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /Application/Common/Error/ExceptionHandling.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Common.Error 2 | { 3 | internal class ExceptionHandling : Exception 4 | { 5 | 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /Application/Common/Error/IErrorMessageLog.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Common.Error 2 | { 3 | public interface IErrorMessageLog 4 | { 5 | bool LogError(string layerName, string className, string methodName, string msg); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /Application/Common/Interface/IApplicationDBContext.cs: -------------------------------------------------------------------------------- 1 | using Domain.Master; 2 | using Microsoft.EntityFrameworkCore; 3 | 4 | namespace Application.Common.Interface; 5 | 6 | public interface IApplicationDBContext 7 | { 8 | /// 9 | /// dbset for the application settings 10 | /// 11 | DbSet AppSettings { get; set; } 12 | /// 13 | /// Creates a DbSet that can be used to query and save instances of entity 14 | /// 15 | /// Entity type 16 | /// A set for the given entity type 17 | DbSet Set() where TEntity : class; 18 | /// 19 | /// Saves all changes made in this context to the database. 20 | /// 21 | /// 22 | Task SaveChangesAsync(); 23 | } 24 | -------------------------------------------------------------------------------- /Application/Common/Interface/IConfigurationExtension.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Common.Interface 2 | { 3 | public interface IConfigurationExtension 4 | { 5 | string GetConfiguration(string configKey); 6 | string GetSectionKeyValue(string key); 7 | 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Application/Common/Interface/ICurrentUserService.cs: -------------------------------------------------------------------------------- 1 | using Application.Common.Model; 2 | 3 | namespace Application.Common.Interface 4 | { 5 | public interface ICurrentUserService 6 | { 7 | string UserId { get; } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Application/Common/Interface/IRepository.cs: -------------------------------------------------------------------------------- 1 | using Domain.Common; 2 | using Microsoft.EntityFrameworkCore; 3 | using System.Linq.Expressions; 4 | 5 | namespace Application.Common.Interface 6 | { 7 | public interface IRepository where T : class 8 | { 9 | Task AddAsync(T entity); 10 | void Update(T entity); 11 | Task UpdateAsync(object Id, T entity); 12 | Task DeleteAsync(object id); 13 | Task Delete(T entity); 14 | Task Delete(object id); 15 | Task Delete(Expression> where); 16 | Task Get(object id); 17 | Task Get(Expression> where); 18 | IEnumerable GetMany(Expression> where); 19 | IEnumerable GetAll(); 20 | Task Count(Expression> where); 21 | Task Count(); 22 | #region Properties 23 | 24 | /// 25 | /// Gets a table 26 | /// 27 | IQueryable Table { get; } 28 | 29 | /// 30 | /// Gets a table with "no tracking" enabled (EF feature) Use it only when you load record(s) only for read-only operations 31 | /// 32 | IQueryable TableNoTracking { get; } 33 | 34 | DbSet Set() where TEntity : class; 35 | 36 | #endregion 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Application/Common/Interface/IUnitOfWork.cs: -------------------------------------------------------------------------------- 1 | using Domain; 2 | using Domain.Master; 3 | 4 | namespace Application.Common.Interface 5 | { 6 | public interface IUnitOfWork 7 | { 8 | IRepository Repository() where T : class; 9 | Task SaveAsync(); 10 | int Save(); 11 | void BeginTransaction(); 12 | void CommitTransaction(); 13 | void RollbackTransaction(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Application/Common/Interface/IUserService.cs: -------------------------------------------------------------------------------- 1 | using Application.Common.Model; 2 | 3 | namespace Application.Common.Interface 4 | { 5 | public interface IUserService 6 | { 7 | Task> GetUserByIdAsync(string userId); 8 | Task> GetUserByEmailAsync(string email); 9 | Task>> GetAllUsers(); 10 | Task> GetCurrentUser(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Application/Common/Mapping/AutoMapperProfile.cs: -------------------------------------------------------------------------------- 1 | using Application.Master.Dto; 2 | using AutoMapper; 3 | using Domain.Master; 4 | 5 | namespace Application.Common.Mapping; 6 | 7 | public class AutoMapperProfile : Profile 8 | { 9 | public AutoMapperProfile() 10 | { 11 | CreateMap().ReverseMap(); 12 | //CreateMap().ForMember(dest => dest.Require8105 , opt => opt.MapFrom(src => src.Requires8105)).ReverseMap(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Application/Common/Mapping/ManualViewMapping.cs: -------------------------------------------------------------------------------- 1 | using Application.Master.Dto; 2 | using Domain.Master; 3 | 4 | namespace Application.Common.Mapping 5 | { 6 | internal static class ManualViewMapping 7 | { 8 | /// 9 | /// Convert AppSetting to AppSettingVm 10 | /// 11 | /// 12 | /// 13 | public static AppSetting ConvertToModel(this AppSettingVm appSettingVm) 14 | { 15 | if (appSettingVm == null) 16 | return null; 17 | 18 | var appSetting = new AppSetting 19 | { 20 | Id = appSettingVm.Id, 21 | ReferenceKey = appSettingVm.ReferenceKey, 22 | Value = appSettingVm.Value, 23 | Description = appSettingVm.Description, 24 | Type = appSettingVm.Type, 25 | }; 26 | return appSetting; 27 | 28 | } 29 | 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Application/Common/Model/GlobalDeclaration.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Common.Model 2 | { 3 | internal class GlobalDeclaration 4 | { 5 | internal static readonly string _internalServerError = "Something went wrong."; 6 | internal static readonly string _successResponse = "Successful Response!"; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Application/Common/Model/ResponseModel.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Common.Model; 2 | 3 | public class ResponseModel 4 | { 5 | public bool Success { get; set; } 6 | public string Message { get; set; } 7 | public T? Output { get; set; } 8 | 9 | public static ResponseModel SuccessResponse(string message, T output) 10 | { 11 | return new ResponseModel 12 | { 13 | Success = true, 14 | Message = message, 15 | Output = output 16 | }; 17 | } 18 | public static ResponseModel FailureResponse(string message) 19 | { 20 | return new ResponseModel 21 | { 22 | Success = false, 23 | Message = message 24 | }; 25 | } 26 | 27 | } -------------------------------------------------------------------------------- /Application/Common/Model/UserModel.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Common.Model 2 | { 3 | public class UserModel 4 | { 5 | public string UserId { get; set; } 6 | public string UserName { get; set; } 7 | public string Email { get; set; } 8 | public bool EmailConfirmed { get; set; } 9 | public string PhoneNumber { get; set; } 10 | public string Author { get; set; } 11 | public DateTime Created { get; set; } 12 | public string Editor { get; set; } 13 | public DateTime Modified { get; set; } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Application/Common/Model/Video.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace Application.Common.Model 8 | { 9 | public class Video 10 | { 11 | public string? Title { get; set; } 12 | public string? Url { get; set; } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Application/DependencyInjection.cs: -------------------------------------------------------------------------------- 1 | using Application.Master; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using System.Reflection; 4 | 5 | namespace Application 6 | { 7 | public static class DependencyInjection 8 | { 9 | public static IServiceCollection AddApplication(this IServiceCollection services) 10 | { 11 | services.AddScoped(); 12 | services.AddAutoMapper(Assembly.GetExecutingAssembly()); 13 | return services; 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Application/Master/Dto/AppSettingVm.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Master.Dto 2 | { 3 | public record AppSettingVm 4 | { 5 | public int Id { get; init; } 6 | public string ReferenceKey { get; init; } = string.Empty; 7 | public string Value { get; init; } = string.Empty; 8 | public string Description { get; init; } = string.Empty; 9 | public string Type { get; init; } = string.Empty; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Application/Master/IMasterServices.cs: -------------------------------------------------------------------------------- 1 | using Application.Common.Model; 2 | using Domain.Master; 3 | 4 | namespace Application.Master 5 | { 6 | public interface IMasterServices 7 | { 8 | Task> GetAppSettingsAsync(); 9 | Task GetAppSettingByIdAsync(int Id); 10 | Task> UpsertAsync(AppSetting appSetting); 11 | Task DeleteAsync(int Id); 12 | 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Application/Master/MasterServices.cs: -------------------------------------------------------------------------------- 1 | using Application.Common.Error; 2 | using Application.Common.Interface; 3 | using Application.Common.Model; 4 | using AutoMapper; 5 | using Domain.Master; 6 | using Microsoft.EntityFrameworkCore; 7 | using Microsoft.Extensions.Logging; 8 | 9 | namespace Application.Master 10 | { 11 | internal class MasterServices (IUnitOfWork unitOfWork, ILogger logger, IMapper mapper, IErrorMessageLog errorMessageLog) : IMasterServices 12 | { 13 | #region Properties 14 | private readonly IUnitOfWork _unitOfWork = unitOfWork; 15 | private readonly ILogger _logger = logger; 16 | private readonly IMapper _mapper = mapper; 17 | private readonly IErrorMessageLog _errorMessageLog = errorMessageLog; 18 | #endregion 19 | 20 | #region AppSetting 21 | 22 | #region Command 23 | public async Task> UpsertAsync(AppSetting appSetting) 24 | { 25 | try 26 | { 27 | if (appSetting.Id > 0) 28 | _unitOfWork.Repository().Update(appSetting); 29 | else 30 | _unitOfWork.Repository().AddAsync(appSetting); 31 | 32 | await _unitOfWork.SaveAsync(); 33 | return ResponseModel.SuccessResponse("AppSetting Inserted", appSetting); 34 | } 35 | catch (Exception ex) 36 | { 37 | Log("UpsertAsync", ex.Message); 38 | _logger.LogError(ex, ex.Message); 39 | return ResponseModel.FailureResponse("Operation Failed"); 40 | } 41 | 42 | } 43 | public async Task DeleteAsync(int Id) 44 | { 45 | try 46 | { 47 | var IsDeteted = await _unitOfWork.Repository().Delete(Id); 48 | await _unitOfWork.SaveAsync(); 49 | return IsDeteted; 50 | } 51 | catch (Exception) 52 | { 53 | //handle exception 54 | return false; 55 | } 56 | } 57 | #endregion 58 | #region Queries 59 | public async Task> GetAppSettingsAsync() 60 | { 61 | var appsettigs = await _unitOfWork.Repository() 62 | .TableNoTracking 63 | .OrderBy(t => t.ReferenceKey) 64 | .ToListAsync(); 65 | return appsettigs; 66 | } 67 | public async Task GetAppSettingByIdAsync(int Id) 68 | { 69 | try 70 | { 71 | var appSetting = await _unitOfWork.Repository().Get(Id); 72 | return appSetting; 73 | } 74 | catch (Exception) 75 | { 76 | //handle exception 77 | throw; 78 | } 79 | } 80 | 81 | #endregion 82 | 83 | #endregion 84 | 85 | #region Error 86 | private void Log(string method, string msg) 87 | { 88 | errorMessageLog.LogError("Application", "MasterServices", method, msg); 89 | } 90 | #endregion 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /CleanBlazorWeb.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.8.34525.116 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CleanBlazorWeb", "CleanBlazorWeb\CleanBlazorWeb\CleanBlazorWeb.csproj", "{52A765E1-5360-41EF-91F2-5330470ED217}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CleanBlazorWeb.Client", "CleanBlazorWeb\CleanBlazorWeb.Client\CleanBlazorWeb.Client.csproj", "{4F0E08A6-FE02-442B-A279-65BD430853E6}" 9 | EndProject 10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Core", "Core", "{65F6BE4B-1C20-49C5-A584-421822688D16}" 11 | EndProject 12 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Application", "Application\Application.csproj", "{C653410E-8323-4FE3-AE97-647A2145428D}" 13 | EndProject 14 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Domain", "Domain\Domain.csproj", "{54108848-E0C9-408A-852B-CA1623CD9938}" 15 | EndProject 16 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Infra", "Infra", "{9E7843EF-8843-4436-B0C2-B148B347910A}" 17 | EndProject 18 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Infrastructure", "Infrastructure\Infrastructure.csproj", "{C8EAD560-92C8-4FE4-A603-EAC8EA4D933C}" 19 | EndProject 20 | Global 21 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 22 | Debug|Any CPU = Debug|Any CPU 23 | Release|Any CPU = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 26 | {52A765E1-5360-41EF-91F2-5330470ED217}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 27 | {52A765E1-5360-41EF-91F2-5330470ED217}.Debug|Any CPU.Build.0 = Debug|Any CPU 28 | {52A765E1-5360-41EF-91F2-5330470ED217}.Release|Any CPU.ActiveCfg = Release|Any CPU 29 | {52A765E1-5360-41EF-91F2-5330470ED217}.Release|Any CPU.Build.0 = Release|Any CPU 30 | {4F0E08A6-FE02-442B-A279-65BD430853E6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 31 | {4F0E08A6-FE02-442B-A279-65BD430853E6}.Debug|Any CPU.Build.0 = Debug|Any CPU 32 | {4F0E08A6-FE02-442B-A279-65BD430853E6}.Release|Any CPU.ActiveCfg = Release|Any CPU 33 | {4F0E08A6-FE02-442B-A279-65BD430853E6}.Release|Any CPU.Build.0 = Release|Any CPU 34 | {C653410E-8323-4FE3-AE97-647A2145428D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 35 | {C653410E-8323-4FE3-AE97-647A2145428D}.Debug|Any CPU.Build.0 = Debug|Any CPU 36 | {C653410E-8323-4FE3-AE97-647A2145428D}.Release|Any CPU.ActiveCfg = Release|Any CPU 37 | {C653410E-8323-4FE3-AE97-647A2145428D}.Release|Any CPU.Build.0 = Release|Any CPU 38 | {54108848-E0C9-408A-852B-CA1623CD9938}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 39 | {54108848-E0C9-408A-852B-CA1623CD9938}.Debug|Any CPU.Build.0 = Debug|Any CPU 40 | {54108848-E0C9-408A-852B-CA1623CD9938}.Release|Any CPU.ActiveCfg = Release|Any CPU 41 | {54108848-E0C9-408A-852B-CA1623CD9938}.Release|Any CPU.Build.0 = Release|Any CPU 42 | {C8EAD560-92C8-4FE4-A603-EAC8EA4D933C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 43 | {C8EAD560-92C8-4FE4-A603-EAC8EA4D933C}.Debug|Any CPU.Build.0 = Debug|Any CPU 44 | {C8EAD560-92C8-4FE4-A603-EAC8EA4D933C}.Release|Any CPU.ActiveCfg = Release|Any CPU 45 | {C8EAD560-92C8-4FE4-A603-EAC8EA4D933C}.Release|Any CPU.Build.0 = Release|Any CPU 46 | EndGlobalSection 47 | GlobalSection(SolutionProperties) = preSolution 48 | HideSolutionNode = FALSE 49 | EndGlobalSection 50 | GlobalSection(NestedProjects) = preSolution 51 | {C653410E-8323-4FE3-AE97-647A2145428D} = {65F6BE4B-1C20-49C5-A584-421822688D16} 52 | {54108848-E0C9-408A-852B-CA1623CD9938} = {65F6BE4B-1C20-49C5-A584-421822688D16} 53 | {C8EAD560-92C8-4FE4-A603-EAC8EA4D933C} = {9E7843EF-8843-4436-B0C2-B148B347910A} 54 | EndGlobalSection 55 | GlobalSection(ExtensibilityGlobals) = postSolution 56 | SolutionGuid = {109BF1EC-A0F1-41AC-9DB7-8BCCC3694A5C} 57 | EndGlobalSection 58 | EndGlobal 59 | -------------------------------------------------------------------------------- /CleanBlazorWeb/CleanBlazorWeb.Client/CleanBlazorWeb.Client.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | true 8 | Default 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /CleanBlazorWeb/CleanBlazorWeb.Client/Pages/Auth.razor: -------------------------------------------------------------------------------- 1 | @page "/auth" 2 | 3 | @using Microsoft.AspNetCore.Authorization 4 | 5 | @attribute [Authorize] 6 | @rendermode InteractiveAuto 7 | 8 | Auth 9 | 10 |

You are authenticated

11 | 12 | 13 | Hello @context.User.Identity?.Name! 14 | 15 | -------------------------------------------------------------------------------- /CleanBlazorWeb/CleanBlazorWeb.Client/Pages/Counter.razor: -------------------------------------------------------------------------------- 1 | @page "/counter" 2 | @rendermode InteractiveAuto 3 | 4 | Counter 5 | 6 |

Counter

7 | 8 |

Current count: @currentCount

9 | 10 | 11 | 12 | @code { 13 | private int currentCount = 0; 14 | 15 | private void IncrementCount() 16 | { 17 | currentCount++; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /CleanBlazorWeb/CleanBlazorWeb.Client/PersistentAuthenticationStateProvider.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Components; 2 | using Microsoft.AspNetCore.Components.Authorization; 3 | using System.Security.Claims; 4 | 5 | namespace CleanBlazorWeb.Client 6 | { 7 | // This is a client-side AuthenticationStateProvider that determines the user's authentication state by 8 | // looking for data persisted in the page when it was rendered on the server. This authentication state will 9 | // be fixed for the lifetime of the WebAssembly application. So, if the user needs to log in or out, a full 10 | // page reload is required. 11 | // 12 | // This only provides a user name and email for display purposes. It does not actually include any tokens 13 | // that authenticate to the server when making subsequent requests. That works separately using a 14 | // cookie that will be included on HttpClient requests to the server. 15 | internal class PersistentAuthenticationStateProvider : AuthenticationStateProvider 16 | { 17 | private static readonly Task defaultUnauthenticatedTask = 18 | Task.FromResult(new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()))); 19 | 20 | private readonly Task authenticationStateTask = defaultUnauthenticatedTask; 21 | 22 | public PersistentAuthenticationStateProvider(PersistentComponentState state) 23 | { 24 | if (!state.TryTakeFromJson(nameof(UserInfo), out var userInfo) || userInfo is null) 25 | { 26 | return; 27 | } 28 | 29 | Claim[] claims = [ 30 | new Claim(ClaimTypes.NameIdentifier, userInfo.UserId), 31 | new Claim(ClaimTypes.Name, userInfo.Email), 32 | new Claim(ClaimTypes.Email, userInfo.Email)]; 33 | 34 | authenticationStateTask = Task.FromResult( 35 | new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity(claims, 36 | authenticationType: nameof(PersistentAuthenticationStateProvider))))); 37 | } 38 | 39 | public override Task GetAuthenticationStateAsync() => authenticationStateTask; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /CleanBlazorWeb/CleanBlazorWeb.Client/Program.cs: -------------------------------------------------------------------------------- 1 | using CleanBlazorWeb.Client; 2 | using Microsoft.AspNetCore.Components.Authorization; 3 | using Microsoft.AspNetCore.Components.WebAssembly.Hosting; 4 | 5 | namespace CleanBlazorWeb.Client 6 | { 7 | internal class Program 8 | { 9 | static async Task Main(string[] args) 10 | { 11 | var builder = WebAssemblyHostBuilder.CreateDefault(args); 12 | 13 | builder.Services.AddAuthorizationCore(); 14 | builder.Services.AddCascadingAuthenticationState(); 15 | builder.Services.AddSingleton(); 16 | 17 | await builder.Build().RunAsync(); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /CleanBlazorWeb/CleanBlazorWeb.Client/RedirectToLogin.razor: -------------------------------------------------------------------------------- 1 | @inject NavigationManager NavigationManager 2 | 3 | @code { 4 | protected override void OnInitialized() 5 | { 6 | NavigationManager.NavigateTo($"Account/Login?returnUrl={Uri.EscapeDataString(NavigationManager.Uri)}", forceLoad: true); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /CleanBlazorWeb/CleanBlazorWeb.Client/UserInfo.cs: -------------------------------------------------------------------------------- 1 | namespace CleanBlazorWeb.Client 2 | { 3 | // Add properties to this class and update the server and client AuthenticationStateProviders 4 | // to expose more information about the authenticated user to the client. 5 | public class UserInfo 6 | { 7 | public required string UserId { get; set; } 8 | public required string Email { get; set; } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /CleanBlazorWeb/CleanBlazorWeb.Client/_Imports.razor: -------------------------------------------------------------------------------- 1 | @using System.Net.Http 2 | @using System.Net.Http.Json 3 | @using Microsoft.AspNetCore.Components.Authorization 4 | @using Microsoft.AspNetCore.Components.Forms 5 | @using Microsoft.AspNetCore.Components.Routing 6 | @using Microsoft.AspNetCore.Components.Web 7 | @using static Microsoft.AspNetCore.Components.Web.RenderMode 8 | @using Microsoft.AspNetCore.Components.Web.Virtualization 9 | @using Microsoft.JSInterop 10 | @using CleanBlazorWeb.Client 11 | -------------------------------------------------------------------------------- /CleanBlazorWeb/CleanBlazorWeb.Client/wwwroot/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /CleanBlazorWeb/CleanBlazorWeb.Client/wwwroot/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /CleanBlazorWeb/CleanBlazorWeb/CleanBlazorWeb.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | aspnet-CleanBlazorWeb-982fc812-1868-4151-8b78-d2a30abf3a8c 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /CleanBlazorWeb/CleanBlazorWeb/Components/Account/IdentityComponentsEndpointRouteBuilderExtensions.cs: -------------------------------------------------------------------------------- 1 | using CleanBlazorWeb.Components.Account.Pages; 2 | using CleanBlazorWeb.Components.Account.Pages.Manage; 3 | using CleanBlazorWeb.Data; 4 | using Microsoft.AspNetCore.Authentication; 5 | using Microsoft.AspNetCore.Components.Authorization; 6 | using Microsoft.AspNetCore.Http.Extensions; 7 | using Microsoft.AspNetCore.Identity; 8 | using Microsoft.AspNetCore.Mvc; 9 | using Microsoft.Extensions.Primitives; 10 | using System.Security.Claims; 11 | using System.Text.Json; 12 | 13 | namespace Microsoft.AspNetCore.Routing 14 | { 15 | internal static class IdentityComponentsEndpointRouteBuilderExtensions 16 | { 17 | // These endpoints are required by the Identity Razor components defined in the /Components/Account/Pages directory of this project. 18 | public static IEndpointConventionBuilder MapAdditionalIdentityEndpoints(this IEndpointRouteBuilder endpoints) 19 | { 20 | ArgumentNullException.ThrowIfNull(endpoints); 21 | 22 | var accountGroup = endpoints.MapGroup("/Account"); 23 | 24 | accountGroup.MapPost("/PerformExternalLogin", ( 25 | HttpContext context, 26 | [FromServices] SignInManager signInManager, 27 | [FromForm] string provider, 28 | [FromForm] string returnUrl) => 29 | { 30 | IEnumerable> query = [ 31 | new("ReturnUrl", returnUrl), 32 | new("Action", ExternalLogin.LoginCallbackAction)]; 33 | 34 | var redirectUrl = UriHelper.BuildRelative( 35 | context.Request.PathBase, 36 | "/Account/ExternalLogin", 37 | QueryString.Create(query)); 38 | 39 | var properties = signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl); 40 | return TypedResults.Challenge(properties, [provider]); 41 | }); 42 | 43 | accountGroup.MapPost("/Logout", async ( 44 | ClaimsPrincipal user, 45 | SignInManager signInManager, 46 | [FromForm] string returnUrl) => 47 | { 48 | await signInManager.SignOutAsync(); 49 | return TypedResults.LocalRedirect($"~/{returnUrl}"); 50 | }); 51 | 52 | var manageGroup = accountGroup.MapGroup("/Manage").RequireAuthorization(); 53 | 54 | manageGroup.MapPost("/LinkExternalLogin", async ( 55 | HttpContext context, 56 | [FromServices] SignInManager signInManager, 57 | [FromForm] string provider) => 58 | { 59 | // Clear the existing external cookie to ensure a clean login process 60 | await context.SignOutAsync(IdentityConstants.ExternalScheme); 61 | 62 | var redirectUrl = UriHelper.BuildRelative( 63 | context.Request.PathBase, 64 | "/Account/Manage/ExternalLogins", 65 | QueryString.Create("Action", ExternalLogins.LinkLoginCallbackAction)); 66 | 67 | var properties = signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl, signInManager.UserManager.GetUserId(context.User)); 68 | return TypedResults.Challenge(properties, [provider]); 69 | }); 70 | 71 | var loggerFactory = endpoints.ServiceProvider.GetRequiredService(); 72 | var downloadLogger = loggerFactory.CreateLogger("DownloadPersonalData"); 73 | 74 | manageGroup.MapPost("/DownloadPersonalData", async ( 75 | HttpContext context, 76 | [FromServices] UserManager userManager, 77 | [FromServices] AuthenticationStateProvider authenticationStateProvider) => 78 | { 79 | var user = await userManager.GetUserAsync(context.User); 80 | if (user is null) 81 | { 82 | return Results.NotFound($"Unable to load user with ID '{userManager.GetUserId(context.User)}'."); 83 | } 84 | 85 | var userId = await userManager.GetUserIdAsync(user); 86 | downloadLogger.LogInformation("User with ID '{UserId}' asked for their personal data.", userId); 87 | 88 | // Only include personal data for download 89 | var personalData = new Dictionary(); 90 | var personalDataProps = typeof(ApplicationUser).GetProperties().Where( 91 | prop => Attribute.IsDefined(prop, typeof(PersonalDataAttribute))); 92 | foreach (var p in personalDataProps) 93 | { 94 | personalData.Add(p.Name, p.GetValue(user)?.ToString() ?? "null"); 95 | } 96 | 97 | var logins = await userManager.GetLoginsAsync(user); 98 | foreach (var l in logins) 99 | { 100 | personalData.Add($"{l.LoginProvider} external login provider key", l.ProviderKey); 101 | } 102 | 103 | personalData.Add("Authenticator Key", (await userManager.GetAuthenticatorKeyAsync(user))!); 104 | var fileBytes = JsonSerializer.SerializeToUtf8Bytes(personalData); 105 | 106 | context.Response.Headers.TryAdd("Content-Disposition", "attachment; filename=PersonalData.json"); 107 | return TypedResults.File(fileBytes, contentType: "application/json", fileDownloadName: "PersonalData.json"); 108 | }); 109 | 110 | return accountGroup; 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /CleanBlazorWeb/CleanBlazorWeb/Components/Account/IdentityNoOpEmailSender.cs: -------------------------------------------------------------------------------- 1 | using CleanBlazorWeb.Data; 2 | using Microsoft.AspNetCore.Identity; 3 | using Microsoft.AspNetCore.Identity.UI.Services; 4 | 5 | namespace CleanBlazorWeb.Components.Account 6 | { 7 | // Remove the "else if (EmailSender is IdentityNoOpEmailSender)" block from RegisterConfirmation.razor after updating with a real implementation. 8 | internal sealed class IdentityNoOpEmailSender : IEmailSender 9 | { 10 | private readonly IEmailSender emailSender = new NoOpEmailSender(); 11 | 12 | public Task SendConfirmationLinkAsync(ApplicationUser user, string email, string confirmationLink) => 13 | emailSender.SendEmailAsync(email, "Confirm your email", $"Please confirm your account by clicking here."); 14 | 15 | public Task SendPasswordResetLinkAsync(ApplicationUser user, string email, string resetLink) => 16 | emailSender.SendEmailAsync(email, "Reset your password", $"Please reset your password by clicking here."); 17 | 18 | public Task SendPasswordResetCodeAsync(ApplicationUser user, string email, string resetCode) => 19 | emailSender.SendEmailAsync(email, "Reset your password", $"Please reset your password using the following code: {resetCode}"); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /CleanBlazorWeb/CleanBlazorWeb/Components/Account/IdentityRedirectManager.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Components; 2 | using System.Diagnostics.CodeAnalysis; 3 | 4 | namespace CleanBlazorWeb.Components.Account 5 | { 6 | internal sealed class IdentityRedirectManager(NavigationManager navigationManager) 7 | { 8 | public const string StatusCookieName = "Identity.StatusMessage"; 9 | 10 | private static readonly CookieBuilder StatusCookieBuilder = new() 11 | { 12 | SameSite = SameSiteMode.Strict, 13 | HttpOnly = true, 14 | IsEssential = true, 15 | MaxAge = TimeSpan.FromSeconds(5), 16 | }; 17 | 18 | [DoesNotReturn] 19 | public void RedirectTo(string? uri) 20 | { 21 | uri ??= ""; 22 | 23 | // Prevent open redirects. 24 | if (!Uri.IsWellFormedUriString(uri, UriKind.Relative)) 25 | { 26 | uri = navigationManager.ToBaseRelativePath(uri); 27 | } 28 | 29 | // During static rendering, NavigateTo throws a NavigationException which is handled by the framework as a redirect. 30 | // So as long as this is called from a statically rendered Identity component, the InvalidOperationException is never thrown. 31 | navigationManager.NavigateTo(uri); 32 | throw new InvalidOperationException($"{nameof(IdentityRedirectManager)} can only be used during static rendering."); 33 | } 34 | 35 | [DoesNotReturn] 36 | public void RedirectTo(string uri, Dictionary queryParameters) 37 | { 38 | var uriWithoutQuery = navigationManager.ToAbsoluteUri(uri).GetLeftPart(UriPartial.Path); 39 | var newUri = navigationManager.GetUriWithQueryParameters(uriWithoutQuery, queryParameters); 40 | RedirectTo(newUri); 41 | } 42 | 43 | [DoesNotReturn] 44 | public void RedirectToWithStatus(string uri, string message, HttpContext context) 45 | { 46 | context.Response.Cookies.Append(StatusCookieName, message, StatusCookieBuilder.Build(context)); 47 | RedirectTo(uri); 48 | } 49 | 50 | private string CurrentPath => navigationManager.ToAbsoluteUri(navigationManager.Uri).GetLeftPart(UriPartial.Path); 51 | 52 | [DoesNotReturn] 53 | public void RedirectToCurrentPage() => RedirectTo(CurrentPath); 54 | 55 | [DoesNotReturn] 56 | public void RedirectToCurrentPageWithStatus(string message, HttpContext context) 57 | => RedirectToWithStatus(CurrentPath, message, context); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /CleanBlazorWeb/CleanBlazorWeb/Components/Account/IdentityUserAccessor.cs: -------------------------------------------------------------------------------- 1 | using CleanBlazorWeb.Data; 2 | using Microsoft.AspNetCore.Identity; 3 | 4 | namespace CleanBlazorWeb.Components.Account 5 | { 6 | internal sealed class IdentityUserAccessor(UserManager userManager, IdentityRedirectManager redirectManager) 7 | { 8 | public async Task GetRequiredUserAsync(HttpContext context) 9 | { 10 | var user = await userManager.GetUserAsync(context.User); 11 | 12 | if (user is null) 13 | { 14 | redirectManager.RedirectToWithStatus("Account/InvalidUser", $"Error: Unable to load user with ID '{userManager.GetUserId(context.User)}'.", context); 15 | } 16 | 17 | return user; 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /CleanBlazorWeb/CleanBlazorWeb/Components/Account/Pages/AccessDenied.razor: -------------------------------------------------------------------------------- 1 | @page "/Account/AccessDenied" 2 | 3 | Access denied 4 | 5 |
6 |

Access denied

7 |

You do not have access to this resource.

8 |
9 | -------------------------------------------------------------------------------- /CleanBlazorWeb/CleanBlazorWeb/Components/Account/Pages/ConfirmEmail.razor: -------------------------------------------------------------------------------- 1 | @page "/Account/ConfirmEmail" 2 | 3 | @using System.Text 4 | @using Microsoft.AspNetCore.Identity 5 | @using Microsoft.AspNetCore.WebUtilities 6 | @using CleanBlazorWeb.Data 7 | 8 | @inject UserManager UserManager 9 | @inject IdentityRedirectManager RedirectManager 10 | 11 | Confirm email 12 | 13 |

Confirm email

14 | 15 | 16 | @code { 17 | private string? statusMessage; 18 | 19 | [CascadingParameter] 20 | private HttpContext HttpContext { get; set; } = default!; 21 | 22 | [SupplyParameterFromQuery] 23 | private string? UserId { get; set; } 24 | 25 | [SupplyParameterFromQuery] 26 | private string? Code { get; set; } 27 | 28 | protected override async Task OnInitializedAsync() 29 | { 30 | if (UserId is null || Code is null) 31 | { 32 | RedirectManager.RedirectTo(""); 33 | } 34 | 35 | var user = await UserManager.FindByIdAsync(UserId); 36 | if (user is null) 37 | { 38 | HttpContext.Response.StatusCode = StatusCodes.Status404NotFound; 39 | statusMessage = $"Error loading user with ID {UserId}"; 40 | } 41 | else 42 | { 43 | var code = Encoding.UTF8.GetString(WebEncoders.Base64UrlDecode(Code)); 44 | var result = await UserManager.ConfirmEmailAsync(user, code); 45 | statusMessage = result.Succeeded ? "Thank you for confirming your email." : "Error confirming your email."; 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /CleanBlazorWeb/CleanBlazorWeb/Components/Account/Pages/ConfirmEmailChange.razor: -------------------------------------------------------------------------------- 1 | @page "/Account/ConfirmEmailChange" 2 | 3 | @using System.Text 4 | @using Microsoft.AspNetCore.Identity 5 | @using Microsoft.AspNetCore.WebUtilities 6 | @using CleanBlazorWeb.Data 7 | 8 | @inject UserManager UserManager 9 | @inject SignInManager SignInManager 10 | @inject IdentityRedirectManager RedirectManager 11 | 12 | Confirm email change 13 | 14 |

Confirm email change

15 | 16 | 17 | 18 | @code { 19 | private string? message; 20 | 21 | [CascadingParameter] 22 | private HttpContext HttpContext { get; set; } = default!; 23 | 24 | [SupplyParameterFromQuery] 25 | private string? UserId { get; set; } 26 | 27 | [SupplyParameterFromQuery] 28 | private string? Email { get; set; } 29 | 30 | [SupplyParameterFromQuery] 31 | private string? Code { get; set; } 32 | 33 | protected override async Task OnInitializedAsync() 34 | { 35 | if (UserId is null || Email is null || Code is null) 36 | { 37 | RedirectManager.RedirectToWithStatus( 38 | "Account/Login", "Error: Invalid email change confirmation link.", HttpContext); 39 | } 40 | 41 | var user = await UserManager.FindByIdAsync(UserId); 42 | if (user is null) 43 | { 44 | message = "Unable to find user with Id '{userId}'"; 45 | return; 46 | } 47 | 48 | var code = Encoding.UTF8.GetString(WebEncoders.Base64UrlDecode(Code)); 49 | var result = await UserManager.ChangeEmailAsync(user, Email, code); 50 | if (!result.Succeeded) 51 | { 52 | message = "Error changing email."; 53 | return; 54 | } 55 | 56 | // In our UI email and user name are one and the same, so when we update the email 57 | // we need to update the user name. 58 | var setUserNameResult = await UserManager.SetUserNameAsync(user, Email); 59 | if (!setUserNameResult.Succeeded) 60 | { 61 | message = "Error changing user name."; 62 | return; 63 | } 64 | 65 | await SignInManager.RefreshSignInAsync(user); 66 | message = "Thank you for confirming your email change."; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /CleanBlazorWeb/CleanBlazorWeb/Components/Account/Pages/ExternalLogin.razor: -------------------------------------------------------------------------------- 1 | @page "/Account/ExternalLogin" 2 | 3 | @using System.ComponentModel.DataAnnotations 4 | @using System.Security.Claims 5 | @using System.Text 6 | @using System.Text.Encodings.Web 7 | @using Microsoft.AspNetCore.Identity 8 | @using Microsoft.AspNetCore.WebUtilities 9 | @using CleanBlazorWeb.Data 10 | 11 | @inject SignInManager SignInManager 12 | @inject UserManager UserManager 13 | @inject IUserStore UserStore 14 | @inject IEmailSender EmailSender 15 | @inject NavigationManager NavigationManager 16 | @inject IdentityRedirectManager RedirectManager 17 | @inject ILogger Logger 18 | 19 | Register 20 | 21 | 22 |

Register

23 |

Associate your @ProviderDisplayName account.

24 |
25 | 26 |
27 | You've successfully authenticated with @ProviderDisplayName. 28 | Please enter an email address for this site below and click the Register button to finish 29 | logging in. 30 |
31 | 32 |
33 |
34 | 35 | 36 | 37 |
38 | 39 | 40 | 41 |
42 | 43 |
44 |
45 |
46 | 47 | @code { 48 | public const string LoginCallbackAction = "LoginCallback"; 49 | 50 | private string? message; 51 | private ExternalLoginInfo externalLoginInfo = default!; 52 | 53 | [CascadingParameter] 54 | private HttpContext HttpContext { get; set; } = default!; 55 | 56 | [SupplyParameterFromForm] 57 | private InputModel Input { get; set; } = new(); 58 | 59 | [SupplyParameterFromQuery] 60 | private string? RemoteError { get; set; } 61 | 62 | [SupplyParameterFromQuery] 63 | private string? ReturnUrl { get; set; } 64 | 65 | [SupplyParameterFromQuery] 66 | private string? Action { get; set; } 67 | 68 | private string? ProviderDisplayName => externalLoginInfo.ProviderDisplayName; 69 | 70 | protected override async Task OnInitializedAsync() 71 | { 72 | if (RemoteError is not null) 73 | { 74 | RedirectManager.RedirectToWithStatus("Account/Login", $"Error from external provider: {RemoteError}", HttpContext); 75 | } 76 | 77 | var info = await SignInManager.GetExternalLoginInfoAsync(); 78 | if (info is null) 79 | { 80 | RedirectManager.RedirectToWithStatus("Account/Login", "Error loading external login information.", HttpContext); 81 | } 82 | 83 | externalLoginInfo = info; 84 | 85 | if (HttpMethods.IsGet(HttpContext.Request.Method)) 86 | { 87 | if (Action == LoginCallbackAction) 88 | { 89 | await OnLoginCallbackAsync(); 90 | return; 91 | } 92 | 93 | // We should only reach this page via the login callback, so redirect back to 94 | // the login page if we get here some other way. 95 | RedirectManager.RedirectTo("Account/Login"); 96 | } 97 | } 98 | 99 | private async Task OnLoginCallbackAsync() 100 | { 101 | // Sign in the user with this external login provider if the user already has a login. 102 | var result = await SignInManager.ExternalLoginSignInAsync( 103 | externalLoginInfo.LoginProvider, 104 | externalLoginInfo.ProviderKey, 105 | isPersistent: false, 106 | bypassTwoFactor: true); 107 | 108 | if (result.Succeeded) 109 | { 110 | Logger.LogInformation( 111 | "{Name} logged in with {LoginProvider} provider.", 112 | externalLoginInfo.Principal.Identity?.Name, 113 | externalLoginInfo.LoginProvider); 114 | RedirectManager.RedirectTo(ReturnUrl); 115 | } 116 | else if (result.IsLockedOut) 117 | { 118 | RedirectManager.RedirectTo("Account/Lockout"); 119 | } 120 | 121 | // If the user does not have an account, then ask the user to create an account. 122 | if (externalLoginInfo.Principal.HasClaim(c => c.Type == ClaimTypes.Email)) 123 | { 124 | Input.Email = externalLoginInfo.Principal.FindFirstValue(ClaimTypes.Email) ?? ""; 125 | } 126 | } 127 | 128 | private async Task OnValidSubmitAsync() 129 | { 130 | var emailStore = GetEmailStore(); 131 | var user = CreateUser(); 132 | 133 | await UserStore.SetUserNameAsync(user, Input.Email, CancellationToken.None); 134 | await emailStore.SetEmailAsync(user, Input.Email, CancellationToken.None); 135 | 136 | var result = await UserManager.CreateAsync(user); 137 | if (result.Succeeded) 138 | { 139 | result = await UserManager.AddLoginAsync(user, externalLoginInfo); 140 | if (result.Succeeded) 141 | { 142 | Logger.LogInformation("User created an account using {Name} provider.", externalLoginInfo.LoginProvider); 143 | 144 | var userId = await UserManager.GetUserIdAsync(user); 145 | var code = await UserManager.GenerateEmailConfirmationTokenAsync(user); 146 | code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code)); 147 | 148 | var callbackUrl = NavigationManager.GetUriWithQueryParameters( 149 | NavigationManager.ToAbsoluteUri("Account/ConfirmEmail").AbsoluteUri, 150 | new Dictionary { ["userId"] = userId, ["code"] = code }); 151 | await EmailSender.SendConfirmationLinkAsync(user, Input.Email, HtmlEncoder.Default.Encode(callbackUrl)); 152 | 153 | // If account confirmation is required, we need to show the link if we don't have a real email sender 154 | if (UserManager.Options.SignIn.RequireConfirmedAccount) 155 | { 156 | RedirectManager.RedirectTo("Account/RegisterConfirmation", new() { ["email"] = Input.Email }); 157 | } 158 | 159 | await SignInManager.SignInAsync(user, isPersistent: false, externalLoginInfo.LoginProvider); 160 | RedirectManager.RedirectTo(ReturnUrl); 161 | } 162 | } 163 | 164 | message = $"Error: {string.Join(",", result.Errors.Select(error => error.Description))}"; 165 | } 166 | 167 | private ApplicationUser CreateUser() 168 | { 169 | try 170 | { 171 | return Activator.CreateInstance(); 172 | } 173 | catch 174 | { 175 | throw new InvalidOperationException($"Can't create an instance of '{nameof(ApplicationUser)}'. " + 176 | $"Ensure that '{nameof(ApplicationUser)}' is not an abstract class and has a parameterless constructor"); 177 | } 178 | } 179 | 180 | private IUserEmailStore GetEmailStore() 181 | { 182 | if (!UserManager.SupportsUserEmail) 183 | { 184 | throw new NotSupportedException("The default UI requires a user store with email support."); 185 | } 186 | return (IUserEmailStore)UserStore; 187 | } 188 | 189 | private sealed class InputModel 190 | { 191 | [Required] 192 | [EmailAddress] 193 | public string Email { get; set; } = ""; 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /CleanBlazorWeb/CleanBlazorWeb/Components/Account/Pages/ForgotPassword.razor: -------------------------------------------------------------------------------- 1 | @page "/Account/ForgotPassword" 2 | 3 | @using System.ComponentModel.DataAnnotations 4 | @using System.Text 5 | @using System.Text.Encodings.Web 6 | @using Microsoft.AspNetCore.Identity 7 | @using Microsoft.AspNetCore.WebUtilities 8 | @using CleanBlazorWeb.Data 9 | 10 | @inject UserManager UserManager 11 | @inject IEmailSender EmailSender 12 | @inject NavigationManager NavigationManager 13 | @inject IdentityRedirectManager RedirectManager 14 | 15 | Forgot your password? 16 | 17 |

Forgot your password?

18 |

Enter your email.

19 |
20 |
21 |
22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 |
31 | 32 |
33 |
34 |
35 | 36 | @code { 37 | [SupplyParameterFromForm] 38 | private InputModel Input { get; set; } = new(); 39 | 40 | private async Task OnValidSubmitAsync() 41 | { 42 | var user = await UserManager.FindByEmailAsync(Input.Email); 43 | if (user is null || !(await UserManager.IsEmailConfirmedAsync(user))) 44 | { 45 | // Don't reveal that the user does not exist or is not confirmed 46 | RedirectManager.RedirectTo("Account/ForgotPasswordConfirmation"); 47 | } 48 | 49 | // For more information on how to enable account confirmation and password reset please 50 | // visit https://go.microsoft.com/fwlink/?LinkID=532713 51 | var code = await UserManager.GeneratePasswordResetTokenAsync(user); 52 | code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code)); 53 | var callbackUrl = NavigationManager.GetUriWithQueryParameters( 54 | NavigationManager.ToAbsoluteUri("Account/ResetPassword").AbsoluteUri, 55 | new Dictionary { ["code"] = code }); 56 | 57 | await EmailSender.SendPasswordResetLinkAsync(user, Input.Email, HtmlEncoder.Default.Encode(callbackUrl)); 58 | 59 | RedirectManager.RedirectTo("Account/ForgotPasswordConfirmation"); 60 | } 61 | 62 | private sealed class InputModel 63 | { 64 | [Required] 65 | [EmailAddress] 66 | public string Email { get; set; } = ""; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /CleanBlazorWeb/CleanBlazorWeb/Components/Account/Pages/ForgotPasswordConfirmation.razor: -------------------------------------------------------------------------------- 1 | @page "/Account/ForgotPasswordConfirmation" 2 | 3 | Forgot password confirmation 4 | 5 |

Forgot password confirmation

6 |

7 | Please check your email to reset your password. 8 |

9 | -------------------------------------------------------------------------------- /CleanBlazorWeb/CleanBlazorWeb/Components/Account/Pages/InvalidPasswordReset.razor: -------------------------------------------------------------------------------- 1 | @page "/Account/InvalidPasswordReset" 2 | 3 | Invalid password reset 4 | 5 |

Invalid password reset

6 |

7 | The password reset link is invalid. 8 |

9 | -------------------------------------------------------------------------------- /CleanBlazorWeb/CleanBlazorWeb/Components/Account/Pages/InvalidUser.razor: -------------------------------------------------------------------------------- 1 | @page "/Account/InvalidUser" 2 | 3 | Invalid user 4 | 5 |

Invalid user

6 | 7 | 8 | -------------------------------------------------------------------------------- /CleanBlazorWeb/CleanBlazorWeb/Components/Account/Pages/Lockout.razor: -------------------------------------------------------------------------------- 1 | @page "/Account/Lockout" 2 | 3 | Locked out 4 | 5 |
6 |

Locked out

7 |

This account has been locked out, please try again later.

8 |
9 | -------------------------------------------------------------------------------- /CleanBlazorWeb/CleanBlazorWeb/Components/Account/Pages/Login.razor: -------------------------------------------------------------------------------- 1 | @page "/Account/Login" 2 | 3 | @using System.ComponentModel.DataAnnotations 4 | @using Microsoft.AspNetCore.Authentication 5 | @using Microsoft.AspNetCore.Identity 6 | @using CleanBlazorWeb.Data 7 | 8 | @inject SignInManager SignInManager 9 | @inject ILogger Logger 10 | @inject NavigationManager NavigationManager 11 | @inject IdentityRedirectManager RedirectManager 12 | 13 | Log in 14 | 15 |

Log in

16 |
17 |
18 |
19 | 20 | 21 | 22 |

Use a local account to log in.

23 |
24 | 25 |
26 | 27 | 28 | 29 |
30 |
31 | 32 | 33 | 34 |
35 |
36 | 40 |
41 |
42 | 43 |
44 | 55 |
56 |
57 |
58 |
59 |
60 |

Use another service to log in.

61 |
62 | 63 |
64 |
65 |
66 | 67 | @code { 68 | private string? errorMessage; 69 | 70 | [CascadingParameter] 71 | private HttpContext HttpContext { get; set; } = default!; 72 | 73 | [SupplyParameterFromForm] 74 | private InputModel Input { get; set; } = new(); 75 | 76 | [SupplyParameterFromQuery] 77 | private string? ReturnUrl { get; set; } 78 | 79 | protected override async Task OnInitializedAsync() 80 | { 81 | if (HttpMethods.IsGet(HttpContext.Request.Method)) 82 | { 83 | // Clear the existing external cookie to ensure a clean login process 84 | await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme); 85 | } 86 | } 87 | 88 | public async Task LoginUser() 89 | { 90 | // This doesn't count login failures towards account lockout 91 | // To enable password failures to trigger account lockout, set lockoutOnFailure: true 92 | var result = await SignInManager.PasswordSignInAsync(Input.Email, Input.Password, Input.RememberMe, lockoutOnFailure: false); 93 | if (result.Succeeded) 94 | { 95 | Logger.LogInformation("User logged in."); 96 | RedirectManager.RedirectTo(ReturnUrl); 97 | } 98 | else if (result.RequiresTwoFactor) 99 | { 100 | RedirectManager.RedirectTo( 101 | "Account/LoginWith2fa", 102 | new() { ["returnUrl"] = ReturnUrl, ["rememberMe"] = Input.RememberMe }); 103 | } 104 | else if (result.IsLockedOut) 105 | { 106 | Logger.LogWarning("User account locked out."); 107 | RedirectManager.RedirectTo("Account/Lockout"); 108 | } 109 | else 110 | { 111 | errorMessage = "Error: Invalid login attempt."; 112 | } 113 | } 114 | 115 | private sealed class InputModel 116 | { 117 | [Required] 118 | [EmailAddress] 119 | public string Email { get; set; } = ""; 120 | 121 | [Required] 122 | [DataType(DataType.Password)] 123 | public string Password { get; set; } = ""; 124 | 125 | [Display(Name = "Remember me?")] 126 | public bool RememberMe { get; set; } 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /CleanBlazorWeb/CleanBlazorWeb/Components/Account/Pages/LoginWith2fa.razor: -------------------------------------------------------------------------------- 1 | @page "/Account/LoginWith2fa" 2 | 3 | @using System.ComponentModel.DataAnnotations 4 | @using Microsoft.AspNetCore.Identity 5 | @using CleanBlazorWeb.Data 6 | 7 | @inject SignInManager SignInManager 8 | @inject UserManager UserManager 9 | @inject IdentityRedirectManager RedirectManager 10 | @inject ILogger Logger 11 | 12 | Two-factor authentication 13 | 14 |

Two-factor authentication

15 |
16 | 17 |

Your login is protected with an authenticator app. Enter your authenticator code below.

18 |
19 |
20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 |
30 |
31 | 35 |
36 |
37 | 38 |
39 |
40 |
41 |
42 |

43 | Don't have access to your authenticator device? You can 44 | log in with a recovery code. 45 |

46 | 47 | @code { 48 | private string? message; 49 | private ApplicationUser user = default!; 50 | 51 | [SupplyParameterFromForm] 52 | private InputModel Input { get; set; } = new(); 53 | 54 | [SupplyParameterFromQuery] 55 | private string? ReturnUrl { get; set; } 56 | 57 | [SupplyParameterFromQuery] 58 | private bool RememberMe { get; set; } 59 | 60 | protected override async Task OnInitializedAsync() 61 | { 62 | // Ensure the user has gone through the username & password screen first 63 | user = await SignInManager.GetTwoFactorAuthenticationUserAsync() ?? 64 | throw new InvalidOperationException("Unable to load two-factor authentication user."); 65 | } 66 | 67 | private async Task OnValidSubmitAsync() 68 | { 69 | var authenticatorCode = Input.TwoFactorCode!.Replace(" ", string.Empty).Replace("-", string.Empty); 70 | var result = await SignInManager.TwoFactorAuthenticatorSignInAsync(authenticatorCode, RememberMe, Input.RememberMachine); 71 | var userId = await UserManager.GetUserIdAsync(user); 72 | 73 | if (result.Succeeded) 74 | { 75 | Logger.LogInformation("User with ID '{UserId}' logged in with 2fa.", userId); 76 | RedirectManager.RedirectTo(ReturnUrl); 77 | } 78 | else if (result.IsLockedOut) 79 | { 80 | Logger.LogWarning("User with ID '{UserId}' account locked out.", userId); 81 | RedirectManager.RedirectTo("Account/Lockout"); 82 | } 83 | else 84 | { 85 | Logger.LogWarning("Invalid authenticator code entered for user with ID '{UserId}'.", userId); 86 | message = "Error: Invalid authenticator code."; 87 | } 88 | } 89 | 90 | private sealed class InputModel 91 | { 92 | [Required] 93 | [StringLength(7, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)] 94 | [DataType(DataType.Text)] 95 | [Display(Name = "Authenticator code")] 96 | public string? TwoFactorCode { get; set; } 97 | 98 | [Display(Name = "Remember this machine")] 99 | public bool RememberMachine { get; set; } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /CleanBlazorWeb/CleanBlazorWeb/Components/Account/Pages/LoginWithRecoveryCode.razor: -------------------------------------------------------------------------------- 1 | @page "/Account/LoginWithRecoveryCode" 2 | 3 | @using System.ComponentModel.DataAnnotations 4 | @using Microsoft.AspNetCore.Identity 5 | @using CleanBlazorWeb.Data 6 | 7 | @inject SignInManager SignInManager 8 | @inject UserManager UserManager 9 | @inject IdentityRedirectManager RedirectManager 10 | @inject ILogger Logger 11 | 12 | Recovery code verification 13 | 14 |

Recovery code verification

15 |
16 | 17 |

18 | You have requested to log in with a recovery code. This login will not be remembered until you provide 19 | an authenticator app code at log in or disable 2FA and log in again. 20 |

21 |
22 |
23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 |
31 | 32 |
33 |
34 |
35 | 36 | @code { 37 | private string? message; 38 | private ApplicationUser user = default!; 39 | 40 | [SupplyParameterFromForm] 41 | private InputModel Input { get; set; } = new(); 42 | 43 | [SupplyParameterFromQuery] 44 | private string? ReturnUrl { get; set; } 45 | 46 | protected override async Task OnInitializedAsync() 47 | { 48 | // Ensure the user has gone through the username & password screen first 49 | user = await SignInManager.GetTwoFactorAuthenticationUserAsync() ?? 50 | throw new InvalidOperationException("Unable to load two-factor authentication user."); 51 | } 52 | 53 | private async Task OnValidSubmitAsync() 54 | { 55 | var recoveryCode = Input.RecoveryCode.Replace(" ", string.Empty); 56 | 57 | var result = await SignInManager.TwoFactorRecoveryCodeSignInAsync(recoveryCode); 58 | 59 | var userId = await UserManager.GetUserIdAsync(user); 60 | 61 | if (result.Succeeded) 62 | { 63 | Logger.LogInformation("User with ID '{UserId}' logged in with a recovery code.", userId); 64 | RedirectManager.RedirectTo(ReturnUrl); 65 | } 66 | else if (result.IsLockedOut) 67 | { 68 | Logger.LogWarning("User account locked out."); 69 | RedirectManager.RedirectTo("Account/Lockout"); 70 | } 71 | else 72 | { 73 | Logger.LogWarning("Invalid recovery code entered for user with ID '{UserId}' ", userId); 74 | message = "Error: Invalid recovery code entered."; 75 | } 76 | } 77 | 78 | private sealed class InputModel 79 | { 80 | [Required] 81 | [DataType(DataType.Text)] 82 | [Display(Name = "Recovery Code")] 83 | public string RecoveryCode { get; set; } = ""; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /CleanBlazorWeb/CleanBlazorWeb/Components/Account/Pages/Manage/ChangePassword.razor: -------------------------------------------------------------------------------- 1 | @page "/Account/Manage/ChangePassword" 2 | 3 | @using System.ComponentModel.DataAnnotations 4 | @using Microsoft.AspNetCore.Identity 5 | @using CleanBlazorWeb.Data 6 | 7 | @inject UserManager UserManager 8 | @inject SignInManager SignInManager 9 | @inject IdentityUserAccessor UserAccessor 10 | @inject IdentityRedirectManager RedirectManager 11 | @inject ILogger Logger 12 | 13 | Change password 14 | 15 |

Change password

16 | 17 |
18 |
19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 |
27 |
28 | 29 | 30 | 31 |
32 |
33 | 34 | 35 | 36 |
37 | 38 |
39 |
40 |
41 | 42 | @code { 43 | private string? message; 44 | private ApplicationUser user = default!; 45 | private bool hasPassword; 46 | 47 | [CascadingParameter] 48 | private HttpContext HttpContext { get; set; } = default!; 49 | 50 | [SupplyParameterFromForm] 51 | private InputModel Input { get; set; } = new(); 52 | 53 | protected override async Task OnInitializedAsync() 54 | { 55 | user = await UserAccessor.GetRequiredUserAsync(HttpContext); 56 | hasPassword = await UserManager.HasPasswordAsync(user); 57 | if (!hasPassword) 58 | { 59 | RedirectManager.RedirectTo("Account/Manage/SetPassword"); 60 | } 61 | } 62 | 63 | private async Task OnValidSubmitAsync() 64 | { 65 | var changePasswordResult = await UserManager.ChangePasswordAsync(user, Input.OldPassword, Input.NewPassword); 66 | if (!changePasswordResult.Succeeded) 67 | { 68 | message = $"Error: {string.Join(",", changePasswordResult.Errors.Select(error => error.Description))}"; 69 | return; 70 | } 71 | 72 | await SignInManager.RefreshSignInAsync(user); 73 | Logger.LogInformation("User changed their password successfully."); 74 | 75 | RedirectManager.RedirectToCurrentPageWithStatus("Your password has been changed", HttpContext); 76 | } 77 | 78 | private sealed class InputModel 79 | { 80 | [Required] 81 | [DataType(DataType.Password)] 82 | [Display(Name = "Current password")] 83 | public string OldPassword { get; set; } = ""; 84 | 85 | [Required] 86 | [StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)] 87 | [DataType(DataType.Password)] 88 | [Display(Name = "New password")] 89 | public string NewPassword { get; set; } = ""; 90 | 91 | [DataType(DataType.Password)] 92 | [Display(Name = "Confirm new password")] 93 | [Compare("NewPassword", ErrorMessage = "The new password and confirmation password do not match.")] 94 | public string ConfirmPassword { get; set; } = ""; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /CleanBlazorWeb/CleanBlazorWeb/Components/Account/Pages/Manage/DeletePersonalData.razor: -------------------------------------------------------------------------------- 1 | @page "/Account/Manage/DeletePersonalData" 2 | 3 | @using System.ComponentModel.DataAnnotations 4 | @using Microsoft.AspNetCore.Identity 5 | @using CleanBlazorWeb.Data 6 | 7 | @inject UserManager UserManager 8 | @inject SignInManager SignInManager 9 | @inject IdentityUserAccessor UserAccessor 10 | @inject IdentityRedirectManager RedirectManager 11 | @inject ILogger Logger 12 | 13 | Delete Personal Data 14 | 15 | 16 | 17 |

Delete Personal Data

18 | 19 | 24 | 25 |
26 | 27 | 28 | 29 | @if (requirePassword) 30 | { 31 |
32 | 33 | 34 | 35 |
36 | } 37 | 38 |
39 |
40 | 41 | @code { 42 | private string? message; 43 | private ApplicationUser user = default!; 44 | private bool requirePassword; 45 | 46 | [CascadingParameter] 47 | private HttpContext HttpContext { get; set; } = default!; 48 | 49 | [SupplyParameterFromForm] 50 | private InputModel Input { get; set; } = new(); 51 | 52 | protected override async Task OnInitializedAsync() 53 | { 54 | Input ??= new(); 55 | user = await UserAccessor.GetRequiredUserAsync(HttpContext); 56 | requirePassword = await UserManager.HasPasswordAsync(user); 57 | } 58 | 59 | private async Task OnValidSubmitAsync() 60 | { 61 | if (requirePassword && !await UserManager.CheckPasswordAsync(user, Input.Password)) 62 | { 63 | message = "Error: Incorrect password."; 64 | return; 65 | } 66 | 67 | var result = await UserManager.DeleteAsync(user); 68 | if (!result.Succeeded) 69 | { 70 | throw new InvalidOperationException("Unexpected error occurred deleting user."); 71 | } 72 | 73 | await SignInManager.SignOutAsync(); 74 | 75 | var userId = await UserManager.GetUserIdAsync(user); 76 | Logger.LogInformation("User with ID '{UserId}' deleted themselves.", userId); 77 | 78 | RedirectManager.RedirectToCurrentPage(); 79 | } 80 | 81 | private sealed class InputModel 82 | { 83 | [DataType(DataType.Password)] 84 | public string Password { get; set; } = ""; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /CleanBlazorWeb/CleanBlazorWeb/Components/Account/Pages/Manage/Disable2fa.razor: -------------------------------------------------------------------------------- 1 | @page "/Account/Manage/Disable2fa" 2 | 3 | @using Microsoft.AspNetCore.Identity 4 | @using CleanBlazorWeb.Data 5 | 6 | @inject UserManager UserManager 7 | @inject IdentityUserAccessor UserAccessor 8 | @inject IdentityRedirectManager RedirectManager 9 | @inject ILogger Logger 10 | 11 | Disable two-factor authentication (2FA) 12 | 13 | 14 |

Disable two-factor authentication (2FA)

15 | 16 | 25 | 26 |
27 |
28 | 29 | 30 | 31 |
32 | 33 | @code { 34 | private ApplicationUser user = default!; 35 | 36 | [CascadingParameter] 37 | private HttpContext HttpContext { get; set; } = default!; 38 | 39 | protected override async Task OnInitializedAsync() 40 | { 41 | user = await UserAccessor.GetRequiredUserAsync(HttpContext); 42 | 43 | if (HttpMethods.IsGet(HttpContext.Request.Method) && !await UserManager.GetTwoFactorEnabledAsync(user)) 44 | { 45 | throw new InvalidOperationException("Cannot disable 2FA for user as it's not currently enabled."); 46 | } 47 | } 48 | 49 | private async Task OnSubmitAsync() 50 | { 51 | var disable2faResult = await UserManager.SetTwoFactorEnabledAsync(user, false); 52 | if (!disable2faResult.Succeeded) 53 | { 54 | throw new InvalidOperationException("Unexpected error occurred disabling 2FA."); 55 | } 56 | 57 | var userId = await UserManager.GetUserIdAsync(user); 58 | Logger.LogInformation("User with ID '{UserId}' has disabled 2fa.", userId); 59 | RedirectManager.RedirectToWithStatus( 60 | "Account/Manage/TwoFactorAuthentication", 61 | "2fa has been disabled. You can reenable 2fa when you setup an authenticator app", 62 | HttpContext); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /CleanBlazorWeb/CleanBlazorWeb/Components/Account/Pages/Manage/Email.razor: -------------------------------------------------------------------------------- 1 | @page "/Account/Manage/Email" 2 | 3 | @using System.ComponentModel.DataAnnotations 4 | @using System.Text 5 | @using System.Text.Encodings.Web 6 | @using Microsoft.AspNetCore.Identity 7 | @using Microsoft.AspNetCore.WebUtilities 8 | @using CleanBlazorWeb.Data 9 | 10 | @inject UserManager UserManager 11 | @inject IEmailSender EmailSender 12 | @inject IdentityUserAccessor UserAccessor 13 | @inject NavigationManager NavigationManager 14 | 15 | Manage email 16 | 17 |

Manage email

18 | 19 | 20 |
21 |
22 |
23 | 24 | 25 | 26 | 27 | 28 | @if (isEmailConfirmed) 29 | { 30 |
31 | 32 |
33 | 34 |
35 | 36 |
37 | } 38 | else 39 | { 40 |
41 | 42 | 43 | 44 |
45 | } 46 |
47 | 48 | 49 | 50 |
51 | 52 |
53 |
54 |
55 | 56 | @code { 57 | private string? message; 58 | private ApplicationUser user = default!; 59 | private string? email; 60 | private bool isEmailConfirmed; 61 | 62 | [CascadingParameter] 63 | private HttpContext HttpContext { get; set; } = default!; 64 | 65 | [SupplyParameterFromForm(FormName = "change-email")] 66 | private InputModel Input { get; set; } = new(); 67 | 68 | protected override async Task OnInitializedAsync() 69 | { 70 | user = await UserAccessor.GetRequiredUserAsync(HttpContext); 71 | email = await UserManager.GetEmailAsync(user); 72 | isEmailConfirmed = await UserManager.IsEmailConfirmedAsync(user); 73 | 74 | Input.NewEmail ??= email; 75 | } 76 | 77 | private async Task OnValidSubmitAsync() 78 | { 79 | if (Input.NewEmail is null || Input.NewEmail == email) 80 | { 81 | message = "Your email is unchanged."; 82 | return; 83 | } 84 | 85 | var userId = await UserManager.GetUserIdAsync(user); 86 | var code = await UserManager.GenerateChangeEmailTokenAsync(user, Input.NewEmail); 87 | code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code)); 88 | var callbackUrl = NavigationManager.GetUriWithQueryParameters( 89 | NavigationManager.ToAbsoluteUri("Account/ConfirmEmailChange").AbsoluteUri, 90 | new Dictionary { ["userId"] = userId, ["email"] = Input.NewEmail, ["code"] = code }); 91 | 92 | await EmailSender.SendConfirmationLinkAsync(user, Input.NewEmail, HtmlEncoder.Default.Encode(callbackUrl)); 93 | 94 | message = "Confirmation link to change email sent. Please check your email."; 95 | } 96 | 97 | private async Task OnSendEmailVerificationAsync() 98 | { 99 | if (email is null) 100 | { 101 | return; 102 | } 103 | 104 | var userId = await UserManager.GetUserIdAsync(user); 105 | var code = await UserManager.GenerateEmailConfirmationTokenAsync(user); 106 | code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code)); 107 | var callbackUrl = NavigationManager.GetUriWithQueryParameters( 108 | NavigationManager.ToAbsoluteUri("Account/ConfirmEmail").AbsoluteUri, 109 | new Dictionary { ["userId"] = userId, ["code"] = code }); 110 | 111 | await EmailSender.SendConfirmationLinkAsync(user, email, HtmlEncoder.Default.Encode(callbackUrl)); 112 | 113 | message = "Verification email sent. Please check your email."; 114 | } 115 | 116 | private sealed class InputModel 117 | { 118 | [Required] 119 | [EmailAddress] 120 | [Display(Name = "New email")] 121 | public string? NewEmail { get; set; } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /CleanBlazorWeb/CleanBlazorWeb/Components/Account/Pages/Manage/EnableAuthenticator.razor: -------------------------------------------------------------------------------- 1 | @page "/Account/Manage/EnableAuthenticator" 2 | 3 | @using System.ComponentModel.DataAnnotations 4 | @using System.Globalization 5 | @using System.Text 6 | @using System.Text.Encodings.Web 7 | @using Microsoft.AspNetCore.Identity 8 | @using CleanBlazorWeb.Data 9 | 10 | @inject UserManager UserManager 11 | @inject IdentityUserAccessor UserAccessor 12 | @inject UrlEncoder UrlEncoder 13 | @inject IdentityRedirectManager RedirectManager 14 | @inject ILogger Logger 15 | 16 | Configure authenticator app 17 | 18 | @if (recoveryCodes is not null) 19 | { 20 | 21 | } 22 | else 23 | { 24 | 25 |

Configure authenticator app

26 |
27 |

To use an authenticator app go through the following steps:

28 |
    29 |
  1. 30 |

    31 | Download a two-factor authenticator app like Microsoft Authenticator for 32 | Android and 33 | iOS or 34 | Google Authenticator for 35 | Android and 36 | iOS. 37 |

    38 |
  2. 39 |
  3. 40 |

    Scan the QR Code or enter this key @sharedKey into your two factor authenticator app. Spaces and casing do not matter.

    41 | 42 |
    43 |
    44 |
  4. 45 |
  5. 46 |

    47 | Once you have scanned the QR code or input the key above, your two factor authentication app will provide you 48 | with a unique code. Enter the code in the confirmation box below. 49 |

    50 |
    51 |
    52 | 53 | 54 |
    55 | 56 | 57 | 58 |
    59 | 60 | 61 |
    62 |
    63 |
    64 |
  6. 65 |
66 |
67 | } 68 | 69 | @code { 70 | private const string AuthenticatorUriFormat = "otpauth://totp/{0}:{1}?secret={2}&issuer={0}&digits=6"; 71 | 72 | private string? message; 73 | private ApplicationUser user = default!; 74 | private string? sharedKey; 75 | private string? authenticatorUri; 76 | private IEnumerable? recoveryCodes; 77 | 78 | [CascadingParameter] 79 | private HttpContext HttpContext { get; set; } = default!; 80 | 81 | [SupplyParameterFromForm] 82 | private InputModel Input { get; set; } = new(); 83 | 84 | protected override async Task OnInitializedAsync() 85 | { 86 | user = await UserAccessor.GetRequiredUserAsync(HttpContext); 87 | 88 | await LoadSharedKeyAndQrCodeUriAsync(user); 89 | } 90 | 91 | private async Task OnValidSubmitAsync() 92 | { 93 | // Strip spaces and hyphens 94 | var verificationCode = Input.Code.Replace(" ", string.Empty).Replace("-", string.Empty); 95 | 96 | var is2faTokenValid = await UserManager.VerifyTwoFactorTokenAsync( 97 | user, UserManager.Options.Tokens.AuthenticatorTokenProvider, verificationCode); 98 | 99 | if (!is2faTokenValid) 100 | { 101 | message = "Error: Verification code is invalid."; 102 | return; 103 | } 104 | 105 | await UserManager.SetTwoFactorEnabledAsync(user, true); 106 | var userId = await UserManager.GetUserIdAsync(user); 107 | Logger.LogInformation("User with ID '{UserId}' has enabled 2FA with an authenticator app.", userId); 108 | 109 | message = "Your authenticator app has been verified."; 110 | 111 | if (await UserManager.CountRecoveryCodesAsync(user) == 0) 112 | { 113 | recoveryCodes = await UserManager.GenerateNewTwoFactorRecoveryCodesAsync(user, 10); 114 | } 115 | else 116 | { 117 | RedirectManager.RedirectToWithStatus("Account/Manage/TwoFactorAuthentication", message, HttpContext); 118 | } 119 | } 120 | 121 | private async ValueTask LoadSharedKeyAndQrCodeUriAsync(ApplicationUser user) 122 | { 123 | // Load the authenticator key & QR code URI to display on the form 124 | var unformattedKey = await UserManager.GetAuthenticatorKeyAsync(user); 125 | if (string.IsNullOrEmpty(unformattedKey)) 126 | { 127 | await UserManager.ResetAuthenticatorKeyAsync(user); 128 | unformattedKey = await UserManager.GetAuthenticatorKeyAsync(user); 129 | } 130 | 131 | sharedKey = FormatKey(unformattedKey!); 132 | 133 | var email = await UserManager.GetEmailAsync(user); 134 | authenticatorUri = GenerateQrCodeUri(email!, unformattedKey!); 135 | } 136 | 137 | private string FormatKey(string unformattedKey) 138 | { 139 | var result = new StringBuilder(); 140 | int currentPosition = 0; 141 | while (currentPosition + 4 < unformattedKey.Length) 142 | { 143 | result.Append(unformattedKey.AsSpan(currentPosition, 4)).Append(' '); 144 | currentPosition += 4; 145 | } 146 | if (currentPosition < unformattedKey.Length) 147 | { 148 | result.Append(unformattedKey.AsSpan(currentPosition)); 149 | } 150 | 151 | return result.ToString().ToLowerInvariant(); 152 | } 153 | 154 | private string GenerateQrCodeUri(string email, string unformattedKey) 155 | { 156 | return string.Format( 157 | CultureInfo.InvariantCulture, 158 | AuthenticatorUriFormat, 159 | UrlEncoder.Encode("Microsoft.AspNetCore.Identity.UI"), 160 | UrlEncoder.Encode(email), 161 | unformattedKey); 162 | } 163 | 164 | private sealed class InputModel 165 | { 166 | [Required] 167 | [StringLength(7, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)] 168 | [DataType(DataType.Text)] 169 | [Display(Name = "Verification Code")] 170 | public string Code { get; set; } = ""; 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /CleanBlazorWeb/CleanBlazorWeb/Components/Account/Pages/Manage/ExternalLogins.razor: -------------------------------------------------------------------------------- 1 | @page "/Account/Manage/ExternalLogins" 2 | 3 | @using Microsoft.AspNetCore.Authentication 4 | @using Microsoft.AspNetCore.Identity 5 | @using CleanBlazorWeb.Data 6 | 7 | @inject UserManager UserManager 8 | @inject SignInManager SignInManager 9 | @inject IdentityUserAccessor UserAccessor 10 | @inject IUserStore UserStore 11 | @inject IdentityRedirectManager RedirectManager 12 | 13 | Manage your external logins 14 | 15 | 16 | @if (currentLogins?.Count > 0) 17 | { 18 |

Registered Logins

19 | 20 | 21 | @foreach (var login in currentLogins) 22 | { 23 | 24 | 25 | 42 | 43 | } 44 | 45 |
@login.ProviderDisplayName 26 | @if (showRemoveButton) 27 | { 28 |
29 | 30 |
31 | 32 | 33 | 34 |
35 | 36 | } 37 | else 38 | { 39 | @:   40 | } 41 |
46 | } 47 | @if (otherLogins?.Count > 0) 48 | { 49 |

Add another service to log in.

50 |
51 |
52 | 53 |
54 |

55 | @foreach (var provider in otherLogins) 56 | { 57 | 60 | } 61 |

62 |
63 | 64 | } 65 | 66 | @code { 67 | public const string LinkLoginCallbackAction = "LinkLoginCallback"; 68 | 69 | private ApplicationUser user = default!; 70 | private IList? currentLogins; 71 | private IList? otherLogins; 72 | private bool showRemoveButton; 73 | 74 | [CascadingParameter] 75 | private HttpContext HttpContext { get; set; } = default!; 76 | 77 | [SupplyParameterFromForm] 78 | private string? LoginProvider { get; set; } 79 | 80 | [SupplyParameterFromForm] 81 | private string? ProviderKey { get; set; } 82 | 83 | [SupplyParameterFromQuery] 84 | private string? Action { get; set; } 85 | 86 | protected override async Task OnInitializedAsync() 87 | { 88 | user = await UserAccessor.GetRequiredUserAsync(HttpContext); 89 | currentLogins = await UserManager.GetLoginsAsync(user); 90 | otherLogins = (await SignInManager.GetExternalAuthenticationSchemesAsync()) 91 | .Where(auth => currentLogins.All(ul => auth.Name != ul.LoginProvider)) 92 | .ToList(); 93 | 94 | string? passwordHash = null; 95 | if (UserStore is IUserPasswordStore userPasswordStore) 96 | { 97 | passwordHash = await userPasswordStore.GetPasswordHashAsync(user, HttpContext.RequestAborted); 98 | } 99 | 100 | showRemoveButton = passwordHash is not null || currentLogins.Count > 1; 101 | 102 | if (HttpMethods.IsGet(HttpContext.Request.Method) && Action == LinkLoginCallbackAction) 103 | { 104 | await OnGetLinkLoginCallbackAsync(); 105 | } 106 | } 107 | 108 | private async Task OnSubmitAsync() 109 | { 110 | var result = await UserManager.RemoveLoginAsync(user, LoginProvider!, ProviderKey!); 111 | if (!result.Succeeded) 112 | { 113 | RedirectManager.RedirectToCurrentPageWithStatus("Error: The external login was not removed.", HttpContext); 114 | } 115 | 116 | await SignInManager.RefreshSignInAsync(user); 117 | RedirectManager.RedirectToCurrentPageWithStatus("The external login was removed.", HttpContext); 118 | } 119 | 120 | private async Task OnGetLinkLoginCallbackAsync() 121 | { 122 | var userId = await UserManager.GetUserIdAsync(user); 123 | var info = await SignInManager.GetExternalLoginInfoAsync(userId); 124 | if (info is null) 125 | { 126 | RedirectManager.RedirectToCurrentPageWithStatus("Error: Could not load external login info.", HttpContext); 127 | } 128 | 129 | var result = await UserManager.AddLoginAsync(user, info); 130 | if (!result.Succeeded) 131 | { 132 | RedirectManager.RedirectToCurrentPageWithStatus("Error: The external login was not added. External logins can only be associated with one account.", HttpContext); 133 | } 134 | 135 | // Clear the existing external cookie to ensure a clean login process 136 | await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme); 137 | 138 | RedirectManager.RedirectToCurrentPageWithStatus("The external login was added.", HttpContext); 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /CleanBlazorWeb/CleanBlazorWeb/Components/Account/Pages/Manage/GenerateRecoveryCodes.razor: -------------------------------------------------------------------------------- 1 | @page "/Account/Manage/GenerateRecoveryCodes" 2 | 3 | @using Microsoft.AspNetCore.Identity 4 | @using CleanBlazorWeb.Data 5 | 6 | @inject UserManager UserManager 7 | @inject IdentityUserAccessor UserAccessor 8 | @inject IdentityRedirectManager RedirectManager 9 | @inject ILogger Logger 10 | 11 | Generate two-factor authentication (2FA) recovery codes 12 | 13 | @if (recoveryCodes is not null) 14 | { 15 | 16 | } 17 | else 18 | { 19 |

Generate two-factor authentication (2FA) recovery codes

20 | 33 |
34 |
35 | 36 | 37 | 38 |
39 | } 40 | 41 | @code { 42 | private string? message; 43 | private ApplicationUser user = default!; 44 | private IEnumerable? recoveryCodes; 45 | 46 | [CascadingParameter] 47 | private HttpContext HttpContext { get; set; } = default!; 48 | 49 | protected override async Task OnInitializedAsync() 50 | { 51 | user = await UserAccessor.GetRequiredUserAsync(HttpContext); 52 | 53 | var isTwoFactorEnabled = await UserManager.GetTwoFactorEnabledAsync(user); 54 | if (!isTwoFactorEnabled) 55 | { 56 | throw new InvalidOperationException("Cannot generate recovery codes for user because they do not have 2FA enabled."); 57 | } 58 | } 59 | 60 | private async Task OnSubmitAsync() 61 | { 62 | var userId = await UserManager.GetUserIdAsync(user); 63 | recoveryCodes = await UserManager.GenerateNewTwoFactorRecoveryCodesAsync(user, 10); 64 | message = "You have generated new recovery codes."; 65 | 66 | Logger.LogInformation("User with ID '{UserId}' has generated new 2FA recovery codes.", userId); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /CleanBlazorWeb/CleanBlazorWeb/Components/Account/Pages/Manage/Index.razor: -------------------------------------------------------------------------------- 1 | @page "/Account/Manage" 2 | 3 | @using System.ComponentModel.DataAnnotations 4 | @using Microsoft.AspNetCore.Identity 5 | @using CleanBlazorWeb.Data 6 | 7 | @inject UserManager UserManager 8 | @inject SignInManager SignInManager 9 | @inject IdentityUserAccessor UserAccessor 10 | @inject IdentityRedirectManager RedirectManager 11 | 12 | Profile 13 | 14 |

Profile

15 | 16 | 17 |
18 |
19 | 20 | 21 | 22 |
23 | 24 | 25 |
26 |
27 | 28 | 29 | 30 |
31 | 32 |
33 |
34 |
35 | 36 | @code { 37 | private ApplicationUser user = default!; 38 | private string? username; 39 | private string? phoneNumber; 40 | 41 | [CascadingParameter] 42 | private HttpContext HttpContext { get; set; } = default!; 43 | 44 | [SupplyParameterFromForm] 45 | private InputModel Input { get; set; } = new(); 46 | 47 | protected override async Task OnInitializedAsync() 48 | { 49 | user = await UserAccessor.GetRequiredUserAsync(HttpContext); 50 | username = await UserManager.GetUserNameAsync(user); 51 | phoneNumber = await UserManager.GetPhoneNumberAsync(user); 52 | 53 | Input.PhoneNumber ??= phoneNumber; 54 | } 55 | 56 | private async Task OnValidSubmitAsync() 57 | { 58 | if (Input.PhoneNumber != phoneNumber) 59 | { 60 | var setPhoneResult = await UserManager.SetPhoneNumberAsync(user, Input.PhoneNumber); 61 | if (!setPhoneResult.Succeeded) 62 | { 63 | RedirectManager.RedirectToCurrentPageWithStatus("Error: Failed to set phone number.", HttpContext); 64 | } 65 | } 66 | 67 | await SignInManager.RefreshSignInAsync(user); 68 | RedirectManager.RedirectToCurrentPageWithStatus("Your profile has been updated", HttpContext); 69 | } 70 | 71 | private sealed class InputModel 72 | { 73 | [Phone] 74 | [Display(Name = "Phone number")] 75 | public string? PhoneNumber { get; set; } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /CleanBlazorWeb/CleanBlazorWeb/Components/Account/Pages/Manage/PersonalData.razor: -------------------------------------------------------------------------------- 1 | @page "/Account/Manage/PersonalData" 2 | 3 | @inject IdentityUserAccessor UserAccessor 4 | 5 | Personal Data 6 | 7 | 8 |

Personal Data

9 | 10 |
11 |
12 |

Your account contains personal data that you have given us. This page allows you to download or delete that data.

13 |

14 | Deleting this data will permanently remove your account, and this cannot be recovered. 15 |

16 |
17 | 18 | 19 | 20 |

21 | Delete 22 |

23 |
24 |
25 | 26 | @code { 27 | [CascadingParameter] 28 | private HttpContext HttpContext { get; set; } = default!; 29 | 30 | protected override async Task OnInitializedAsync() 31 | { 32 | _ = await UserAccessor.GetRequiredUserAsync(HttpContext); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /CleanBlazorWeb/CleanBlazorWeb/Components/Account/Pages/Manage/ResetAuthenticator.razor: -------------------------------------------------------------------------------- 1 | @page "/Account/Manage/ResetAuthenticator" 2 | 3 | @using Microsoft.AspNetCore.Identity 4 | @using CleanBlazorWeb.Data 5 | 6 | @inject UserManager UserManager 7 | @inject SignInManager SignInManager 8 | @inject IdentityUserAccessor UserAccessor 9 | @inject IdentityRedirectManager RedirectManager 10 | @inject ILogger Logger 11 | 12 | Reset authenticator key 13 | 14 | 15 |

Reset authenticator key

16 | 26 |
27 |
28 | 29 | 30 | 31 |
32 | 33 | @code { 34 | [CascadingParameter] 35 | private HttpContext HttpContext { get; set; } = default!; 36 | 37 | private async Task OnSubmitAsync() 38 | { 39 | var user = await UserAccessor.GetRequiredUserAsync(HttpContext); 40 | await UserManager.SetTwoFactorEnabledAsync(user, false); 41 | await UserManager.ResetAuthenticatorKeyAsync(user); 42 | var userId = await UserManager.GetUserIdAsync(user); 43 | Logger.LogInformation("User with ID '{UserId}' has reset their authentication app key.", userId); 44 | 45 | await SignInManager.RefreshSignInAsync(user); 46 | 47 | RedirectManager.RedirectToWithStatus( 48 | "Account/Manage/EnableAuthenticator", 49 | "Your authenticator app key has been reset, you will need to configure your authenticator app using the new key.", 50 | HttpContext); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /CleanBlazorWeb/CleanBlazorWeb/Components/Account/Pages/Manage/SetPassword.razor: -------------------------------------------------------------------------------- 1 | @page "/Account/Manage/SetPassword" 2 | 3 | @using System.ComponentModel.DataAnnotations 4 | @using Microsoft.AspNetCore.Identity 5 | @using CleanBlazorWeb.Data 6 | 7 | @inject UserManager UserManager 8 | @inject SignInManager SignInManager 9 | @inject IdentityUserAccessor UserAccessor 10 | @inject IdentityRedirectManager RedirectManager 11 | 12 | Set password 13 | 14 |

Set your password

15 | 16 |

17 | You do not have a local username/password for this site. Add a local 18 | account so you can log in without an external login. 19 |

20 |
21 |
22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 |
30 |
31 | 32 | 33 | 34 |
35 | 36 |
37 |
38 |
39 | 40 | @code { 41 | private string? message; 42 | private ApplicationUser user = default!; 43 | 44 | [CascadingParameter] 45 | private HttpContext HttpContext { get; set; } = default!; 46 | 47 | [SupplyParameterFromForm] 48 | private InputModel Input { get; set; } = new(); 49 | 50 | protected override async Task OnInitializedAsync() 51 | { 52 | user = await UserAccessor.GetRequiredUserAsync(HttpContext); 53 | 54 | var hasPassword = await UserManager.HasPasswordAsync(user); 55 | if (hasPassword) 56 | { 57 | RedirectManager.RedirectTo("Account/Manage/ChangePassword"); 58 | } 59 | } 60 | 61 | private async Task OnValidSubmitAsync() 62 | { 63 | var addPasswordResult = await UserManager.AddPasswordAsync(user, Input.NewPassword!); 64 | if (!addPasswordResult.Succeeded) 65 | { 66 | message = $"Error: {string.Join(",", addPasswordResult.Errors.Select(error => error.Description))}"; 67 | return; 68 | } 69 | 70 | await SignInManager.RefreshSignInAsync(user); 71 | RedirectManager.RedirectToCurrentPageWithStatus("Your password has been set.", HttpContext); 72 | } 73 | 74 | private sealed class InputModel 75 | { 76 | [Required] 77 | [StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)] 78 | [DataType(DataType.Password)] 79 | [Display(Name = "New password")] 80 | public string? NewPassword { get; set; } 81 | 82 | [DataType(DataType.Password)] 83 | [Display(Name = "Confirm new password")] 84 | [Compare("NewPassword", ErrorMessage = "The new password and confirmation password do not match.")] 85 | public string? ConfirmPassword { get; set; } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /CleanBlazorWeb/CleanBlazorWeb/Components/Account/Pages/Manage/TwoFactorAuthentication.razor: -------------------------------------------------------------------------------- 1 | @page "/Account/Manage/TwoFactorAuthentication" 2 | 3 | @using Microsoft.AspNetCore.Http.Features 4 | @using Microsoft.AspNetCore.Identity 5 | @using CleanBlazorWeb.Data 6 | 7 | @inject UserManager UserManager 8 | @inject SignInManager SignInManager 9 | @inject IdentityUserAccessor UserAccessor 10 | @inject IdentityRedirectManager RedirectManager 11 | 12 | Two-factor authentication (2FA) 13 | 14 | 15 |

Two-factor authentication (2FA)

16 | @if (canTrack) 17 | { 18 | if (is2faEnabled) 19 | { 20 | if (recoveryCodesLeft == 0) 21 | { 22 |
23 | You have no recovery codes left. 24 |

You must generate a new set of recovery codes before you can log in with a recovery code.

25 |
26 | } 27 | else if (recoveryCodesLeft == 1) 28 | { 29 |
30 | You have 1 recovery code left. 31 |

You can generate a new set of recovery codes.

32 |
33 | } 34 | else if (recoveryCodesLeft <= 3) 35 | { 36 |
37 | You have @recoveryCodesLeft recovery codes left. 38 |

You should generate a new set of recovery codes.

39 |
40 | } 41 | 42 | if (isMachineRemembered) 43 | { 44 |
45 | 46 | 47 | 48 | } 49 | 50 | Disable 2FA 51 | Reset recovery codes 52 | } 53 | 54 |

Authenticator app

55 | @if (!hasAuthenticator) 56 | { 57 | Add authenticator app 58 | } 59 | else 60 | { 61 | Set up authenticator app 62 | Reset authenticator app 63 | } 64 | } 65 | else 66 | { 67 |
68 | Privacy and cookie policy have not been accepted. 69 |

You must accept the policy before you can enable two factor authentication.

70 |
71 | } 72 | 73 | @code { 74 | private bool canTrack; 75 | private bool hasAuthenticator; 76 | private int recoveryCodesLeft; 77 | private bool is2faEnabled; 78 | private bool isMachineRemembered; 79 | 80 | [CascadingParameter] 81 | private HttpContext HttpContext { get; set; } = default!; 82 | 83 | protected override async Task OnInitializedAsync() 84 | { 85 | var user = await UserAccessor.GetRequiredUserAsync(HttpContext); 86 | canTrack = HttpContext.Features.Get()?.CanTrack ?? true; 87 | hasAuthenticator = await UserManager.GetAuthenticatorKeyAsync(user) is not null; 88 | is2faEnabled = await UserManager.GetTwoFactorEnabledAsync(user); 89 | isMachineRemembered = await SignInManager.IsTwoFactorClientRememberedAsync(user); 90 | recoveryCodesLeft = await UserManager.CountRecoveryCodesAsync(user); 91 | } 92 | 93 | private async Task OnSubmitForgetBrowserAsync() 94 | { 95 | await SignInManager.ForgetTwoFactorClientAsync(); 96 | 97 | RedirectManager.RedirectToCurrentPageWithStatus( 98 | "The current browser has been forgotten. When you login again from this browser you will be prompted for your 2fa code.", 99 | HttpContext); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /CleanBlazorWeb/CleanBlazorWeb/Components/Account/Pages/Manage/_Imports.razor: -------------------------------------------------------------------------------- 1 | @layout ManageLayout 2 | @attribute [Microsoft.AspNetCore.Authorization.Authorize] 3 | -------------------------------------------------------------------------------- /CleanBlazorWeb/CleanBlazorWeb/Components/Account/Pages/Register.razor: -------------------------------------------------------------------------------- 1 | @page "/Account/Register" 2 | 3 | @using System.ComponentModel.DataAnnotations 4 | @using System.Text 5 | @using System.Text.Encodings.Web 6 | @using Microsoft.AspNetCore.Identity 7 | @using Microsoft.AspNetCore.WebUtilities 8 | @using CleanBlazorWeb.Data 9 | 10 | @inject UserManager UserManager 11 | @inject IUserStore UserStore 12 | @inject SignInManager SignInManager 13 | @inject IEmailSender EmailSender 14 | @inject ILogger Logger 15 | @inject NavigationManager NavigationManager 16 | @inject IdentityRedirectManager RedirectManager 17 | 18 | Register 19 | 20 |

Register

21 | 22 |
23 |
24 | 25 | 26 | 27 |

Create a new account.

28 |
29 | 30 |
31 | 32 | 33 | 34 |
35 |
36 | 37 | 38 | 39 |
40 |
41 | 42 | 43 | 44 |
45 | 46 |
47 |
48 |
49 |
50 |

Use another service to register.

51 |
52 | 53 |
54 |
55 |
56 | 57 | @code { 58 | private IEnumerable? identityErrors; 59 | 60 | [SupplyParameterFromForm] 61 | private InputModel Input { get; set; } = new(); 62 | 63 | [SupplyParameterFromQuery] 64 | private string? ReturnUrl { get; set; } 65 | 66 | private string? Message => identityErrors is null ? null : $"Error: {string.Join(", ", identityErrors.Select(error => error.Description))}"; 67 | 68 | public async Task RegisterUser(EditContext editContext) 69 | { 70 | var user = CreateUser(); 71 | 72 | await UserStore.SetUserNameAsync(user, Input.Email, CancellationToken.None); 73 | var emailStore = GetEmailStore(); 74 | await emailStore.SetEmailAsync(user, Input.Email, CancellationToken.None); 75 | var result = await UserManager.CreateAsync(user, Input.Password); 76 | 77 | if (!result.Succeeded) 78 | { 79 | identityErrors = result.Errors; 80 | return; 81 | } 82 | 83 | Logger.LogInformation("User created a new account with password."); 84 | 85 | var userId = await UserManager.GetUserIdAsync(user); 86 | var code = await UserManager.GenerateEmailConfirmationTokenAsync(user); 87 | code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code)); 88 | var callbackUrl = NavigationManager.GetUriWithQueryParameters( 89 | NavigationManager.ToAbsoluteUri("Account/ConfirmEmail").AbsoluteUri, 90 | new Dictionary { ["userId"] = userId, ["code"] = code, ["returnUrl"] = ReturnUrl }); 91 | 92 | await EmailSender.SendConfirmationLinkAsync(user, Input.Email, HtmlEncoder.Default.Encode(callbackUrl)); 93 | 94 | if (UserManager.Options.SignIn.RequireConfirmedAccount) 95 | { 96 | RedirectManager.RedirectTo( 97 | "Account/RegisterConfirmation", 98 | new() { ["email"] = Input.Email, ["returnUrl"] = ReturnUrl }); 99 | } 100 | 101 | await SignInManager.SignInAsync(user, isPersistent: false); 102 | RedirectManager.RedirectTo(ReturnUrl); 103 | } 104 | 105 | private ApplicationUser CreateUser() 106 | { 107 | try 108 | { 109 | return Activator.CreateInstance(); 110 | } 111 | catch 112 | { 113 | throw new InvalidOperationException($"Can't create an instance of '{nameof(ApplicationUser)}'. " + 114 | $"Ensure that '{nameof(ApplicationUser)}' is not an abstract class and has a parameterless constructor."); 115 | } 116 | } 117 | 118 | private IUserEmailStore GetEmailStore() 119 | { 120 | if (!UserManager.SupportsUserEmail) 121 | { 122 | throw new NotSupportedException("The default UI requires a user store with email support."); 123 | } 124 | return (IUserEmailStore)UserStore; 125 | } 126 | 127 | private sealed class InputModel 128 | { 129 | [Required] 130 | [EmailAddress] 131 | [Display(Name = "Email")] 132 | public string Email { get; set; } = ""; 133 | 134 | [Required] 135 | [StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)] 136 | [DataType(DataType.Password)] 137 | [Display(Name = "Password")] 138 | public string Password { get; set; } = ""; 139 | 140 | [DataType(DataType.Password)] 141 | [Display(Name = "Confirm password")] 142 | [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")] 143 | public string ConfirmPassword { get; set; } = ""; 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /CleanBlazorWeb/CleanBlazorWeb/Components/Account/Pages/RegisterConfirmation.razor: -------------------------------------------------------------------------------- 1 | @page "/Account/RegisterConfirmation" 2 | 3 | @using System.Text 4 | @using Microsoft.AspNetCore.Identity 5 | @using Microsoft.AspNetCore.WebUtilities 6 | @using CleanBlazorWeb.Data 7 | 8 | @inject UserManager UserManager 9 | @inject IEmailSender EmailSender 10 | @inject NavigationManager NavigationManager 11 | @inject IdentityRedirectManager RedirectManager 12 | 13 | Register confirmation 14 | 15 |

Register confirmation

16 | 17 | 18 | 19 | @if (emailConfirmationLink is not null) 20 | { 21 |

22 | This app does not currently have a real email sender registered, see these docs for how to configure a real email sender. 23 | Normally this would be emailed: Click here to confirm your account 24 |

25 | } 26 | else 27 | { 28 |

Please check your email to confirm your account.

29 | } 30 | 31 | @code { 32 | private string? emailConfirmationLink; 33 | private string? statusMessage; 34 | 35 | [CascadingParameter] 36 | private HttpContext HttpContext { get; set; } = default!; 37 | 38 | [SupplyParameterFromQuery] 39 | private string? Email { get; set; } 40 | 41 | [SupplyParameterFromQuery] 42 | private string? ReturnUrl { get; set; } 43 | 44 | protected override async Task OnInitializedAsync() 45 | { 46 | if (Email is null) 47 | { 48 | RedirectManager.RedirectTo(""); 49 | } 50 | 51 | var user = await UserManager.FindByEmailAsync(Email); 52 | if (user is null) 53 | { 54 | HttpContext.Response.StatusCode = StatusCodes.Status404NotFound; 55 | statusMessage = "Error finding user for unspecified email"; 56 | } 57 | else if (EmailSender is IdentityNoOpEmailSender) 58 | { 59 | // Once you add a real email sender, you should remove this code that lets you confirm the account 60 | var userId = await UserManager.GetUserIdAsync(user); 61 | var code = await UserManager.GenerateEmailConfirmationTokenAsync(user); 62 | code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code)); 63 | emailConfirmationLink = NavigationManager.GetUriWithQueryParameters( 64 | NavigationManager.ToAbsoluteUri("Account/ConfirmEmail").AbsoluteUri, 65 | new Dictionary { ["userId"] = userId, ["code"] = code, ["returnUrl"] = ReturnUrl }); 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /CleanBlazorWeb/CleanBlazorWeb/Components/Account/Pages/ResendEmailConfirmation.razor: -------------------------------------------------------------------------------- 1 | @page "/Account/ResendEmailConfirmation" 2 | 3 | @using System.ComponentModel.DataAnnotations 4 | @using System.Text 5 | @using System.Text.Encodings.Web 6 | @using Microsoft.AspNetCore.Identity 7 | @using Microsoft.AspNetCore.WebUtilities 8 | @using CleanBlazorWeb.Data 9 | 10 | @inject UserManager UserManager 11 | @inject IEmailSender EmailSender 12 | @inject NavigationManager NavigationManager 13 | @inject IdentityRedirectManager RedirectManager 14 | 15 | Resend email confirmation 16 | 17 |

Resend email confirmation

18 |

Enter your email.

19 |
20 | 21 |
22 |
23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 |
31 | 32 |
33 |
34 |
35 | 36 | @code { 37 | private string? message; 38 | 39 | [SupplyParameterFromForm] 40 | private InputModel Input { get; set; } = new(); 41 | 42 | private async Task OnValidSubmitAsync() 43 | { 44 | var user = await UserManager.FindByEmailAsync(Input.Email!); 45 | if (user is null) 46 | { 47 | message = "Verification email sent. Please check your email."; 48 | return; 49 | } 50 | 51 | var userId = await UserManager.GetUserIdAsync(user); 52 | var code = await UserManager.GenerateEmailConfirmationTokenAsync(user); 53 | code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code)); 54 | var callbackUrl = NavigationManager.GetUriWithQueryParameters( 55 | NavigationManager.ToAbsoluteUri("Account/ConfirmEmail").AbsoluteUri, 56 | new Dictionary { ["userId"] = userId, ["code"] = code }); 57 | await EmailSender.SendConfirmationLinkAsync(user, Input.Email, HtmlEncoder.Default.Encode(callbackUrl)); 58 | 59 | message = "Verification email sent. Please check your email."; 60 | } 61 | 62 | private sealed class InputModel 63 | { 64 | [Required] 65 | [EmailAddress] 66 | public string Email { get; set; } = ""; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /CleanBlazorWeb/CleanBlazorWeb/Components/Account/Pages/ResetPassword.razor: -------------------------------------------------------------------------------- 1 | @page "/Account/ResetPassword" 2 | 3 | @using System.ComponentModel.DataAnnotations 4 | @using System.Text 5 | @using Microsoft.AspNetCore.Identity 6 | @using Microsoft.AspNetCore.WebUtilities 7 | @using CleanBlazorWeb.Data 8 | 9 | @inject IdentityRedirectManager RedirectManager 10 | @inject UserManager UserManager 11 | 12 | Reset password 13 | 14 |

Reset password

15 |

Reset your password.

16 |
17 |
18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 |
30 |
31 | 32 | 33 | 34 |
35 |
36 | 37 | 38 | 39 |
40 | 41 |
42 |
43 |
44 | 45 | @code { 46 | private IEnumerable? identityErrors; 47 | 48 | [SupplyParameterFromForm] 49 | private InputModel Input { get; set; } = new(); 50 | 51 | [SupplyParameterFromQuery] 52 | private string? Code { get; set; } 53 | 54 | private string? Message => identityErrors is null ? null : $"Error: {string.Join(", ", identityErrors.Select(error => error.Description))}"; 55 | 56 | protected override void OnInitialized() 57 | { 58 | if (Code is null) 59 | { 60 | RedirectManager.RedirectTo("Account/InvalidPasswordReset"); 61 | } 62 | 63 | Input.Code = Encoding.UTF8.GetString(WebEncoders.Base64UrlDecode(Code)); 64 | } 65 | 66 | private async Task OnValidSubmitAsync() 67 | { 68 | var user = await UserManager.FindByEmailAsync(Input.Email); 69 | if (user is null) 70 | { 71 | // Don't reveal that the user does not exist 72 | RedirectManager.RedirectTo("Account/ResetPasswordConfirmation"); 73 | } 74 | 75 | var result = await UserManager.ResetPasswordAsync(user, Input.Code, Input.Password); 76 | if (result.Succeeded) 77 | { 78 | RedirectManager.RedirectTo("Account/ResetPasswordConfirmation"); 79 | } 80 | 81 | identityErrors = result.Errors; 82 | } 83 | 84 | private sealed class InputModel 85 | { 86 | [Required] 87 | [EmailAddress] 88 | public string Email { get; set; } = ""; 89 | 90 | [Required] 91 | [StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)] 92 | [DataType(DataType.Password)] 93 | public string Password { get; set; } = ""; 94 | 95 | [DataType(DataType.Password)] 96 | [Display(Name = "Confirm password")] 97 | [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")] 98 | public string ConfirmPassword { get; set; } = ""; 99 | 100 | [Required] 101 | public string Code { get; set; } = ""; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /CleanBlazorWeb/CleanBlazorWeb/Components/Account/Pages/ResetPasswordConfirmation.razor: -------------------------------------------------------------------------------- 1 | @page "/Account/ResetPasswordConfirmation" 2 | Reset password confirmation 3 | 4 |

Reset password confirmation

5 |

6 | Your password has been reset. Please click here to log in. 7 |

8 | -------------------------------------------------------------------------------- /CleanBlazorWeb/CleanBlazorWeb/Components/Account/Pages/_Imports.razor: -------------------------------------------------------------------------------- 1 | @using CleanBlazorWeb.Components.Account.Shared 2 | @layout AccountLayout 3 | -------------------------------------------------------------------------------- /CleanBlazorWeb/CleanBlazorWeb/Components/Account/PersistingRevalidatingAuthenticationStateProvider.cs: -------------------------------------------------------------------------------- 1 | using CleanBlazorWeb.Client; 2 | using CleanBlazorWeb.Data; 3 | using Microsoft.AspNetCore.Components; 4 | using Microsoft.AspNetCore.Components.Authorization; 5 | using Microsoft.AspNetCore.Components.Server; 6 | using Microsoft.AspNetCore.Components.Web; 7 | using Microsoft.AspNetCore.Identity; 8 | using Microsoft.Extensions.Options; 9 | using System.Diagnostics; 10 | using System.Security.Claims; 11 | 12 | namespace CleanBlazorWeb.Components.Account 13 | { 14 | // This is a server-side AuthenticationStateProvider that revalidates the security stamp for the connected user 15 | // every 30 minutes an interactive circuit is connected. It also uses PersistentComponentState to flow the 16 | // authentication state to the client which is then fixed for the lifetime of the WebAssembly application. 17 | internal sealed class PersistingRevalidatingAuthenticationStateProvider : RevalidatingServerAuthenticationStateProvider 18 | { 19 | private readonly IServiceScopeFactory scopeFactory; 20 | private readonly PersistentComponentState state; 21 | private readonly IdentityOptions options; 22 | 23 | private readonly PersistingComponentStateSubscription subscription; 24 | 25 | private Task? authenticationStateTask; 26 | 27 | public PersistingRevalidatingAuthenticationStateProvider( 28 | ILoggerFactory loggerFactory, 29 | IServiceScopeFactory serviceScopeFactory, 30 | PersistentComponentState persistentComponentState, 31 | IOptions optionsAccessor) 32 | : base(loggerFactory) 33 | { 34 | scopeFactory = serviceScopeFactory; 35 | state = persistentComponentState; 36 | options = optionsAccessor.Value; 37 | 38 | AuthenticationStateChanged += OnAuthenticationStateChanged; 39 | subscription = state.RegisterOnPersisting(OnPersistingAsync, RenderMode.InteractiveWebAssembly); 40 | } 41 | 42 | protected override TimeSpan RevalidationInterval => TimeSpan.FromMinutes(30); 43 | 44 | protected override async Task ValidateAuthenticationStateAsync( 45 | AuthenticationState authenticationState, CancellationToken cancellationToken) 46 | { 47 | // Get the user manager from a new scope to ensure it fetches fresh data 48 | await using var scope = scopeFactory.CreateAsyncScope(); 49 | var userManager = scope.ServiceProvider.GetRequiredService>(); 50 | return await ValidateSecurityStampAsync(userManager, authenticationState.User); 51 | } 52 | 53 | private async Task ValidateSecurityStampAsync(UserManager userManager, ClaimsPrincipal principal) 54 | { 55 | var user = await userManager.GetUserAsync(principal); 56 | if (user is null) 57 | { 58 | return false; 59 | } 60 | else if (!userManager.SupportsUserSecurityStamp) 61 | { 62 | return true; 63 | } 64 | else 65 | { 66 | var principalStamp = principal.FindFirstValue(options.ClaimsIdentity.SecurityStampClaimType); 67 | var userStamp = await userManager.GetSecurityStampAsync(user); 68 | return principalStamp == userStamp; 69 | } 70 | } 71 | 72 | private void OnAuthenticationStateChanged(Task task) 73 | { 74 | authenticationStateTask = task; 75 | } 76 | 77 | private async Task OnPersistingAsync() 78 | { 79 | if (authenticationStateTask is null) 80 | { 81 | throw new UnreachableException($"Authentication state not set in {nameof(OnPersistingAsync)}()."); 82 | } 83 | 84 | var authenticationState = await authenticationStateTask; 85 | var principal = authenticationState.User; 86 | 87 | if (principal.Identity?.IsAuthenticated == true) 88 | { 89 | var userId = principal.FindFirst(options.ClaimsIdentity.UserIdClaimType)?.Value; 90 | var email = principal.FindFirst(options.ClaimsIdentity.EmailClaimType)?.Value; 91 | 92 | if (userId != null && email != null) 93 | { 94 | state.PersistAsJson(nameof(UserInfo), new UserInfo 95 | { 96 | UserId = userId, 97 | Email = email, 98 | }); 99 | } 100 | } 101 | } 102 | 103 | protected override void Dispose(bool disposing) 104 | { 105 | subscription.Dispose(); 106 | AuthenticationStateChanged -= OnAuthenticationStateChanged; 107 | base.Dispose(disposing); 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /CleanBlazorWeb/CleanBlazorWeb/Components/Account/Shared/AccountLayout.razor: -------------------------------------------------------------------------------- 1 | @inherits LayoutComponentBase 2 | @layout CleanBlazorWeb.Components.Layout.MainLayout 3 | @inject NavigationManager NavigationManager 4 | 5 | @if (HttpContext is null) 6 | { 7 |

Loading...

8 | } 9 | else 10 | { 11 | @Body 12 | } 13 | 14 | @code { 15 | [CascadingParameter] 16 | private HttpContext? HttpContext { get; set; } 17 | 18 | protected override void OnParametersSet() 19 | { 20 | if (HttpContext is null) 21 | { 22 | // If this code runs, we're currently rendering in interactive mode, so there is no HttpContext. 23 | // The identity pages need to set cookies, so they require an HttpContext. To achieve this we 24 | // must transition back from interactive mode to a server-rendered page. 25 | NavigationManager.Refresh(forceReload: true); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /CleanBlazorWeb/CleanBlazorWeb/Components/Account/Shared/ExternalLoginPicker.razor: -------------------------------------------------------------------------------- 1 | @using Microsoft.AspNetCore.Authentication 2 | @using Microsoft.AspNetCore.Identity 3 | @using CleanBlazorWeb.Data 4 | 5 | @inject SignInManager SignInManager 6 | @inject IdentityRedirectManager RedirectManager 7 | 8 | @if (externalLogins.Length == 0) 9 | { 10 |
11 |

12 | There are no external authentication services configured. See this article 13 | about setting up this ASP.NET application to support logging in via external services. 14 |

15 |
16 | } 17 | else 18 | { 19 |
20 |
21 | 22 | 23 |

24 | @foreach (var provider in externalLogins) 25 | { 26 | 27 | } 28 |

29 |
30 |
31 | } 32 | 33 | @code { 34 | private AuthenticationScheme[] externalLogins = []; 35 | 36 | [SupplyParameterFromQuery] 37 | private string? ReturnUrl { get; set; } 38 | 39 | protected override async Task OnInitializedAsync() 40 | { 41 | externalLogins = (await SignInManager.GetExternalAuthenticationSchemesAsync()).ToArray(); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /CleanBlazorWeb/CleanBlazorWeb/Components/Account/Shared/ManageLayout.razor: -------------------------------------------------------------------------------- 1 | @inherits LayoutComponentBase 2 | @layout AccountLayout 3 | 4 |

Manage your account

5 | 6 |
7 |

Change your account settings

8 |
9 |
10 |
11 | 12 |
13 |
14 | @Body 15 |
16 |
17 |
18 | -------------------------------------------------------------------------------- /CleanBlazorWeb/CleanBlazorWeb/Components/Account/Shared/ManageNavMenu.razor: -------------------------------------------------------------------------------- 1 | @using Microsoft.AspNetCore.Identity 2 | @using CleanBlazorWeb.Data 3 | 4 | @inject SignInManager SignInManager 5 | 6 | 29 | 30 | @code { 31 | private bool hasExternalLogins; 32 | 33 | protected override async Task OnInitializedAsync() 34 | { 35 | hasExternalLogins = (await SignInManager.GetExternalAuthenticationSchemesAsync()).Any(); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /CleanBlazorWeb/CleanBlazorWeb/Components/Account/Shared/ShowRecoveryCodes.razor: -------------------------------------------------------------------------------- 1 |  2 |

Recovery codes

3 | 11 |
12 |
13 | @foreach (var recoveryCode in RecoveryCodes) 14 | { 15 |
16 | @recoveryCode 17 |
18 | } 19 |
20 |
21 | 22 | @code { 23 | [Parameter] 24 | public string[] RecoveryCodes { get; set; } = []; 25 | 26 | [Parameter] 27 | public string? StatusMessage { get; set; } 28 | } 29 | -------------------------------------------------------------------------------- /CleanBlazorWeb/CleanBlazorWeb/Components/Account/Shared/StatusMessage.razor: -------------------------------------------------------------------------------- 1 | @if (!string.IsNullOrEmpty(DisplayMessage)) 2 | { 3 | var statusMessageClass = DisplayMessage.StartsWith("Error") ? "danger" : "success"; 4 | 7 | } 8 | 9 | @code { 10 | private string? messageFromCookie; 11 | 12 | [Parameter] 13 | public string? Message { get; set; } 14 | 15 | [CascadingParameter] 16 | private HttpContext HttpContext { get; set; } = default!; 17 | 18 | private string? DisplayMessage => Message ?? messageFromCookie; 19 | 20 | protected override void OnInitialized() 21 | { 22 | messageFromCookie = HttpContext.Request.Cookies[IdentityRedirectManager.StatusCookieName]; 23 | 24 | if (messageFromCookie is not null) 25 | { 26 | HttpContext.Response.Cookies.Delete(IdentityRedirectManager.StatusCookieName); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /CleanBlazorWeb/CleanBlazorWeb/Components/App.razor: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /CleanBlazorWeb/CleanBlazorWeb/Components/Layout/MainLayout.razor: -------------------------------------------------------------------------------- 1 | @inherits LayoutComponentBase 2 | 3 |
4 | 7 | 8 |
9 |
10 | About 11 |
12 | 13 |
14 | @Body 15 |
16 |
17 |
18 | 19 |
20 | An unhandled error has occurred. 21 | Reload 22 | 🗙 23 |
24 | -------------------------------------------------------------------------------- /CleanBlazorWeb/CleanBlazorWeb/Components/Layout/MainLayout.razor.css: -------------------------------------------------------------------------------- 1 | .page { 2 | position: relative; 3 | display: flex; 4 | flex-direction: column; 5 | } 6 | 7 | main { 8 | flex: 1; 9 | } 10 | 11 | .sidebar { 12 | background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%); 13 | } 14 | 15 | .top-row { 16 | background-color: #f7f7f7; 17 | border-bottom: 1px solid #d6d5d5; 18 | justify-content: flex-end; 19 | height: 3.5rem; 20 | display: flex; 21 | align-items: center; 22 | } 23 | 24 | .top-row ::deep a, .top-row ::deep .btn-link { 25 | white-space: nowrap; 26 | margin-left: 1.5rem; 27 | text-decoration: none; 28 | } 29 | 30 | .top-row ::deep a:hover, .top-row ::deep .btn-link:hover { 31 | text-decoration: underline; 32 | } 33 | 34 | .top-row ::deep a:first-child { 35 | overflow: hidden; 36 | text-overflow: ellipsis; 37 | } 38 | 39 | @media (max-width: 640.98px) { 40 | .top-row { 41 | justify-content: space-between; 42 | } 43 | 44 | .top-row ::deep a, .top-row ::deep .btn-link { 45 | margin-left: 0; 46 | } 47 | } 48 | 49 | @media (min-width: 641px) { 50 | .page { 51 | flex-direction: row; 52 | } 53 | 54 | .sidebar { 55 | width: 250px; 56 | height: 100vh; 57 | position: sticky; 58 | top: 0; 59 | } 60 | 61 | .top-row { 62 | position: sticky; 63 | top: 0; 64 | z-index: 1; 65 | } 66 | 67 | .top-row.auth ::deep a:first-child { 68 | flex: 1; 69 | text-align: right; 70 | width: 0; 71 | } 72 | 73 | .top-row, article { 74 | padding-left: 2rem !important; 75 | padding-right: 1.5rem !important; 76 | } 77 | } 78 | 79 | #blazor-error-ui { 80 | background: lightyellow; 81 | bottom: 0; 82 | box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); 83 | display: none; 84 | left: 0; 85 | padding: 0.6rem 1.25rem 0.7rem 1.25rem; 86 | position: fixed; 87 | width: 100%; 88 | z-index: 1000; 89 | } 90 | 91 | #blazor-error-ui .dismiss { 92 | cursor: pointer; 93 | position: absolute; 94 | right: 0.75rem; 95 | top: 0.5rem; 96 | } 97 | -------------------------------------------------------------------------------- /CleanBlazorWeb/CleanBlazorWeb/Components/Layout/NavMenu.razor: -------------------------------------------------------------------------------- 1 | @implements IDisposable 2 | 3 | @inject NavigationManager NavigationManager 4 | 5 | 10 | 11 | 12 | 13 | 71 | 72 | @code { 73 | private string? currentUrl; 74 | 75 | protected override void OnInitialized() 76 | { 77 | currentUrl = NavigationManager.ToBaseRelativePath(NavigationManager.Uri); 78 | NavigationManager.LocationChanged += OnLocationChanged; 79 | } 80 | 81 | private void OnLocationChanged(object? sender, LocationChangedEventArgs e) 82 | { 83 | currentUrl = NavigationManager.ToBaseRelativePath(e.Location); 84 | StateHasChanged(); 85 | } 86 | 87 | public void Dispose() 88 | { 89 | NavigationManager.LocationChanged -= OnLocationChanged; 90 | } 91 | } 92 | 93 | -------------------------------------------------------------------------------- /CleanBlazorWeb/CleanBlazorWeb/Components/Layout/NavMenu.razor.css: -------------------------------------------------------------------------------- 1 | .navbar-toggler { 2 | appearance: none; 3 | cursor: pointer; 4 | width: 3.5rem; 5 | height: 2.5rem; 6 | color: white; 7 | position: absolute; 8 | top: 0.5rem; 9 | right: 1rem; 10 | border: 1px solid rgba(255, 255, 255, 0.1); 11 | background: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e") no-repeat center/1.75rem rgba(255, 255, 255, 0.1); 12 | } 13 | 14 | .navbar-toggler:checked { 15 | background-color: rgba(255, 255, 255, 0.5); 16 | } 17 | 18 | .top-row { 19 | height: 3.5rem; 20 | background-color: rgba(0,0,0,0.4); 21 | } 22 | 23 | .navbar-brand { 24 | font-size: 1.1rem; 25 | } 26 | 27 | .bi { 28 | display: inline-block; 29 | position: relative; 30 | width: 1.25rem; 31 | height: 1.25rem; 32 | margin-right: 0.75rem; 33 | top: -1px; 34 | background-size: cover; 35 | } 36 | 37 | .bi-house-door-fill-nav-menu { 38 | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-house-door-fill' viewBox='0 0 16 16'%3E%3Cpath d='M6.5 14.5v-3.505c0-.245.25-.495.5-.495h2c.25 0 .5.25.5.5v3.5a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.146-.354L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293L8.354 1.146a.5.5 0 0 0-.708 0l-6 6A.5.5 0 0 0 1.5 7.5v7a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5Z'/%3E%3C/svg%3E"); 39 | } 40 | 41 | .bi-plus-square-fill-nav-menu { 42 | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-plus-square-fill' viewBox='0 0 16 16'%3E%3Cpath d='M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2zm6.5 4.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3a.5.5 0 0 1 1 0z'/%3E%3C/svg%3E"); 43 | } 44 | 45 | .bi-list-nested-nav-menu { 46 | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-list-nested' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M4.5 11.5A.5.5 0 0 1 5 11h10a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 3 7h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 1 3h10a.5.5 0 0 1 0 1H1a.5.5 0 0 1-.5-.5z'/%3E%3C/svg%3E"); 47 | } 48 | 49 | .bi-lock-nav-menu { 50 | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-list-nested' viewBox='0 0 16 16'%3E%3Cpath d='M8 1a2 2 0 0 1 2 2v4H6V3a2 2 0 0 1 2-2zm3 6V3a3 3 0 0 0-6 0v4a2 2 0 0 0-2 2v5a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2zM5 8h6a1 1 0 0 1 1 1v5a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V9a1 1 0 0 1 1-1z'/%3E%3C/svg%3E"); 51 | } 52 | 53 | .bi-person-nav-menu { 54 | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-person' viewBox='0 0 16 16'%3E%3Cpath d='M8 8a3 3 0 1 0 0-6 3 3 0 0 0 0 6Zm2-3a2 2 0 1 1-4 0 2 2 0 0 1 4 0Zm4 8c0 1-1 1-1 1H3s-1 0-1-1 1-4 6-4 6 3 6 4Zm-1-.004c-.001-.246-.154-.986-.832-1.664C11.516 10.68 10.289 10 8 10c-2.29 0-3.516.68-4.168 1.332-.678.678-.83 1.418-.832 1.664h10Z'/%3E%3C/svg%3E"); 55 | } 56 | 57 | .bi-person-badge-nav-menu { 58 | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-person-badge' viewBox='0 0 16 16'%3E%3Cpath d='M6.5 2a.5.5 0 0 0 0 1h3a.5.5 0 0 0 0-1h-3zM11 8a3 3 0 1 1-6 0 3 3 0 0 1 6 0z'/%3E%3Cpath d='M4.5 0A2.5 2.5 0 0 0 2 2.5V14a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V2.5A2.5 2.5 0 0 0 11.5 0h-7zM3 2.5A1.5 1.5 0 0 1 4.5 1h7A1.5 1.5 0 0 1 13 2.5v10.795a4.2 4.2 0 0 0-.776-.492C11.392 12.387 10.063 12 8 12s-3.392.387-4.224.803a4.2 4.2 0 0 0-.776.492V2.5z'/%3E%3C/svg%3E"); 59 | } 60 | 61 | .bi-person-fill-nav-menu { 62 | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-person-fill' viewBox='0 0 16 16'%3E%3Cpath d='M3 14s-1 0-1-1 1-4 6-4 6 3 6 4-1 1-1 1H3Zm5-6a3 3 0 1 0 0-6 3 3 0 0 0 0 6Z'/%3E%3C/svg%3E"); 63 | } 64 | 65 | .bi-arrow-bar-left-nav-menu { 66 | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-arrow-bar-left' viewBox='0 0 16 16'%3E%3Cpath d='M12.5 15a.5.5 0 0 1-.5-.5v-13a.5.5 0 0 1 1 0v13a.5.5 0 0 1-.5.5ZM10 8a.5.5 0 0 1-.5.5H3.707l2.147 2.146a.5.5 0 0 1-.708.708l-3-3a.5.5 0 0 1 0-.708l3-3a.5.5 0 1 1 .708.708L3.707 7.5H9.5a.5.5 0 0 1 .5.5Z'/%3E%3C/svg%3E"); 67 | } 68 | 69 | .nav-item { 70 | font-size: 0.9rem; 71 | padding-bottom: 0.5rem; 72 | } 73 | 74 | .nav-item:first-of-type { 75 | padding-top: 1rem; 76 | } 77 | 78 | .nav-item:last-of-type { 79 | padding-bottom: 1rem; 80 | } 81 | 82 | .nav-item ::deep .nav-link { 83 | color: #d7d7d7; 84 | background: none; 85 | border: none; 86 | border-radius: 4px; 87 | height: 3rem; 88 | display: flex; 89 | align-items: center; 90 | line-height: 3rem; 91 | width: 100%; 92 | } 93 | 94 | .nav-item ::deep a.active { 95 | background-color: rgba(255,255,255,0.37); 96 | color: white; 97 | } 98 | 99 | .nav-item ::deep .nav-link:hover { 100 | background-color: rgba(255,255,255,0.1); 101 | color: white; 102 | } 103 | 104 | .nav-scrollable { 105 | display: none; 106 | } 107 | 108 | .navbar-toggler:checked ~ .nav-scrollable { 109 | display: block; 110 | } 111 | 112 | @media (min-width: 641px) { 113 | .navbar-toggler { 114 | display: none; 115 | } 116 | 117 | .nav-scrollable { 118 | /* Never collapse the sidebar for wide screens */ 119 | display: block; 120 | 121 | /* Allow sidebar to scroll for tall menus */ 122 | height: calc(100vh - 3.5rem); 123 | overflow-y: auto; 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /CleanBlazorWeb/CleanBlazorWeb/Components/Pages/Error.razor: -------------------------------------------------------------------------------- 1 | @page "/Error" 2 | @using System.Diagnostics 3 | 4 | Error 5 | 6 |

Error.

7 |

An error occurred while processing your request.

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

12 | Request ID: @RequestId 13 |

14 | } 15 | 16 |

Development Mode

17 |

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

20 |

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

26 | 27 | @code{ 28 | [CascadingParameter] 29 | private HttpContext? HttpContext { get; set; } 30 | 31 | private string? RequestId { get; set; } 32 | private bool ShowRequestId => !string.IsNullOrEmpty(RequestId); 33 | 34 | protected override void OnInitialized() => 35 | RequestId = Activity.Current?.Id ?? HttpContext?.TraceIdentifier; 36 | } 37 | -------------------------------------------------------------------------------- /CleanBlazorWeb/CleanBlazorWeb/Components/Pages/Home.razor: -------------------------------------------------------------------------------- 1 | @page "/" 2 | 3 | Home 4 | 5 |

Hello, world!

6 | 7 | Welcome to your new app. 8 | -------------------------------------------------------------------------------- /CleanBlazorWeb/CleanBlazorWeb/Components/Pages/Weather.razor: -------------------------------------------------------------------------------- 1 | @page "/weather" 2 | @attribute [StreamRendering] 3 | 4 | Weather 5 | 6 |

Weather

7 | 8 |

This component demonstrates showing data.

9 | 10 | @if (forecasts == null) 11 | { 12 |

Loading...

13 | } 14 | else 15 | { 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | @foreach (var forecast in forecasts) 27 | { 28 | 29 | 30 | 31 | 32 | 33 | 34 | } 35 | 36 |
DateTemp. (C)Temp. (F)Summary
@forecast.Date.ToShortDateString()@forecast.TemperatureC@forecast.TemperatureF@forecast.Summary
37 | } 38 | 39 | @code { 40 | private WeatherForecast[]? forecasts; 41 | 42 | protected override async Task OnInitializedAsync() 43 | { 44 | // Simulate asynchronous loading to demonstrate streaming rendering 45 | await Task.Delay(500); 46 | 47 | var startDate = DateOnly.FromDateTime(DateTime.Now); 48 | var summaries = new[] { "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" }; 49 | forecasts = Enumerable.Range(1, 5).Select(index => new WeatherForecast 50 | { 51 | Date = startDate.AddDays(index), 52 | TemperatureC = Random.Shared.Next(-20, 55), 53 | Summary = summaries[Random.Shared.Next(summaries.Length)] 54 | }).ToArray(); 55 | } 56 | 57 | private class WeatherForecast 58 | { 59 | public DateOnly Date { get; set; } 60 | public int TemperatureC { get; set; } 61 | public string? Summary { get; set; } 62 | public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /CleanBlazorWeb/CleanBlazorWeb/Components/Routes.razor: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /CleanBlazorWeb/CleanBlazorWeb/Components/_Imports.razor: -------------------------------------------------------------------------------- 1 | @using System.Net.Http 2 | @using System.Net.Http.Json 3 | @using Microsoft.AspNetCore.Components.Authorization 4 | @using Microsoft.AspNetCore.Components.Forms 5 | @using Microsoft.AspNetCore.Components.Routing 6 | @using Microsoft.AspNetCore.Components.Web 7 | @using static Microsoft.AspNetCore.Components.Web.RenderMode 8 | @using Microsoft.AspNetCore.Components.Web.Virtualization 9 | @using Microsoft.JSInterop 10 | @using CleanBlazorWeb 11 | @using CleanBlazorWeb.Client 12 | @using CleanBlazorWeb.Components 13 | -------------------------------------------------------------------------------- /CleanBlazorWeb/CleanBlazorWeb/Data/ApplicationDbContext.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Identity.EntityFrameworkCore; 2 | using Microsoft.EntityFrameworkCore; 3 | 4 | namespace CleanBlazorWeb.Data 5 | { 6 | public class ApplicationDbContext(DbContextOptions options) : IdentityDbContext(options) 7 | { 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /CleanBlazorWeb/CleanBlazorWeb/Data/ApplicationUser.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Identity; 2 | 3 | namespace CleanBlazorWeb.Data 4 | { 5 | // Add profile data for application users by adding properties to the ApplicationUser class 6 | public class ApplicationUser : IdentityUser 7 | { 8 | } 9 | 10 | } 11 | -------------------------------------------------------------------------------- /CleanBlazorWeb/CleanBlazorWeb/Program.cs: -------------------------------------------------------------------------------- 1 | using CleanBlazorWeb.Client.Pages; 2 | using CleanBlazorWeb.Components; 3 | using CleanBlazorWeb.Components.Account; 4 | using CleanBlazorWeb.Data; 5 | using Microsoft.AspNetCore.Components.Authorization; 6 | using Microsoft.AspNetCore.Identity; 7 | using Microsoft.EntityFrameworkCore; 8 | 9 | namespace CleanBlazorWeb 10 | { 11 | public class Program 12 | { 13 | public static void Main(string[] args) 14 | { 15 | var builder = WebApplication.CreateBuilder(args); 16 | 17 | // Add services to the container. 18 | builder.Services.AddRazorComponents() 19 | .AddInteractiveServerComponents() 20 | .AddInteractiveWebAssemblyComponents(); 21 | 22 | builder.Services.AddCascadingAuthenticationState(); 23 | builder.Services.AddScoped(); 24 | builder.Services.AddScoped(); 25 | builder.Services.AddScoped(); 26 | 27 | builder.Services.AddAuthentication(options => 28 | { 29 | options.DefaultScheme = IdentityConstants.ApplicationScheme; 30 | options.DefaultSignInScheme = IdentityConstants.ExternalScheme; 31 | }) 32 | .AddIdentityCookies(); 33 | 34 | var connectionString = builder.Configuration.GetConnectionString("DefaultConnection") ?? throw new InvalidOperationException("Connection string 'DefaultConnection' not found."); 35 | builder.Services.AddDbContext(options => 36 | options.UseSqlServer(connectionString)); 37 | builder.Services.AddDatabaseDeveloperPageExceptionFilter(); 38 | 39 | builder.Services.AddIdentityCore(options => options.SignIn.RequireConfirmedAccount = true) 40 | .AddEntityFrameworkStores() 41 | .AddSignInManager() 42 | .AddDefaultTokenProviders(); 43 | 44 | builder.Services.AddSingleton, IdentityNoOpEmailSender>(); 45 | 46 | var app = builder.Build(); 47 | 48 | // Configure the HTTP request pipeline. 49 | if (app.Environment.IsDevelopment()) 50 | { 51 | app.UseWebAssemblyDebugging(); 52 | app.UseMigrationsEndPoint(); 53 | } 54 | else 55 | { 56 | app.UseExceptionHandler("/Error"); 57 | // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. 58 | app.UseHsts(); 59 | } 60 | 61 | app.UseHttpsRedirection(); 62 | 63 | app.UseStaticFiles(); 64 | app.UseAntiforgery(); 65 | 66 | app.MapRazorComponents() 67 | .AddInteractiveServerRenderMode() 68 | .AddInteractiveWebAssemblyRenderMode() 69 | .AddAdditionalAssemblies(typeof(Client._Imports).Assembly); 70 | 71 | // Add additional endpoints required by the Identity /Account Razor components. 72 | app.MapAdditionalIdentityEndpoints(); 73 | 74 | app.Run(); 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /CleanBlazorWeb/CleanBlazorWeb/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:62320", 8 | "sslPort": 44393 9 | } 10 | }, 11 | "profiles": { 12 | "http": { 13 | "commandName": "Project", 14 | "dotnetRunMessages": true, 15 | "launchBrowser": true, 16 | "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", 17 | "applicationUrl": "http://localhost:5101", 18 | "environmentVariables": { 19 | "ASPNETCORE_ENVIRONMENT": "Development" 20 | } 21 | }, 22 | "https": { 23 | "commandName": "Project", 24 | "dotnetRunMessages": true, 25 | "launchBrowser": true, 26 | "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", 27 | "applicationUrl": "https://localhost:7135;http://localhost:5101", 28 | "environmentVariables": { 29 | "ASPNETCORE_ENVIRONMENT": "Development" 30 | } 31 | }, 32 | "IIS Express": { 33 | "commandName": "IISExpress", 34 | "launchBrowser": true, 35 | "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", 36 | "environmentVariables": { 37 | "ASPNETCORE_ENVIRONMENT": "Development" 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /CleanBlazorWeb/CleanBlazorWeb/Properties/serviceDependencies.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "mssql1": { 4 | "type": "mssql", 5 | "connectionId": "ConnectionStrings:DefaultConnection" 6 | } 7 | } 8 | } -------------------------------------------------------------------------------- /CleanBlazorWeb/CleanBlazorWeb/Properties/serviceDependencies.local.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "mssql1": { 4 | "type": "mssql.local", 5 | "connectionId": "ConnectionStrings:DefaultConnection" 6 | } 7 | } 8 | } -------------------------------------------------------------------------------- /CleanBlazorWeb/CleanBlazorWeb/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /CleanBlazorWeb/CleanBlazorWeb/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "ConnectionStrings": { 3 | "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=aspnet-CleanBlazorWeb-982fc812-1868-4151-8b78-d2a30abf3a8c;Trusted_Connection=True;MultipleActiveResultSets=true" 4 | }, 5 | "Logging": { 6 | "LogLevel": { 7 | "Default": "Information", 8 | "Microsoft.AspNetCore": "Warning" 9 | } 10 | }, 11 | "AllowedHosts": "*" 12 | } 13 | -------------------------------------------------------------------------------- /CleanBlazorWeb/CleanBlazorWeb/wwwroot/app.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; 3 | } 4 | 5 | a, .btn-link { 6 | color: #006bb7; 7 | } 8 | 9 | .btn-primary { 10 | color: #fff; 11 | background-color: #1b6ec2; 12 | border-color: #1861ac; 13 | } 14 | 15 | .btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus { 16 | box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb; 17 | } 18 | 19 | .content { 20 | padding-top: 1.1rem; 21 | } 22 | 23 | h1:focus { 24 | outline: none; 25 | } 26 | 27 | .valid.modified:not([type=checkbox]) { 28 | outline: 1px solid #26b050; 29 | } 30 | 31 | .invalid { 32 | outline: 1px solid #e50000; 33 | } 34 | 35 | .validation-message { 36 | color: #e50000; 37 | } 38 | 39 | .blazor-error-boundary { 40 | background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121; 41 | padding: 1rem 1rem 1rem 3.7rem; 42 | color: white; 43 | } 44 | 45 | .blazor-error-boundary::after { 46 | content: "An error has occurred." 47 | } 48 | 49 | .darker-border-checkbox.form-check-input { 50 | border-color: #929292; 51 | } 52 | -------------------------------------------------------------------------------- /CleanBlazorWeb/CleanBlazorWeb/wwwroot/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rijwanansari/CleanArchitectureDOTNET8BlazorWeb/5289294f24b4ce4ed542c9ccbeebe63df57f75b5/CleanBlazorWeb/CleanBlazorWeb/wwwroot/favicon.png -------------------------------------------------------------------------------- /Domain/Common/AppConstants.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace Domain.Common 8 | { 9 | public static class AppConstants 10 | { 11 | // Roles 12 | public static string ROLE_ADMIN = "Admin"; 13 | public static string ROLE_EMPLOYEE = "Employee"; 14 | public static string ROLE_NON_LTP = "Non-LTP"; 15 | public static string ROLE_MANAGER = "Manager"; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Domain/Common/AuditableWithBaseEntity.cs: -------------------------------------------------------------------------------- 1 | namespace Domain.Common 2 | { 3 | public abstract class AuditableWithBaseEntity : BaseEntity, IAuditableEntity 4 | { 5 | public bool IsDeleted { get; set; } = false; 6 | public DateTime Created { get; set; } 7 | public string Author { get; set; } 8 | public DateTime Modified { get; set; } 9 | public string Editor { get; set; } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Domain/Common/BaseEntity.cs: -------------------------------------------------------------------------------- 1 | namespace Domain.Common 2 | { 3 | public abstract class BaseEntity 4 | { 5 | public virtual T Id { get; set; } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /Domain/Common/IAuditableEntity.cs: -------------------------------------------------------------------------------- 1 | namespace Domain.Common 2 | { 3 | public interface IAuditableEntity 4 | { 5 | bool IsDeleted { get; set; } 6 | DateTime Created { get; set; } 7 | string Author { get; set; } 8 | DateTime Modified { get; set; } 9 | string Editor { get; set; } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Domain/Common/IEntity.cs: -------------------------------------------------------------------------------- 1 | namespace Domain.Common 2 | { 3 | public interface IEntity : IEntity 4 | { 5 | public TId Id { get; set; } 6 | } 7 | public interface IEntity 8 | { 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Domain/Common/PaginatedList.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace Domain.Common 8 | { 9 | public class PaginatedList : List 10 | { 11 | public int CurrentPage { get; set; } 12 | public int TotalPages { get; set; } 13 | public int PageSize { get; set; } 14 | public int TotalCount { get; set; } 15 | 16 | public PaginatedList(IEnumerable currentPage, int count, int pageNumber, int pageSize) 17 | { 18 | CurrentPage = pageNumber; 19 | TotalPages = (int)Math.Ceiling(count / (double)pageSize); 20 | PageSize = pageSize; 21 | TotalCount = count; 22 | AddRange(currentPage); 23 | } 24 | 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Domain/Domain.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /Domain/Master/AppSetting.cs: -------------------------------------------------------------------------------- 1 | global using Domain.Common; 2 | 3 | namespace Domain.Master 4 | { 5 | public class AppSetting : BaseEntity 6 | { 7 | /// 8 | /// Gets or sets the ReferenceKey 9 | /// 10 | public string ReferenceKey { get; set; } = String.Empty; 11 | /// 12 | /// Gets or sets the Value 13 | /// 14 | public string Value { get; set; } = String.Empty; 15 | /// 16 | /// Gets or sets the Description 17 | /// 18 | public string? Description { get; set; } 19 | /// 20 | /// Gets or sets the Type 21 | /// 22 | public string Type { get; set; } = String.Empty; 23 | public DateTime Created { get; set; } 24 | public string Author { get; set; } 25 | public DateTime Modified { get; set; } 26 | public string Editor { get; set; } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Domain/Master/ReferenceField.cs: -------------------------------------------------------------------------------- 1 | namespace Domain.Master 2 | { 3 | public class ReferenceField : AuditableWithBaseEntity 4 | { 5 | public string Title { get; set; } 6 | public string ReferenceType { get; set; } 7 | 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Domain/Master/UserAccess.cs: -------------------------------------------------------------------------------- 1 | namespace Domain.Master 2 | { 3 | public class UserAccess : AuditableWithBaseEntity 4 | { 5 | public string UserEmail { get; set; } = string.Empty; 6 | public string? UserName { get; set; } 7 | public UserType UserType { get; set; } 8 | } 9 | public enum UserType 10 | { 11 | Administrator, 12 | MMUser, 13 | FIUser, 14 | ReadOnly 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Infrastructure/DependencyInjection.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Configuration; 2 | using Microsoft.Extensions.DependencyInjection; 3 | 4 | namespace Infrastructure; 5 | 6 | public static class DependencyInjection 7 | { 8 | //public static IServiceCollection AddPersistence(this IServiceCollection services, IConfiguration configuration) 9 | //{ 10 | // services.AddScoped(provider => provider.GetService()); 11 | // services.AddScoped(typeof(IRepository<>), typeof(EfRepository<>)); 12 | // services.AddScoped(); 13 | // services.AddSingleton(); 14 | // services.AddScoped(); 15 | 16 | // return services; 17 | //} 18 | } 19 | -------------------------------------------------------------------------------- /Infrastructure/Identity/ApplicationUser.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Identity; 2 | 3 | namespace Infrastructure.Identity 4 | { 5 | public class ApplicationUser : IdentityUser 6 | { 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Infrastructure/Identity/UserService.cs: -------------------------------------------------------------------------------- 1 | using Application.Common.Interface; 2 | using Application.Common.Model; 3 | using Microsoft.AspNetCore.Identity; 4 | using Microsoft.EntityFrameworkCore; 5 | 6 | namespace Infrastructure.Identity 7 | { 8 | public class UserService : IUserService 9 | { 10 | private readonly UserManager _userManager; 11 | private readonly ICurrentUserService _currentUserService; 12 | 13 | public UserService(UserManager userManager, ICurrentUserService currentUserService) 14 | { 15 | _userManager = userManager; 16 | _currentUserService = currentUserService; 17 | } 18 | 19 | public async Task> GetUserByIdAsync(string userId) 20 | { 21 | var user = await _userManager.FindByIdAsync(userId); 22 | if (user == null) 23 | { 24 | return ResponseModel.FailureResponse("User not found"); 25 | } 26 | var usermodel = new UserModel 27 | { 28 | UserId = user.Id, 29 | Email = user.Email, 30 | PhoneNumber = user.PhoneNumber, 31 | UserName = user.UserName, 32 | EmailConfirmed = user.EmailConfirmed 33 | }; 34 | return ResponseModel.SuccessResponse("User found", usermodel); 35 | } 36 | 37 | public async Task> GetUserByEmailAsync(string email) 38 | { 39 | var user = await _userManager.FindByEmailAsync(email); 40 | if (user == null) 41 | { 42 | return ResponseModel.FailureResponse("User not found"); 43 | } 44 | var usermodel = new UserModel 45 | { 46 | UserId = user.Id, 47 | Email = user.Email, 48 | PhoneNumber = user.PhoneNumber, 49 | UserName = user.UserName, 50 | EmailConfirmed = user.EmailConfirmed 51 | }; 52 | return ResponseModel.SuccessResponse("User found", usermodel); 53 | } 54 | 55 | public async Task>> GetAllUsers() 56 | { 57 | var users = await _userManager.Users.ToListAsync(); 58 | if (users == null) 59 | { 60 | return ResponseModel>.FailureResponse("User not found"); 61 | } 62 | return new ResponseModel> 63 | { 64 | Success = true, 65 | Output = users.Select(user => new UserModel 66 | { 67 | UserId = user.Id, 68 | Email = user.Email, 69 | PhoneNumber = user.PhoneNumber, 70 | UserName = user.UserName, 71 | EmailConfirmed = user.EmailConfirmed 72 | }).ToList() 73 | }; 74 | } 75 | 76 | public Task> GetCurrentUser() 77 | { 78 | throw new NotImplementedException(); 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /Infrastructure/Infrastructure.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | all 14 | runtime; build; native; contentfiles; analyzers; buildtransitive 15 | 16 | 17 | 18 | all 19 | runtime; build; native; contentfiles; analyzers; buildtransitive 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /Infrastructure/LogCapture/ErrorMessageLog.cs: -------------------------------------------------------------------------------- 1 | using Application.Common.Error; 2 | 3 | namespace Infrastructure.LogCapture 4 | { 5 | internal class ErrorMessageLog : IErrorMessageLog 6 | { 7 | public bool LogError(string layerName, string className, string methodName, string msg) 8 | { 9 | try 10 | { 11 | string fullPath = System.AppDomain.CurrentDomain.BaseDirectory.ToString(); 12 | DateTime dtNow = DateTime.Now; 13 | string date = dtNow.Year + "-" + dtNow.Month + "-" + dtNow.Day; 14 | 15 | fullPath = fullPath + "Logs"; 16 | if (!Directory.Exists(fullPath)) 17 | { 18 | Directory.CreateDirectory(fullPath); 19 | } 20 | 21 | string filePath = fullPath + "\\" + date + "_ErrorLog.txt"; 22 | if (!File.Exists(filePath)) 23 | { 24 | TextWriter sw = new StreamWriter(filePath); 25 | 26 | sw.WriteLine("Layer Name :-" + layerName); 27 | sw.WriteLine("Class Name :-" + className); 28 | sw.WriteLine("Method Name :-" + methodName); 29 | sw.WriteLine("Date Time :-" + DateTime.Now); 30 | sw.WriteLine("Error Message :-" + msg); 31 | 32 | sw.Close(); 33 | } 34 | else 35 | { 36 | string oldLine = File.ReadAllText(filePath); 37 | TextWriter tw = new StreamWriter(filePath); 38 | 39 | tw.WriteLine(oldLine); 40 | 41 | tw.WriteLine(tw.NewLine); 42 | 43 | tw.WriteLine("Layer Name :-" + layerName); 44 | tw.WriteLine("Class Name :-" + className); 45 | tw.WriteLine("Method Name :-" + methodName); 46 | tw.WriteLine("Date Time :-" + DateTime.Now); 47 | tw.WriteLine("Error Message :-" + msg); 48 | tw.Close(); 49 | } 50 | } 51 | catch (Exception) 52 | { 53 | return false; 54 | } 55 | return true; 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Infrastructure/Persistence/ApplicationDBContext.cs: -------------------------------------------------------------------------------- 1 | using Application.Common.Interface; 2 | using Domain.Common; 3 | using Domain.Master; 4 | using Infrastructure.Identity; 5 | using Microsoft.AspNetCore.Identity.EntityFrameworkCore; 6 | using Microsoft.EntityFrameworkCore; 7 | 8 | namespace Infrastructure.Persistence 9 | { 10 | public class ApplicationDBContext(DbContextOptions options, ICurrentUserService currentUserService) : IdentityDbContext(options), IApplicationDBContext 11 | { 12 | 13 | #region Properties 14 | private readonly DateTime _currentDateTime; 15 | private readonly ICurrentUserService _currentUserService = currentUserService; 16 | #endregion 17 | 18 | #region Ctor 19 | //public ApplicationDBContext(DbContextOptions options, ICurrentUserService currentUserService) 20 | // : base(options) 21 | //{ 22 | // _currentDateTime = DateTime.Now; 23 | // _currentUserService = currentUserService; 24 | //} 25 | #endregion 26 | 27 | #region Master 28 | public DbSet AppSettings { get; set; } 29 | 30 | #endregion 31 | 32 | public async Task SaveChangesAsync() 33 | { 34 | 35 | foreach (var entry in ChangeTracker.Entries()) 36 | { 37 | var currentUserID = _currentUserService.UserId; 38 | switch (entry.State) 39 | { 40 | case EntityState.Added: 41 | entry.Entity.Author = currentUserID; //Get Current UsereID 42 | entry.Entity.Created = _currentDateTime; 43 | entry.Entity.Editor = currentUserID; //Get Current UsereID 44 | entry.Entity.Modified = _currentDateTime; 45 | break; 46 | case EntityState.Modified: 47 | entry.Entity.Editor = currentUserID; //Get Current UsereID 48 | entry.Entity.Modified = _currentDateTime; 49 | break; 50 | } 51 | } 52 | return await base.SaveChangesAsync(); 53 | } 54 | 55 | /// 56 | /// Creates a DbSet that can be used to query and save instances of entity 57 | /// 58 | /// Entity type 59 | /// A set for the given entity type 60 | public virtual new DbSet Set() where TEntity : class 61 | { 62 | return base.Set(); 63 | } 64 | 65 | protected override void OnModelCreating(ModelBuilder modelBuilder) 66 | { 67 | modelBuilder.ApplyConfigurationsFromAssembly(typeof(ApplicationDBContext).Assembly); 68 | base.OnModelCreating(modelBuilder); 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Infrastructure/Persistence/Configuration/Master/AppSettingConfiguration.cs: -------------------------------------------------------------------------------- 1 | using Domain.Master; 2 | using Microsoft.EntityFrameworkCore; 3 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 4 | 5 | namespace Infrastructure.Persistence.Configuration.Master 6 | { 7 | public class AppSettingConfiguration : IEntityTypeConfiguration 8 | { 9 | public void Configure(EntityTypeBuilder builder) 10 | { 11 | builder 12 | .ToTable(nameof(AppSetting)); 13 | builder.Property(nameof(AppSetting.ReferenceKey)).HasMaxLength(100).IsRequired(); 14 | builder.Property(nameof(AppSetting.Value)).HasMaxLength(500); 15 | builder.Property(nameof(AppSetting.Description)).HasMaxLength(500).IsRequired(); 16 | builder.Property(nameof(AppSetting.Type)).HasMaxLength(50).IsRequired(); 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Infrastructure/Persistence/EfRepository.cs: -------------------------------------------------------------------------------- 1 | using Application.Common.Interface; 2 | using Domain.Common; 3 | using Microsoft.EntityFrameworkCore; 4 | using System.Linq.Expressions; 5 | 6 | namespace Infrastructure.Persistence 7 | { 8 | public class EfRepository : IRepository where T : class 9 | { 10 | #region Properties 11 | private readonly ApplicationDBContext _context; 12 | 13 | protected DbSet _entities; 14 | #endregion 15 | #region Ctor 16 | 17 | public EfRepository(ApplicationDBContext context) 18 | { 19 | _context = context; 20 | } 21 | 22 | #endregion 23 | #region Utility 24 | protected string GetFullErrorTextAndRollbackEntityChanges(DbUpdateException exception) 25 | { 26 | //rollback entity changes 27 | if (_context is DbContext dbContext) 28 | { 29 | var entries = dbContext.ChangeTracker.Entries() 30 | .Where(e => e.State == EntityState.Added || e.State == EntityState.Modified).ToList(); 31 | 32 | entries.ForEach(entry => 33 | { 34 | try 35 | { 36 | entry.State = EntityState.Unchanged; 37 | } 38 | catch (InvalidOperationException) 39 | { 40 | // ignored 41 | } 42 | }); 43 | } 44 | 45 | try 46 | { 47 | _context.SaveChanges(); 48 | return exception.ToString(); 49 | } 50 | catch (Exception ex) 51 | { 52 | //if after the rollback of changes the context is still not saving, 53 | //return the full text of the exception that occurred when saving 54 | return ex.ToString(); 55 | } 56 | } 57 | #endregion 58 | 59 | #region Repository Methods 60 | /// 61 | /// Gets an entity set 62 | /// 63 | protected virtual DbSet Entities 64 | { 65 | get 66 | { 67 | if (_entities == null) 68 | _entities = _context.Set(); 69 | 70 | return _entities; 71 | } 72 | } 73 | public async Task AddAsync(T entity) 74 | { 75 | await Entities.AddAsync(entity); 76 | return entity; 77 | } 78 | public virtual void Update(T entity) 79 | { 80 | _context.Entry(entity).State = EntityState.Modified; 81 | } 82 | public Task UpdateAsync(object Id, T entity) 83 | { 84 | T exist = _context.Set().Find(Id); 85 | _context.Entry(exist).CurrentValues.SetValues(entity); 86 | return Task.CompletedTask; 87 | } 88 | public async Task DeleteAsync(object id) 89 | { 90 | var entity = await Entities.FindAsync(id); 91 | if (entity == null) 92 | return false; 93 | 94 | Entities.Remove(entity); 95 | return true; 96 | } 97 | public async Task Delete(T entity) 98 | { 99 | Entities.Remove(entity); 100 | return true; 101 | } 102 | public virtual async Task Delete(object id) 103 | { 104 | var entity = await Entities.FindAsync(id); 105 | if (entity == null) 106 | return false; 107 | 108 | Entities.Remove(entity); 109 | return true; 110 | } 111 | public async Task Delete(Expression> where) 112 | { 113 | var entities = Entities.Where(where); 114 | Entities.RemoveRange(entities); 115 | return true; 116 | } 117 | public async Task Get(object id) 118 | { 119 | return await Entities.FindAsync(id); 120 | } 121 | public async Task Get(Expression> where) 122 | { 123 | return await Entities.FirstOrDefaultAsync(where); 124 | } 125 | public virtual IEnumerable GetAll() 126 | { 127 | return Entities; 128 | } 129 | public virtual IEnumerable GetMany(Expression> where) 130 | { 131 | return Entities.Where(where); 132 | } 133 | public virtual void Save() 134 | { 135 | _context.SaveChanges(); 136 | } 137 | public virtual void SaveAsync() 138 | { 139 | _context.SaveChangesAsync(); 140 | } 141 | public virtual async Task Count() 142 | { 143 | return await Entities.CountAsync(); 144 | 145 | } 146 | public virtual async Task Count(Expression> where) 147 | { 148 | return await Entities.CountAsync(where); 149 | 150 | } 151 | public virtual new DbSet Set() where TEntity : class 152 | { 153 | return _context.Set(); 154 | } 155 | 156 | /// 157 | /// Gets a table 158 | /// 159 | public virtual IQueryable Table => Entities; 160 | 161 | /// 162 | /// Gets a table with "no tracking" enabled (EF feature) Use it only when you load record(s) only for read-only operations 163 | /// 164 | public virtual IQueryable TableNoTracking => Entities.AsNoTracking(); 165 | 166 | #endregion 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /Infrastructure/Persistence/Migrations/20240202065228_initializeDb.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.EntityFrameworkCore.Migrations; 3 | 4 | #nullable disable 5 | 6 | namespace SDMSWeb.Migrations 7 | { 8 | /// 9 | public partial class initializeDb : Migration 10 | { 11 | /// 12 | protected override void Up(MigrationBuilder migrationBuilder) 13 | { 14 | migrationBuilder.CreateTable( 15 | name: "AppSetting", 16 | columns: table => new 17 | { 18 | Id = table.Column(type: "int", nullable: false) 19 | .Annotation("SqlServer:Identity", "1, 1"), 20 | ReferenceKey = table.Column(type: "nvarchar(100)", maxLength: 100, nullable: false), 21 | Value = table.Column(type: "nvarchar(500)", maxLength: 500, nullable: false), 22 | Description = table.Column(type: "nvarchar(500)", maxLength: 500, nullable: false), 23 | Type = table.Column(type: "nvarchar(50)", maxLength: 50, nullable: false) 24 | }, 25 | constraints: table => 26 | { 27 | table.PrimaryKey("PK_AppSetting", x => x.Id); 28 | }); 29 | 30 | migrationBuilder.CreateTable( 31 | name: "ReferenceField", 32 | columns: table => new 33 | { 34 | Id = table.Column(type: "bigint", nullable: false) 35 | .Annotation("SqlServer:Identity", "1, 1"), 36 | Title = table.Column(type: "nvarchar(max)", nullable: false), 37 | ReferenceType = table.Column(type: "nvarchar(max)", nullable: false), 38 | IsDeleted = table.Column(type: "bit", nullable: false), 39 | Created = table.Column(type: "datetime2", nullable: false), 40 | Author = table.Column(type: "nvarchar(max)", nullable: false), 41 | Modified = table.Column(type: "datetime2", nullable: false), 42 | Editor = table.Column(type: "nvarchar(max)", nullable: false) 43 | }, 44 | constraints: table => 45 | { 46 | table.PrimaryKey("PK_ReferenceField", x => x.Id); 47 | }); 48 | 49 | migrationBuilder.CreateTable( 50 | name: "UserAccess", 51 | columns: table => new 52 | { 53 | Id = table.Column(type: "int", nullable: false) 54 | .Annotation("SqlServer:Identity", "1, 1"), 55 | UserEmail = table.Column(type: "nvarchar(100)", maxLength: 100, nullable: false), 56 | UserName = table.Column(type: "nvarchar(100)", maxLength: 100, nullable: true), 57 | UserType = table.Column(type: "nvarchar(100)", maxLength: 100, nullable: false), 58 | IsDeleted = table.Column(type: "bit", nullable: false), 59 | Created = table.Column(type: "datetime2", nullable: false), 60 | Author = table.Column(type: "nvarchar(max)", nullable: false), 61 | Modified = table.Column(type: "datetime2", nullable: false), 62 | Editor = table.Column(type: "nvarchar(max)", nullable: false) 63 | }, 64 | constraints: table => 65 | { 66 | table.PrimaryKey("PK_UserAccess", x => x.Id); 67 | }); 68 | } 69 | 70 | /// 71 | protected override void Down(MigrationBuilder migrationBuilder) 72 | { 73 | migrationBuilder.DropTable( 74 | name: "AppSetting"); 75 | 76 | migrationBuilder.DropTable( 77 | name: "ReferenceField"); 78 | 79 | migrationBuilder.DropTable( 80 | name: "UserAccess"); 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /Infrastructure/Persistence/UnitOfWork.cs: -------------------------------------------------------------------------------- 1 | using Application.Common.Interface; 2 | using Microsoft.EntityFrameworkCore.Storage; 3 | using System.Collections; 4 | 5 | namespace Infrastructure.Persistence 6 | { 7 | internal class UnitOfWork : IUnitOfWork 8 | { 9 | #region Properties 10 | private readonly ApplicationDBContext _context; 11 | IDbContextTransaction dbContextTransaction; 12 | private Hashtable _repositories; 13 | #endregion 14 | #region Ctor 15 | public UnitOfWork(ApplicationDBContext context) 16 | { 17 | _context = context; 18 | } 19 | #endregion 20 | public IRepository Repository() where TEntity : class 21 | { 22 | if (_repositories == null) 23 | _repositories = new Hashtable(); 24 | 25 | var type = typeof(TEntity).Name; 26 | 27 | if (!_repositories.ContainsKey(type)) 28 | { 29 | var repositoryType = typeof(EfRepository<>); 30 | 31 | var repositoryInstance = Activator.CreateInstance(repositoryType.MakeGenericType(typeof(TEntity)), _context); 32 | 33 | _repositories.Add(type, repositoryInstance); 34 | } 35 | 36 | return (IRepository)_repositories[type]; 37 | } 38 | 39 | public async Task SaveAsync() 40 | { 41 | return await _context.SaveChangesAsync(); 42 | } 43 | public int Save() 44 | { 45 | return _context.SaveChanges(); 46 | } 47 | public void BeginTransaction() 48 | { 49 | dbContextTransaction = _context.Database.BeginTransaction(); 50 | } 51 | public void CommitTransaction() 52 | { 53 | if (dbContextTransaction != null) 54 | { 55 | dbContextTransaction.Commit(); 56 | } 57 | } 58 | public void RollbackTransaction() 59 | { 60 | if (dbContextTransaction != null) 61 | { 62 | dbContextTransaction.Rollback(); 63 | } 64 | } 65 | private bool disposed = false; 66 | protected virtual void Dispose(bool disposing) 67 | { 68 | if (!this.disposed) 69 | { 70 | if (disposing) 71 | { 72 | _context.Dispose(); 73 | } 74 | } 75 | this.disposed = true; 76 | } 77 | public void Dispose() 78 | { 79 | Dispose(true); 80 | GC.SuppressFinalize(this); 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) [year] [fullname] 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![LinkedIn][linkedin-shield]][linkedin-url] 2 | 3 | [linkedin-shield]: https://img.shields.io/badge/-LinkedIn-black.svg?style=flat-square&logo=linkedin&colorB=555 4 | [linkedin-url]: https://www.linkedin.com/in/rijwanansari/ 5 | 6 | # Clean Architecture - Blazor Web App with .NET 8 using Entity Framework and Repository Pattern 🏗️ 7 | This is a project sample for **Blazor Wb App** with **.NET 8** following **clean architecture** design pattern using entity framework core and **repository pattern**. This is a ready-to-use solution with example CRUD. There is a complete CRUD example for App Setting. 8 | 9 | ## Tech Stack :muscle: 10 | - .NET 8 11 | - ASP.NET Core 12 | - Blazor Web App 13 | - Microsoft Entity Framework Core 8 14 | - Code-First Approach with Configuration 15 | - AutoMapper 16 | - Generic Repository Pattern 17 | - Dependency Injection 18 | - Microsoft SQL Server Database 19 | - Custom Logs Capture Module 20 | - - Authentication and Authorization - Identity 21 | 22 | ## Learn About Clean Architecture 🔖 23 | - [Clean Architecture Concept](https://rijsat.com/2022/02/01/what-is-clean-architecture/) 24 | - [More on Clean Architecture](https://rijsat.com/?s=clean+architecture) 25 | 26 | ## Down the Roadmap 🚀 27 | - Implement Blazor UI component - Ant Design Blazor 28 | 29 | - UI Enhancement - Blazor UI Components 30 | - Implement Admin LTE Design 31 | - File Upload to Azure Blog Example 32 | - Configuration Extension Class 33 | - Graph API Example 34 | - Add some CRUD with complex Models 35 | - Fluent Validation 36 | - Domain Events Implementation 37 | 38 | ## Contribution 🤝 39 | 40 | This is an open-source project and contributors are what make such project with rich feature to learn, inspire, and motivate. Any contributions you make are **greatly appreciated**. 41 | 42 | 1. Fork the Project 43 | 2. Create your Module Branch (`git checkout -b module/FeatureName`) 44 | 3. Commit your Changes (`git commit -m 'Add some FeatureName'`) 45 | 4. Push to the Branch (`git push origin module/FeatureName`) 46 | 5. Open a Pull Request 47 | 48 | ## License 49 | 50 | Distributed under the MIT License. 51 | 52 | ## Support ⭐ 53 | This project is created for learning and sharing purpose. Additionally, this project can be used in your project. Please share your feedback or issues. Please support with star and share in your network 👌 54 | --------------------------------------------------------------------------------