├── .gitignore ├── .gitmodules ├── LICENSE ├── Modular.rest ├── Modular.sln ├── README.md ├── assets └── template.png ├── docker-compose.yml └── src ├── Bootstrapper └── Modular.Bootstrapper │ ├── Modular.Bootstrapper.csproj │ ├── Program.cs │ ├── Properties │ └── launchSettings.json │ ├── Startup.cs │ ├── appsettings.development.json │ └── appsettings.json ├── Modules ├── First │ └── Modular.Modules.First.Api │ │ ├── Controllers │ │ └── FirstController.cs │ │ ├── FirstModule.cs │ │ ├── Modular.Modules.First.Api.csproj │ │ ├── module.first.development.json │ │ └── module.first.json ├── Second │ └── Modular.Modules.Second.Api │ │ ├── Controllers │ │ └── SecondController.cs │ │ ├── Modular.Modules.Second.Api.csproj │ │ ├── SecondModule.cs │ │ ├── module.second.development.json │ │ └── module.second.json ├── Third │ └── Modular.Modules.Third.Api │ │ ├── Controllers │ │ └── ThirdController.cs │ │ ├── Modular.Modules.Third.Api.csproj │ │ ├── ThirdModule.cs │ │ ├── module.third.development.json │ │ └── module.third.json └── Users │ ├── Modular.Modules.Users.Api │ ├── Controllers │ │ ├── AccountController.cs │ │ ├── BaseController.cs │ │ ├── PasswordController.cs │ │ └── UsersController.cs │ ├── Modular.Modules.Users.Api.csproj │ ├── UsersModule.cs │ ├── module.users.development.json │ └── module.users.json │ ├── Modular.Modules.Users.Core │ ├── Commands │ │ ├── ChangePassword.cs │ │ ├── Handlers │ │ │ ├── ChangePasswordHandler.cs │ │ │ ├── SignInHandler.cs │ │ │ ├── SignOutHandler.cs │ │ │ ├── SignUpHandler.cs │ │ │ └── UpdateUserStateHandler.cs │ │ ├── SignIn.cs │ │ ├── SignOut.cs │ │ ├── SignUp.cs │ │ └── UpdateUserState.cs │ ├── DAL │ │ ├── Configurations │ │ │ ├── RoleConfiguration.cs │ │ │ └── UserConfiguration.cs │ │ ├── Migrations │ │ │ ├── 20210722170747_Users_Init.Designer.cs │ │ │ ├── 20210722170747_Users_Init.cs │ │ │ └── UsersDbContextModelSnapshot.cs │ │ ├── Repositories │ │ │ ├── RoleRepository.cs │ │ │ └── UserRepository.cs │ │ ├── UsersDbContext.cs │ │ ├── UsersInitializer.cs │ │ └── UsersUnitOfWork.cs │ ├── DTO │ │ ├── UserDetailsDto.cs │ │ └── UserDto.cs │ ├── Entities │ │ ├── Role.cs │ │ ├── User.cs │ │ └── UserState.cs │ ├── Events │ │ ├── SignedIn.cs │ │ ├── SignedUp.cs │ │ └── UserStateUpdated.cs │ ├── Exceptions │ │ ├── EmailInUseException.cs │ │ ├── InvalidCredentialsException.cs │ │ ├── InvalidEmailException.cs │ │ ├── InvalidPasswordException.cs │ │ ├── InvalidUserStateException.cs │ │ ├── RoleNotFoundException.cs │ │ ├── SignUpDisabledException.cs │ │ ├── UserNotActiveException.cs │ │ ├── UserNotFoundException.cs │ │ └── UserStateCannotBeChangedException.cs │ ├── Extensions.cs │ ├── Modular.Modules.Users.Core.csproj │ ├── Queries │ │ ├── BrowseUsers.cs │ │ ├── GetUser.cs │ │ └── Handlers │ │ │ ├── BrowseUsersHandler.cs │ │ │ ├── Extensions.cs │ │ │ └── GetUserHandler.cs │ ├── RegistrationOptions.cs │ ├── Repositories │ │ ├── IRoleRepository.cs │ │ └── IUserRepository.cs │ └── Services │ │ ├── IUserRequestStorage.cs │ │ └── UserRequestStorage.cs │ └── Users.rest └── Shared └── Modular.Shared.Tests ├── AuthHelper.cs ├── DbHelper.cs ├── Modular.Shared.Tests.csproj ├── OptionsHelper.cs ├── TestApplicationFactory.cs └── WebApiTestBase.cs /.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 | *.suo 8 | *.user 9 | *.userosscache 10 | *.sln.docstates 11 | 12 | # User-specific files (MonoDevelop/Xamarin Studio) 13 | *.userprefs 14 | 15 | # Build results 16 | [Dd]ebug/ 17 | [Dd]ebugPublic/ 18 | [Rr]elease/ 19 | [Rr]eleases/ 20 | x64/ 21 | x86/ 22 | bld/ 23 | [Bb]in/ 24 | [Oo]bj/ 25 | [Ll]og/ 26 | 27 | # Visual Studio 2015/2017 cache/options directory 28 | .vs/ 29 | # Uncomment if you have tasks that create the project's static files in wwwroot 30 | wwwroot/ 31 | 32 | # Visual Studio 2017 auto generated files 33 | Generated\ Files/ 34 | 35 | # MSTest test Results 36 | [Tt]est[Rr]esult*/ 37 | [Bb]uild[Ll]og.* 38 | 39 | # NUNIT 40 | *.VisualState.xml 41 | TestResult.xml 42 | 43 | # Build Results of an ATL Project 44 | [Dd]ebugPS/ 45 | [Rr]eleasePS/ 46 | dlldata.c 47 | 48 | # Benchmark Results 49 | BenchmarkDotNet.Artifacts/ 50 | 51 | # .NET Core 52 | project.lock.json 53 | project.fragment.lock.json 54 | artifacts/ 55 | # **/Properties/launchSettings.json 56 | 57 | # StyleCop 58 | StyleCopReport.xml 59 | 60 | # Files built by Visual Studio 61 | *_i.c 62 | *_p.c 63 | *_i.h 64 | *.ilk 65 | *.meta 66 | *.obj 67 | *.iobj 68 | *.pch 69 | *.pdb 70 | *.ipdb 71 | *.pgc 72 | *.pgd 73 | *.rsp 74 | *.sbr 75 | *.tlb 76 | *.tli 77 | *.tlh 78 | *.tmp 79 | *.tmp_proj 80 | *.log 81 | *.vspscc 82 | *.vssscc 83 | .builds 84 | *.pidb 85 | *.svclog 86 | *.scc 87 | 88 | # Chutzpah Test files 89 | _Chutzpah* 90 | 91 | # Visual C++ cache files 92 | ipch/ 93 | *.aps 94 | *.ncb 95 | *.opendb 96 | *.opensdf 97 | *.sdf 98 | *.cachefile 99 | *.VC.db 100 | *.VC.VC.opendb 101 | 102 | # Visual Studio profiler 103 | *.psess 104 | *.vsp 105 | *.vspx 106 | *.sap 107 | 108 | # Visual Studio Trace Files 109 | *.e2e 110 | 111 | # TFS 2012 Local Workspace 112 | $tf/ 113 | 114 | # Guidance Automation Toolkit 115 | *.gpState 116 | 117 | # ReSharper is a .NET coding add-in 118 | _ReSharper*/ 119 | *.[Rr]e[Ss]harper 120 | *.DotSettings.user 121 | 122 | # JustCode is a .NET coding add-in 123 | .JustCode 124 | 125 | # TeamCity is a build add-in 126 | _TeamCity* 127 | 128 | # DotCover is a Code Coverage Tool 129 | *.dotCover 130 | 131 | # AxoCover is a Code Coverage Tool 132 | .axoCover/* 133 | !.axoCover/settings.json 134 | 135 | # Visual Studio code coverage results 136 | *.coverage 137 | *.coveragexml 138 | 139 | # NCrunch 140 | _NCrunch_* 141 | .*crunch*.local.xml 142 | nCrunchTemp_* 143 | 144 | # MightyMoose 145 | *.mm.* 146 | AutoTest.Net/ 147 | 148 | # Web workbench (sass) 149 | .sass-cache/ 150 | 151 | # Installshield output folder 152 | [Ee]xpress/ 153 | 154 | # DocProject is a documentation generator add-in 155 | DocProject/buildhelp/ 156 | DocProject/Help/*.HxT 157 | DocProject/Help/*.HxC 158 | DocProject/Help/*.hhc 159 | DocProject/Help/*.hhk 160 | DocProject/Help/*.hhp 161 | DocProject/Help/Html2 162 | DocProject/Help/html 163 | 164 | # Click-Once directory 165 | publish/ 166 | 167 | # Publish Web Output 168 | *.[Pp]ublish.xml 169 | *.azurePubxml 170 | # Note: Comment the next line if you want to checkin your web deploy settings, 171 | # but database connection strings (with potential passwords) will be unencrypted 172 | *.pubxml 173 | *.publishproj 174 | 175 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 176 | # checkin your Azure Web App publish settings, but sensitive information contained 177 | # in these scripts will be unencrypted 178 | PublishScripts/ 179 | 180 | # NuGet Packages 181 | *.nupkg 182 | # The packages folder can be ignored because of Package Restore 183 | **/[Pp]ackages/* 184 | # except build/, which is used as an MSBuild target. 185 | !**/[Pp]ackages/build/ 186 | # Uncomment if necessary however generally it will be regenerated when needed 187 | #!**/[Pp]ackages/repositories.config 188 | # NuGet v3's project.json files produces more ignorable files 189 | *.nuget.props 190 | *.nuget.targets 191 | 192 | # Microsoft Azure Build Output 193 | csx/ 194 | *.build.csdef 195 | 196 | # Microsoft Azure Emulator 197 | ecf/ 198 | rcf/ 199 | 200 | # Windows Store app package directories and files 201 | AppPackages/ 202 | BundleArtifacts/ 203 | Package.StoreAssociation.xml 204 | _pkginfo.txt 205 | *.appx 206 | 207 | # Visual Studio cache files 208 | # files ending in .cache can be ignored 209 | *.[Cc]ache 210 | # but keep track of directories ending in .cache 211 | !*.[Cc]ache/ 212 | 213 | # Others 214 | ClientBin/ 215 | ~$* 216 | *~ 217 | *.dbmdl 218 | *.dbproj.schemaview 219 | *.jfm 220 | *.publishsettings 221 | orleans.codegen.cs 222 | 223 | # Including strong name files can present a security risk 224 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 225 | #*.snk 226 | 227 | # Since there are multiple workflows, uncomment next line to ignore bower_components 228 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 229 | #bower_components/ 230 | 231 | # RIA/Silverlight projects 232 | Generated_Code/ 233 | 234 | # Backup & report files from converting an old project file 235 | # to a newer Visual Studio version. Backup files are not needed, 236 | # because we have git ;-) 237 | _UpgradeReport_Files/ 238 | Backup*/ 239 | UpgradeLog*.XML 240 | UpgradeLog*.htm 241 | ServiceFabricBackup/ 242 | *.rptproj.bak 243 | 244 | # SQL Server files 245 | *.mdf 246 | *.ldf 247 | *.ndf 248 | 249 | # Business Intelligence projects 250 | *.rdl.data 251 | *.bim.layout 252 | *.bim_*.settings 253 | *.rptproj.rsuser 254 | 255 | # Microsoft Fakes 256 | FakesAssemblies/ 257 | 258 | # GhostDoc plugin setting file 259 | *.GhostDoc.xml 260 | 261 | # Node.js Tools for Visual Studio 262 | .ntvs_analysis.dat 263 | node_modules/ 264 | 265 | # Visual Studio 6 build log 266 | *.plg 267 | 268 | # Visual Studio 6 workspace options file 269 | *.opt 270 | 271 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 272 | *.vbw 273 | 274 | # Visual Studio LightSwitch build output 275 | **/*.HTMLClient/GeneratedArtifacts 276 | **/*.DesktopClient/GeneratedArtifacts 277 | **/*.DesktopClient/ModelManifest.xml 278 | **/*.Server/GeneratedArtifacts 279 | **/*.Server/ModelManifest.xml 280 | _Pvt_Extensions 281 | 282 | # Paket dependency manager 283 | .paket/paket.exe 284 | paket-files/ 285 | 286 | # FAKE - F# Make 287 | .fake/ 288 | 289 | # JetBrains Rider 290 | .idea/ 291 | *.sln.iml 292 | 293 | # CodeRush 294 | .cr/ 295 | 296 | # Python Tools for Visual Studio (PTVS) 297 | __pycache__/ 298 | *.pyc 299 | 300 | # Cake - Uncomment if you are using it 301 | # tools/** 302 | # !tools/packages.config 303 | 304 | # Tabs Studio 305 | *.tss 306 | 307 | # Telerik's JustMock configuration file 308 | *.jmconfig 309 | 310 | # BizTalk build output 311 | *.btp.cs 312 | *.btm.cs 313 | *.odx.cs 314 | *.xsd.cs 315 | 316 | # OpenCover UI analysis results 317 | OpenCover/ 318 | 319 | # Azure Stream Analytics local run output 320 | ASALocalRun/ 321 | 322 | # MSBuild Binary and Structured Log 323 | *.binlog 324 | 325 | # NVidia Nsight GPU debugger configuration file 326 | *.nvuser 327 | 328 | # MFractors (Xamarin productivity tool) working folder 329 | .mfractor/ 330 | 331 | logs/ 332 | 333 | .vscode/ 334 | 335 | .DS_Store 336 | 337 | appsettings.local.json 338 | module.*.local.json -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "src/modular-framework"] 2 | path = src/modular-framework 3 | url = git@github.com:devmentors/modular-framework.git 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 DevMentors 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 | -------------------------------------------------------------------------------- /Modular.rest: -------------------------------------------------------------------------------- 1 | @url = http://localhost:5000 2 | 3 | ### 4 | GET {{url}} 5 | 6 | ### 7 | GET {{url}}/first 8 | 9 | ### 10 | GET {{url}}/second 11 | 12 | ### 13 | GET {{url}}/third 14 | 15 | ### 16 | GET {{url}}/users -------------------------------------------------------------------------------- /Modular.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.6.30114.105 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Shared", "Shared", "{FCA8CA8F-EE29-4CEA-8627-5EA7638B7C04}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Modular.Shared.Tests", "src\Shared\Modular.Shared.Tests\Modular.Shared.Tests.csproj", "{3C49875F-386E-4C94-A80C-0C75C7A83BCB}" 9 | EndProject 10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Bootstrapper", "Bootstrapper", "{110B9A2A-E39B-4883-ACB6-7E05CE77AF9C}" 11 | EndProject 12 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Modular.Bootstrapper", "src\Bootstrapper\Modular.Bootstrapper\Modular.Bootstrapper.csproj", "{F478360B-DE13-4F0B-9649-C7D4F8539D28}" 13 | EndProject 14 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Modules", "Modules", "{1BA5712D-3DD3-4246-A396-D732B128CB61}" 15 | EndProject 16 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "First", "First", "{CEAF1A2F-AFD5-4545-9E3D-53126DF1AB7F}" 17 | EndProject 18 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Second", "Second", "{D97D0FDB-D5AC-4223-9B74-171EA8ABAFD7}" 19 | EndProject 20 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Third", "Third", "{A84DB51C-E96F-42B6-9CC2-21E52E76DACD}" 21 | EndProject 22 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Users", "Users", "{3C812292-89CC-482D-BC00-3DD04CA4496B}" 23 | EndProject 24 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Modular.Modules.Users.Api", "src\Modules\Users\Modular.Modules.Users.Api\Modular.Modules.Users.Api.csproj", "{3DBF494E-0153-495A-85B7-D23FBEDF5B51}" 25 | EndProject 26 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Modular.Modules.Users.Core", "src\Modules\Users\Modular.Modules.Users.Core\Modular.Modules.Users.Core.csproj", "{BFE2213C-D50C-4AF7-B201-85F4F199F949}" 27 | EndProject 28 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Modular.Modules.First.Api", "src\Modules\First\Modular.Modules.First.Api\Modular.Modules.First.Api.csproj", "{68D23AD7-DFC6-43F9-85DE-891C0BB10DEF}" 29 | EndProject 30 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Modular.Modules.Second.Api", "src\Modules\Second\Modular.Modules.Second.Api\Modular.Modules.Second.Api.csproj", "{8D48D83E-D048-470F-A5A8-6A56A26AE226}" 31 | EndProject 32 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Modular.Modules.Third.Api", "src\Modules\Third\Modular.Modules.Third.Api\Modular.Modules.Third.Api.csproj", "{CACAA9A2-ED85-4531-B2CF-4BBAFEC7AA19}" 33 | EndProject 34 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Modular.Abstractions", "src\modular-framework\src\Modular.Abstractions\Modular.Abstractions.csproj", "{2AE69D9D-5AA1-4B16-84CD-4CD4214368B8}" 35 | EndProject 36 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Modular.Infrastructure", "src\modular-framework\src\Modular.Infrastructure\Modular.Infrastructure.csproj", "{BA3A83F0-C5C0-4D7E-808E-B83BCB24F43B}" 37 | EndProject 38 | Global 39 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 40 | Debug|Any CPU = Debug|Any CPU 41 | Debug|x64 = Debug|x64 42 | Debug|x86 = Debug|x86 43 | Release|Any CPU = Release|Any CPU 44 | Release|x64 = Release|x64 45 | Release|x86 = Release|x86 46 | EndGlobalSection 47 | GlobalSection(SolutionProperties) = preSolution 48 | HideSolutionNode = FALSE 49 | EndGlobalSection 50 | GlobalSection(NestedProjects) = preSolution 51 | {3C49875F-386E-4C94-A80C-0C75C7A83BCB} = {FCA8CA8F-EE29-4CEA-8627-5EA7638B7C04} 52 | {F478360B-DE13-4F0B-9649-C7D4F8539D28} = {110B9A2A-E39B-4883-ACB6-7E05CE77AF9C} 53 | {CEAF1A2F-AFD5-4545-9E3D-53126DF1AB7F} = {1BA5712D-3DD3-4246-A396-D732B128CB61} 54 | {D97D0FDB-D5AC-4223-9B74-171EA8ABAFD7} = {1BA5712D-3DD3-4246-A396-D732B128CB61} 55 | {A84DB51C-E96F-42B6-9CC2-21E52E76DACD} = {1BA5712D-3DD3-4246-A396-D732B128CB61} 56 | {3C812292-89CC-482D-BC00-3DD04CA4496B} = {1BA5712D-3DD3-4246-A396-D732B128CB61} 57 | {3DBF494E-0153-495A-85B7-D23FBEDF5B51} = {3C812292-89CC-482D-BC00-3DD04CA4496B} 58 | {BFE2213C-D50C-4AF7-B201-85F4F199F949} = {3C812292-89CC-482D-BC00-3DD04CA4496B} 59 | {68D23AD7-DFC6-43F9-85DE-891C0BB10DEF} = {CEAF1A2F-AFD5-4545-9E3D-53126DF1AB7F} 60 | {8D48D83E-D048-470F-A5A8-6A56A26AE226} = {D97D0FDB-D5AC-4223-9B74-171EA8ABAFD7} 61 | {CACAA9A2-ED85-4531-B2CF-4BBAFEC7AA19} = {A84DB51C-E96F-42B6-9CC2-21E52E76DACD} 62 | {2AE69D9D-5AA1-4B16-84CD-4CD4214368B8} = {FCA8CA8F-EE29-4CEA-8627-5EA7638B7C04} 63 | {BA3A83F0-C5C0-4D7E-808E-B83BCB24F43B} = {FCA8CA8F-EE29-4CEA-8627-5EA7638B7C04} 64 | EndGlobalSection 65 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 66 | {3C49875F-386E-4C94-A80C-0C75C7A83BCB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 67 | {3C49875F-386E-4C94-A80C-0C75C7A83BCB}.Debug|Any CPU.Build.0 = Debug|Any CPU 68 | {3C49875F-386E-4C94-A80C-0C75C7A83BCB}.Debug|x64.ActiveCfg = Debug|Any CPU 69 | {3C49875F-386E-4C94-A80C-0C75C7A83BCB}.Debug|x64.Build.0 = Debug|Any CPU 70 | {3C49875F-386E-4C94-A80C-0C75C7A83BCB}.Debug|x86.ActiveCfg = Debug|Any CPU 71 | {3C49875F-386E-4C94-A80C-0C75C7A83BCB}.Debug|x86.Build.0 = Debug|Any CPU 72 | {3C49875F-386E-4C94-A80C-0C75C7A83BCB}.Release|Any CPU.ActiveCfg = Release|Any CPU 73 | {3C49875F-386E-4C94-A80C-0C75C7A83BCB}.Release|Any CPU.Build.0 = Release|Any CPU 74 | {3C49875F-386E-4C94-A80C-0C75C7A83BCB}.Release|x64.ActiveCfg = Release|Any CPU 75 | {3C49875F-386E-4C94-A80C-0C75C7A83BCB}.Release|x64.Build.0 = Release|Any CPU 76 | {3C49875F-386E-4C94-A80C-0C75C7A83BCB}.Release|x86.ActiveCfg = Release|Any CPU 77 | {3C49875F-386E-4C94-A80C-0C75C7A83BCB}.Release|x86.Build.0 = Release|Any CPU 78 | {F478360B-DE13-4F0B-9649-C7D4F8539D28}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 79 | {F478360B-DE13-4F0B-9649-C7D4F8539D28}.Debug|Any CPU.Build.0 = Debug|Any CPU 80 | {F478360B-DE13-4F0B-9649-C7D4F8539D28}.Debug|x64.ActiveCfg = Debug|Any CPU 81 | {F478360B-DE13-4F0B-9649-C7D4F8539D28}.Debug|x64.Build.0 = Debug|Any CPU 82 | {F478360B-DE13-4F0B-9649-C7D4F8539D28}.Debug|x86.ActiveCfg = Debug|Any CPU 83 | {F478360B-DE13-4F0B-9649-C7D4F8539D28}.Debug|x86.Build.0 = Debug|Any CPU 84 | {F478360B-DE13-4F0B-9649-C7D4F8539D28}.Release|Any CPU.ActiveCfg = Release|Any CPU 85 | {F478360B-DE13-4F0B-9649-C7D4F8539D28}.Release|Any CPU.Build.0 = Release|Any CPU 86 | {F478360B-DE13-4F0B-9649-C7D4F8539D28}.Release|x64.ActiveCfg = Release|Any CPU 87 | {F478360B-DE13-4F0B-9649-C7D4F8539D28}.Release|x64.Build.0 = Release|Any CPU 88 | {F478360B-DE13-4F0B-9649-C7D4F8539D28}.Release|x86.ActiveCfg = Release|Any CPU 89 | {F478360B-DE13-4F0B-9649-C7D4F8539D28}.Release|x86.Build.0 = Release|Any CPU 90 | {3DBF494E-0153-495A-85B7-D23FBEDF5B51}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 91 | {3DBF494E-0153-495A-85B7-D23FBEDF5B51}.Debug|Any CPU.Build.0 = Debug|Any CPU 92 | {3DBF494E-0153-495A-85B7-D23FBEDF5B51}.Debug|x64.ActiveCfg = Debug|Any CPU 93 | {3DBF494E-0153-495A-85B7-D23FBEDF5B51}.Debug|x64.Build.0 = Debug|Any CPU 94 | {3DBF494E-0153-495A-85B7-D23FBEDF5B51}.Debug|x86.ActiveCfg = Debug|Any CPU 95 | {3DBF494E-0153-495A-85B7-D23FBEDF5B51}.Debug|x86.Build.0 = Debug|Any CPU 96 | {3DBF494E-0153-495A-85B7-D23FBEDF5B51}.Release|Any CPU.ActiveCfg = Release|Any CPU 97 | {3DBF494E-0153-495A-85B7-D23FBEDF5B51}.Release|Any CPU.Build.0 = Release|Any CPU 98 | {3DBF494E-0153-495A-85B7-D23FBEDF5B51}.Release|x64.ActiveCfg = Release|Any CPU 99 | {3DBF494E-0153-495A-85B7-D23FBEDF5B51}.Release|x64.Build.0 = Release|Any CPU 100 | {3DBF494E-0153-495A-85B7-D23FBEDF5B51}.Release|x86.ActiveCfg = Release|Any CPU 101 | {3DBF494E-0153-495A-85B7-D23FBEDF5B51}.Release|x86.Build.0 = Release|Any CPU 102 | {BFE2213C-D50C-4AF7-B201-85F4F199F949}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 103 | {BFE2213C-D50C-4AF7-B201-85F4F199F949}.Debug|Any CPU.Build.0 = Debug|Any CPU 104 | {BFE2213C-D50C-4AF7-B201-85F4F199F949}.Debug|x64.ActiveCfg = Debug|Any CPU 105 | {BFE2213C-D50C-4AF7-B201-85F4F199F949}.Debug|x64.Build.0 = Debug|Any CPU 106 | {BFE2213C-D50C-4AF7-B201-85F4F199F949}.Debug|x86.ActiveCfg = Debug|Any CPU 107 | {BFE2213C-D50C-4AF7-B201-85F4F199F949}.Debug|x86.Build.0 = Debug|Any CPU 108 | {BFE2213C-D50C-4AF7-B201-85F4F199F949}.Release|Any CPU.ActiveCfg = Release|Any CPU 109 | {BFE2213C-D50C-4AF7-B201-85F4F199F949}.Release|Any CPU.Build.0 = Release|Any CPU 110 | {BFE2213C-D50C-4AF7-B201-85F4F199F949}.Release|x64.ActiveCfg = Release|Any CPU 111 | {BFE2213C-D50C-4AF7-B201-85F4F199F949}.Release|x64.Build.0 = Release|Any CPU 112 | {BFE2213C-D50C-4AF7-B201-85F4F199F949}.Release|x86.ActiveCfg = Release|Any CPU 113 | {BFE2213C-D50C-4AF7-B201-85F4F199F949}.Release|x86.Build.0 = Release|Any CPU 114 | {68D23AD7-DFC6-43F9-85DE-891C0BB10DEF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 115 | {68D23AD7-DFC6-43F9-85DE-891C0BB10DEF}.Debug|Any CPU.Build.0 = Debug|Any CPU 116 | {68D23AD7-DFC6-43F9-85DE-891C0BB10DEF}.Debug|x64.ActiveCfg = Debug|Any CPU 117 | {68D23AD7-DFC6-43F9-85DE-891C0BB10DEF}.Debug|x64.Build.0 = Debug|Any CPU 118 | {68D23AD7-DFC6-43F9-85DE-891C0BB10DEF}.Debug|x86.ActiveCfg = Debug|Any CPU 119 | {68D23AD7-DFC6-43F9-85DE-891C0BB10DEF}.Debug|x86.Build.0 = Debug|Any CPU 120 | {68D23AD7-DFC6-43F9-85DE-891C0BB10DEF}.Release|Any CPU.ActiveCfg = Release|Any CPU 121 | {68D23AD7-DFC6-43F9-85DE-891C0BB10DEF}.Release|Any CPU.Build.0 = Release|Any CPU 122 | {68D23AD7-DFC6-43F9-85DE-891C0BB10DEF}.Release|x64.ActiveCfg = Release|Any CPU 123 | {68D23AD7-DFC6-43F9-85DE-891C0BB10DEF}.Release|x64.Build.0 = Release|Any CPU 124 | {68D23AD7-DFC6-43F9-85DE-891C0BB10DEF}.Release|x86.ActiveCfg = Release|Any CPU 125 | {68D23AD7-DFC6-43F9-85DE-891C0BB10DEF}.Release|x86.Build.0 = Release|Any CPU 126 | {8D48D83E-D048-470F-A5A8-6A56A26AE226}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 127 | {8D48D83E-D048-470F-A5A8-6A56A26AE226}.Debug|Any CPU.Build.0 = Debug|Any CPU 128 | {8D48D83E-D048-470F-A5A8-6A56A26AE226}.Debug|x64.ActiveCfg = Debug|Any CPU 129 | {8D48D83E-D048-470F-A5A8-6A56A26AE226}.Debug|x64.Build.0 = Debug|Any CPU 130 | {8D48D83E-D048-470F-A5A8-6A56A26AE226}.Debug|x86.ActiveCfg = Debug|Any CPU 131 | {8D48D83E-D048-470F-A5A8-6A56A26AE226}.Debug|x86.Build.0 = Debug|Any CPU 132 | {8D48D83E-D048-470F-A5A8-6A56A26AE226}.Release|Any CPU.ActiveCfg = Release|Any CPU 133 | {8D48D83E-D048-470F-A5A8-6A56A26AE226}.Release|Any CPU.Build.0 = Release|Any CPU 134 | {8D48D83E-D048-470F-A5A8-6A56A26AE226}.Release|x64.ActiveCfg = Release|Any CPU 135 | {8D48D83E-D048-470F-A5A8-6A56A26AE226}.Release|x64.Build.0 = Release|Any CPU 136 | {8D48D83E-D048-470F-A5A8-6A56A26AE226}.Release|x86.ActiveCfg = Release|Any CPU 137 | {8D48D83E-D048-470F-A5A8-6A56A26AE226}.Release|x86.Build.0 = Release|Any CPU 138 | {CACAA9A2-ED85-4531-B2CF-4BBAFEC7AA19}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 139 | {CACAA9A2-ED85-4531-B2CF-4BBAFEC7AA19}.Debug|Any CPU.Build.0 = Debug|Any CPU 140 | {CACAA9A2-ED85-4531-B2CF-4BBAFEC7AA19}.Debug|x64.ActiveCfg = Debug|Any CPU 141 | {CACAA9A2-ED85-4531-B2CF-4BBAFEC7AA19}.Debug|x64.Build.0 = Debug|Any CPU 142 | {CACAA9A2-ED85-4531-B2CF-4BBAFEC7AA19}.Debug|x86.ActiveCfg = Debug|Any CPU 143 | {CACAA9A2-ED85-4531-B2CF-4BBAFEC7AA19}.Debug|x86.Build.0 = Debug|Any CPU 144 | {CACAA9A2-ED85-4531-B2CF-4BBAFEC7AA19}.Release|Any CPU.ActiveCfg = Release|Any CPU 145 | {CACAA9A2-ED85-4531-B2CF-4BBAFEC7AA19}.Release|Any CPU.Build.0 = Release|Any CPU 146 | {CACAA9A2-ED85-4531-B2CF-4BBAFEC7AA19}.Release|x64.ActiveCfg = Release|Any CPU 147 | {CACAA9A2-ED85-4531-B2CF-4BBAFEC7AA19}.Release|x64.Build.0 = Release|Any CPU 148 | {CACAA9A2-ED85-4531-B2CF-4BBAFEC7AA19}.Release|x86.ActiveCfg = Release|Any CPU 149 | {CACAA9A2-ED85-4531-B2CF-4BBAFEC7AA19}.Release|x86.Build.0 = Release|Any CPU 150 | {2AE69D9D-5AA1-4B16-84CD-4CD4214368B8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 151 | {2AE69D9D-5AA1-4B16-84CD-4CD4214368B8}.Debug|Any CPU.Build.0 = Debug|Any CPU 152 | {2AE69D9D-5AA1-4B16-84CD-4CD4214368B8}.Debug|x64.ActiveCfg = Debug|Any CPU 153 | {2AE69D9D-5AA1-4B16-84CD-4CD4214368B8}.Debug|x64.Build.0 = Debug|Any CPU 154 | {2AE69D9D-5AA1-4B16-84CD-4CD4214368B8}.Debug|x86.ActiveCfg = Debug|Any CPU 155 | {2AE69D9D-5AA1-4B16-84CD-4CD4214368B8}.Debug|x86.Build.0 = Debug|Any CPU 156 | {2AE69D9D-5AA1-4B16-84CD-4CD4214368B8}.Release|Any CPU.ActiveCfg = Release|Any CPU 157 | {2AE69D9D-5AA1-4B16-84CD-4CD4214368B8}.Release|Any CPU.Build.0 = Release|Any CPU 158 | {2AE69D9D-5AA1-4B16-84CD-4CD4214368B8}.Release|x64.ActiveCfg = Release|Any CPU 159 | {2AE69D9D-5AA1-4B16-84CD-4CD4214368B8}.Release|x64.Build.0 = Release|Any CPU 160 | {2AE69D9D-5AA1-4B16-84CD-4CD4214368B8}.Release|x86.ActiveCfg = Release|Any CPU 161 | {2AE69D9D-5AA1-4B16-84CD-4CD4214368B8}.Release|x86.Build.0 = Release|Any CPU 162 | {BA3A83F0-C5C0-4D7E-808E-B83BCB24F43B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 163 | {BA3A83F0-C5C0-4D7E-808E-B83BCB24F43B}.Debug|Any CPU.Build.0 = Debug|Any CPU 164 | {BA3A83F0-C5C0-4D7E-808E-B83BCB24F43B}.Debug|x64.ActiveCfg = Debug|Any CPU 165 | {BA3A83F0-C5C0-4D7E-808E-B83BCB24F43B}.Debug|x64.Build.0 = Debug|Any CPU 166 | {BA3A83F0-C5C0-4D7E-808E-B83BCB24F43B}.Debug|x86.ActiveCfg = Debug|Any CPU 167 | {BA3A83F0-C5C0-4D7E-808E-B83BCB24F43B}.Debug|x86.Build.0 = Debug|Any CPU 168 | {BA3A83F0-C5C0-4D7E-808E-B83BCB24F43B}.Release|Any CPU.ActiveCfg = Release|Any CPU 169 | {BA3A83F0-C5C0-4D7E-808E-B83BCB24F43B}.Release|Any CPU.Build.0 = Release|Any CPU 170 | {BA3A83F0-C5C0-4D7E-808E-B83BCB24F43B}.Release|x64.ActiveCfg = Release|Any CPU 171 | {BA3A83F0-C5C0-4D7E-808E-B83BCB24F43B}.Release|x64.Build.0 = Release|Any CPU 172 | {BA3A83F0-C5C0-4D7E-808E-B83BCB24F43B}.Release|x86.ActiveCfg = Release|Any CPU 173 | {BA3A83F0-C5C0-4D7E-808E-B83BCB24F43B}.Release|x86.Build.0 = Release|Any CPU 174 | EndGlobalSection 175 | EndGlobal 176 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Modular Monolith Template 2 | 3 | ![Overview](https://raw.githubusercontent.com/devmentors/modular-monolith-template/master/assets/template.png) 4 | 5 | ## About 6 | 7 | Modular monolith template provides a basic solution for the modular applications. It is based on [modular framework](https://github.com/devmentors/modular-framework) and can be used as a starting point for your next application. 8 | 9 | **How to start the solution?** 10 | ---------------- 11 | 12 | Initialize the submodule: 13 | 14 | ``` 15 | git submodule update --init 16 | ``` 17 | 18 | Start the infrastructure using [Docker](https://docs.docker.com/get-docker/): 19 | 20 | ``` 21 | docker-compose up -d 22 | ``` 23 | 24 | Start API located under Bootstrapper project: 25 | 26 | ``` 27 | cd src/Bootstrapper/Modular.Bootstrapper 28 | dotnet run 29 | ``` -------------------------------------------------------------------------------- /assets/template.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devmentors/modular-monolith-template/ef4dbbf615c6e993560f938634839e3a6835a5ea/assets/template.png -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | postgres: 5 | image: postgres 6 | shm_size: '4gb' 7 | container_name: postgres 8 | restart: unless-stopped 9 | environment: 10 | - POSTGRES_HOST_AUTH_METHOD=trust 11 | ports: 12 | - 5432:5432 13 | volumes: 14 | - postgres:/var/lib/postgresql/data 15 | 16 | volumes: 17 | postgres: 18 | driver: local -------------------------------------------------------------------------------- /src/Bootstrapper/Modular.Bootstrapper/Modular.Bootstrapper.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | all 19 | runtime; build; native; contentfiles; analyzers; buildtransitive 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/Bootstrapper/Modular.Bootstrapper/Program.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Microsoft.AspNetCore.Hosting; 3 | using Microsoft.Extensions.Hosting; 4 | using Modular.Infrastructure.Logging; 5 | using Modular.Infrastructure.Modules; 6 | 7 | namespace Modular.Bootstrapper 8 | { 9 | public class Program 10 | { 11 | public static Task Main(string[] args) 12 | => CreateHostBuilder(args).Build().RunAsync(); 13 | 14 | public static IHostBuilder CreateHostBuilder(string[] args) => 15 | Host.CreateDefaultBuilder(args) 16 | .ConfigureWebHostDefaults(webBuilder => webBuilder.UseStartup()) 17 | .ConfigureModules() 18 | .UseLogging(); 19 | } 20 | } -------------------------------------------------------------------------------- /src/Bootstrapper/Modular.Bootstrapper/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:38666", 7 | "sslPort": 44381 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": false, 14 | "environmentVariables": { 15 | "ASPNETCORE_ENVIRONMENT": "development" 16 | } 17 | }, 18 | "Modular.Bootstrapper": { 19 | "commandName": "Project", 20 | "dotnetRunMessages": "true", 21 | "launchBrowser": false, 22 | "applicationUrl": "http://localhost:5000", 23 | "environmentVariables": { 24 | "ASPNETCORE_ENVIRONMENT": "development" 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Bootstrapper/Modular.Bootstrapper/Startup.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using System.Reflection; 4 | using Microsoft.AspNetCore.Builder; 5 | using Microsoft.AspNetCore.Hosting; 6 | using Microsoft.AspNetCore.Http; 7 | using Microsoft.Extensions.Configuration; 8 | using Microsoft.Extensions.DependencyInjection; 9 | using Microsoft.Extensions.Logging; 10 | using Modular.Abstractions.Modules; 11 | using Modular.Infrastructure; 12 | using Modular.Infrastructure.Messaging.Outbox; 13 | using Modular.Infrastructure.Modules; 14 | using Modular.Infrastructure.Postgres; 15 | using Modular.Infrastructure.Services; 16 | 17 | namespace Modular.Bootstrapper 18 | { 19 | public class Startup 20 | { 21 | private readonly IList _assemblies; 22 | private readonly IList _modules; 23 | 24 | public Startup(IConfiguration configuration) 25 | { 26 | _assemblies = ModuleLoader.LoadAssemblies(configuration, "Modular.Modules."); 27 | _modules = ModuleLoader.LoadModules(_assemblies); 28 | } 29 | 30 | public void ConfigureServices(IServiceCollection services) 31 | { 32 | services.AddModularInfrastructure(_assemblies, _modules); 33 | services.AddPostgres(); 34 | services.AddOutbox(); 35 | services.AddHostedService(); 36 | foreach (var module in _modules) 37 | { 38 | module.Register(services); 39 | } 40 | } 41 | 42 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILogger logger) 43 | { 44 | logger.LogInformation($"Modules: {string.Join(", ", _modules.Select(x => x.Name))}"); 45 | app.UseModularInfrastructure(); 46 | foreach (var module in _modules) 47 | { 48 | module.Use(app); 49 | } 50 | 51 | app.UseEndpoints(endpoints => 52 | { 53 | endpoints.MapControllers(); 54 | endpoints.MapGet("/", context => context.Response.WriteAsync("Modular API")); 55 | endpoints.MapModuleInfo(); 56 | }); 57 | 58 | _assemblies.Clear(); 59 | _modules.Clear(); 60 | } 61 | } 62 | } -------------------------------------------------------------------------------- /src/Bootstrapper/Modular.Bootstrapper/appsettings.development.json: -------------------------------------------------------------------------------- 1 | { 2 | "logging": { 3 | "logLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Information", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/Bootstrapper/Modular.Bootstrapper/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": { 3 | "name": "modular" 4 | }, 5 | "auth": { 6 | "issuerSigningKey": "fa5DRdkVwZeQnrDAcBrHCYwAWd6y2crPUbSZq4zUWBRFwDfKDXQWH38vZRfv", 7 | "issuer": "modular", 8 | "validIssuer": "modular", 9 | "validateAudience": false, 10 | "validateIssuer": true, 11 | "validateLifetime": true, 12 | "expiry": "07.00:00:00", 13 | "cookie": { 14 | "httpOnly": true, 15 | "sameSite": "unspecified", 16 | "secure": false 17 | } 18 | }, 19 | "cors": { 20 | "allowCredentials": true, 21 | "allowedOrigins": [ 22 | "http://localhost:3000" 23 | ], 24 | "allowedMethods": [ 25 | "POST", 26 | "PUT", 27 | "DELETE" 28 | ], 29 | "allowedHeaders": [ 30 | "Content-Type", 31 | "Authorization" 32 | ], 33 | "exposedHeaders": [ 34 | "Resource-ID" 35 | ] 36 | }, 37 | "logger": { 38 | "level": "information", 39 | "overrides": { 40 | "Microsoft.EntityFrameworkCore.Database.Command": "Warning", 41 | "Microsoft.EntityFrameworkCore.Infrastructure": "Warning" 42 | }, 43 | "excludePaths": [ 44 | "/", 45 | "/ping", 46 | "/metrics" 47 | ], 48 | "excludeProperties": [ 49 | "api_key", 50 | "access_key", 51 | "ApiKey", 52 | "ApiSecret", 53 | "ClientId", 54 | "ClientSecret", 55 | "ConnectionString", 56 | "Password", 57 | "Email", 58 | "Login", 59 | "Secret", 60 | "Token" 61 | ], 62 | "console": { 63 | "enabled": true 64 | }, 65 | "file": { 66 | "enabled": true, 67 | "path": "logs/logs.txt", 68 | "interval": "day" 69 | }, 70 | "seq": { 71 | "enabled": true, 72 | "url": "http://localhost:5341", 73 | "apiKey": "secret" 74 | }, 75 | "tags": {} 76 | }, 77 | "messaging": { 78 | "useAsyncDispatcher": true 79 | }, 80 | "outbox": { 81 | "enabled": false, 82 | "interval": "00:00:01" 83 | }, 84 | "postgres": { 85 | "connectionString": "Host=localhost;Database=modular;Username=postgres;Password=" 86 | }, 87 | "security": { 88 | "encryption": { 89 | "enabled": true, 90 | "key": "3Lt2jm83Gmb8Ja3jQPkBVuApzbF8DVPX" 91 | } 92 | } 93 | } -------------------------------------------------------------------------------- /src/Modules/First/Modular.Modules.First.Api/Controllers/FirstController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Http; 2 | using Microsoft.AspNetCore.Mvc; 3 | using Swashbuckle.AspNetCore.Annotations; 4 | 5 | namespace Modular.Modules.First.Api.Controllers 6 | { 7 | [Route("[controller]")] 8 | internal class FirstController : Controller 9 | { 10 | [SwaggerOperation("Get first")] 11 | [ProducesResponseType(StatusCodes.Status200OK)] 12 | [HttpGet] 13 | public ActionResult Get() => Ok("First module"); 14 | } 15 | } -------------------------------------------------------------------------------- /src/Modules/First/Modular.Modules.First.Api/FirstModule.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Microsoft.AspNetCore.Builder; 3 | using Microsoft.Extensions.DependencyInjection; 4 | using Modular.Abstractions.Modules; 5 | 6 | namespace Modular.Modules.First.Api 7 | { 8 | internal class FirstModule : IModule 9 | { 10 | public string Name { get; } = "First"; 11 | 12 | public IEnumerable Policies { get; } = new[] 13 | { 14 | "first" 15 | }; 16 | 17 | public void Register(IServiceCollection services) 18 | { 19 | } 20 | 21 | public void Use(IApplicationBuilder app) 22 | { 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /src/Modules/First/Modular.Modules.First.Api/Modular.Modules.First.Api.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | Always 15 | 16 | 17 | 18 | Always 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/Modules/First/Modular.Modules.First.Api/module.first.development.json: -------------------------------------------------------------------------------- 1 | { 2 | "first": { 3 | } 4 | } -------------------------------------------------------------------------------- /src/Modules/First/Modular.Modules.First.Api/module.first.json: -------------------------------------------------------------------------------- 1 | { 2 | "first": { 3 | "module": { 4 | "name": "First", 5 | "enabled": true 6 | } 7 | } 8 | } -------------------------------------------------------------------------------- /src/Modules/Second/Modular.Modules.Second.Api/Controllers/SecondController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Http; 2 | using Microsoft.AspNetCore.Mvc; 3 | using Swashbuckle.AspNetCore.Annotations; 4 | 5 | namespace Modular.Modules.Second.Api.Controllers 6 | { 7 | [Route("[controller]")] 8 | internal class SecondController : Controller 9 | { 10 | [SwaggerOperation("Get second")] 11 | [ProducesResponseType(StatusCodes.Status200OK)] 12 | [HttpGet] 13 | public ActionResult Get() => Ok("Second module"); 14 | } 15 | } -------------------------------------------------------------------------------- /src/Modules/Second/Modular.Modules.Second.Api/Modular.Modules.Second.Api.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | Always 15 | 16 | 17 | 18 | Always 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/Modules/Second/Modular.Modules.Second.Api/SecondModule.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Microsoft.AspNetCore.Builder; 3 | using Microsoft.Extensions.DependencyInjection; 4 | using Modular.Abstractions.Modules; 5 | 6 | namespace Modular.Modules.Second.Api 7 | { 8 | internal class SecondModule : IModule 9 | { 10 | public string Name { get; } = "Second"; 11 | 12 | public IEnumerable Policies { get; } = new[] 13 | { 14 | "second" 15 | }; 16 | 17 | public void Register(IServiceCollection services) 18 | { 19 | } 20 | 21 | public void Use(IApplicationBuilder app) 22 | { 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /src/Modules/Second/Modular.Modules.Second.Api/module.second.development.json: -------------------------------------------------------------------------------- 1 | { 2 | "second": { 3 | } 4 | } -------------------------------------------------------------------------------- /src/Modules/Second/Modular.Modules.Second.Api/module.second.json: -------------------------------------------------------------------------------- 1 | { 2 | "second": { 3 | "module": { 4 | "name": "Second", 5 | "enabled": true 6 | } 7 | } 8 | } -------------------------------------------------------------------------------- /src/Modules/Third/Modular.Modules.Third.Api/Controllers/ThirdController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Http; 2 | using Microsoft.AspNetCore.Mvc; 3 | using Swashbuckle.AspNetCore.Annotations; 4 | 5 | namespace Modular.Modules.Third.Api.Controllers 6 | { 7 | [Route("[controller]")] 8 | internal class ThirdController : Controller 9 | { 10 | [SwaggerOperation("Get third")] 11 | [ProducesResponseType(StatusCodes.Status200OK)] 12 | [HttpGet] 13 | public ActionResult Get() => Ok("Third module"); 14 | } 15 | } -------------------------------------------------------------------------------- /src/Modules/Third/Modular.Modules.Third.Api/Modular.Modules.Third.Api.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | Always 15 | 16 | 17 | 18 | Always 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/Modules/Third/Modular.Modules.Third.Api/ThirdModule.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Microsoft.AspNetCore.Builder; 3 | using Microsoft.Extensions.DependencyInjection; 4 | using Modular.Abstractions.Modules; 5 | 6 | namespace Modular.Modules.Third.Api 7 | { 8 | internal class ThirdModule : IModule 9 | { 10 | public string Name { get; } = "Third"; 11 | 12 | public IEnumerable Policies { get; } = new[] 13 | { 14 | "third" 15 | }; 16 | 17 | public void Register(IServiceCollection services) 18 | { 19 | } 20 | 21 | public void Use(IApplicationBuilder app) 22 | { 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /src/Modules/Third/Modular.Modules.Third.Api/module.third.development.json: -------------------------------------------------------------------------------- 1 | { 2 | "third": { 3 | } 4 | } -------------------------------------------------------------------------------- /src/Modules/Third/Modular.Modules.Third.Api/module.third.json: -------------------------------------------------------------------------------- 1 | { 2 | "third": { 3 | "module": { 4 | "name": "Third", 5 | "enabled": true 6 | } 7 | } 8 | } -------------------------------------------------------------------------------- /src/Modules/Users/Modular.Modules.Users.Api/Controllers/AccountController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Microsoft.AspNetCore.Authorization; 4 | using Microsoft.AspNetCore.Http; 5 | using Microsoft.AspNetCore.Mvc; 6 | using Modular.Modules.Users.Core.Commands; 7 | using Modular.Modules.Users.Core.DTO; 8 | using Modular.Modules.Users.Core.Queries; 9 | using Modular.Modules.Users.Core.Services; 10 | using Modular.Abstractions.Contexts; 11 | using Modular.Abstractions.Dispatchers; 12 | using Modular.Infrastructure.Api; 13 | using Swashbuckle.AspNetCore.Annotations; 14 | 15 | namespace Modular.Modules.Users.Api.Controllers 16 | { 17 | internal class AccountController : BaseController 18 | { 19 | private const string AccessTokenCookie = "__access-token"; 20 | private readonly IDispatcher _dispatcher; 21 | private readonly IContext _context; 22 | private readonly IUserRequestStorage _userRequestStorage; 23 | private readonly CookieOptions _cookieOptions; 24 | 25 | public AccountController(IDispatcher dispatcher, IContext context, IUserRequestStorage userRequestStorage, 26 | CookieOptions cookieOptions) 27 | { 28 | _dispatcher = dispatcher; 29 | _context = context; 30 | _userRequestStorage = userRequestStorage; 31 | _cookieOptions = cookieOptions; 32 | } 33 | 34 | [HttpGet] 35 | [Authorize] 36 | [SwaggerOperation("Get account")] 37 | [ProducesResponseType(StatusCodes.Status200OK)] 38 | [ProducesResponseType(StatusCodes.Status401Unauthorized)] 39 | public async Task> GetAsync() 40 | => OkOrNotFound(await _dispatcher.QueryAsync(new GetUser {UserId = _context.Identity.Id})); 41 | 42 | [HttpPost("sign-up")] 43 | [SwaggerOperation("Sign up")] 44 | [ProducesResponseType(StatusCodes.Status204NoContent)] 45 | [ProducesResponseType(StatusCodes.Status400BadRequest)] 46 | public async Task SignUpAsync(SignUp command) 47 | { 48 | await _dispatcher.SendAsync(command); 49 | return NoContent(); 50 | } 51 | 52 | [HttpPost("sign-in")] 53 | [SwaggerOperation("Sign in")] 54 | [ProducesResponseType(StatusCodes.Status204NoContent)] 55 | [ProducesResponseType(StatusCodes.Status400BadRequest)] 56 | public async Task> SignInAsync(SignIn command) 57 | { 58 | await _dispatcher.SendAsync(command.Bind(x => x.Id, Guid.NewGuid())); 59 | var jwt = _userRequestStorage.GetToken(command.Id); 60 | var user = await _dispatcher.QueryAsync(new GetUser {UserId = jwt.UserId}); 61 | AddCookie(AccessTokenCookie, jwt.AccessToken); 62 | return Ok(user); 63 | } 64 | 65 | [Authorize] 66 | [HttpDelete("sign-out")] 67 | [SwaggerOperation("Sign out")] 68 | [ProducesResponseType(StatusCodes.Status204NoContent)] 69 | [ProducesResponseType(StatusCodes.Status400BadRequest)] 70 | [ProducesResponseType(StatusCodes.Status401Unauthorized)] 71 | public async Task SignOutAsync() 72 | { 73 | await _dispatcher.SendAsync(new SignOut(_context.Identity.Id)); 74 | DeleteCookie(AccessTokenCookie); 75 | return NoContent(); 76 | } 77 | 78 | private void AddCookie(string key, string value) => Response.Cookies.Append(key, value, _cookieOptions); 79 | 80 | private void DeleteCookie(string key) => Response.Cookies.Delete(key, _cookieOptions); 81 | } 82 | } -------------------------------------------------------------------------------- /src/Modules/Users/Modular.Modules.Users.Api/Controllers/BaseController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | using Modular.Infrastructure.Api; 3 | 4 | namespace Modular.Modules.Users.Api.Controllers 5 | { 6 | [ApiController] 7 | [Route("[controller]")] 8 | [ProducesDefaultContentType] 9 | internal abstract class BaseController : ControllerBase 10 | { 11 | protected ActionResult OkOrNotFound(T model) 12 | { 13 | if (model is not null) 14 | { 15 | return Ok(model); 16 | } 17 | 18 | return NotFound(); 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /src/Modules/Users/Modular.Modules.Users.Api/Controllers/PasswordController.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Microsoft.AspNetCore.Authorization; 3 | using Microsoft.AspNetCore.Http; 4 | using Microsoft.AspNetCore.Mvc; 5 | using Modular.Modules.Users.Core.Commands; 6 | using Modular.Abstractions.Contexts; 7 | using Modular.Abstractions.Dispatchers; 8 | using Modular.Infrastructure.Api; 9 | using Swashbuckle.AspNetCore.Annotations; 10 | 11 | namespace Modular.Modules.Users.Api.Controllers 12 | { 13 | internal class PasswordController : BaseController 14 | { 15 | private readonly IDispatcher _dispatcher; 16 | private readonly IContext _context; 17 | 18 | public PasswordController(IDispatcher dispatcher, IContext context) 19 | { 20 | _dispatcher = dispatcher; 21 | _context = context; 22 | } 23 | 24 | [Authorize] 25 | [HttpPut] 26 | [SwaggerOperation("Change password")] 27 | [ProducesResponseType(StatusCodes.Status204NoContent)] 28 | [ProducesResponseType(StatusCodes.Status400BadRequest)] 29 | public async Task ChangeAsync(ChangePassword command) 30 | { 31 | await _dispatcher.SendAsync(command.Bind(x => x.UserId, _context.Identity.Id)); 32 | return NoContent(); 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /src/Modules/Users/Modular.Modules.Users.Api/Controllers/UsersController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Microsoft.AspNetCore.Authorization; 4 | using Microsoft.AspNetCore.Http; 5 | using Microsoft.AspNetCore.Mvc; 6 | using Modular.Modules.Users.Core.Commands; 7 | using Modular.Modules.Users.Core.DTO; 8 | using Modular.Modules.Users.Core.Queries; 9 | using Modular.Abstractions.Dispatchers; 10 | using Modular.Abstractions.Queries; 11 | using Modular.Infrastructure.Api; 12 | using Swashbuckle.AspNetCore.Annotations; 13 | 14 | namespace Modular.Modules.Users.Api.Controllers 15 | { 16 | [Authorize(Policy)] 17 | internal class UsersController : BaseController 18 | { 19 | private const string Policy = "users"; 20 | private readonly IDispatcher _dispatcher; 21 | 22 | public UsersController(IDispatcher dispatcher) 23 | { 24 | _dispatcher = dispatcher; 25 | } 26 | 27 | [HttpGet("{userId:guid}")] 28 | [SwaggerOperation("Get user")] 29 | [ProducesResponseType(StatusCodes.Status200OK)] 30 | [ProducesResponseType(StatusCodes.Status404NotFound)] 31 | [ProducesResponseType(StatusCodes.Status401Unauthorized)] 32 | [ProducesResponseType(StatusCodes.Status403Forbidden)] 33 | public async Task> GetAsync(Guid userId) 34 | => OkOrNotFound(await _dispatcher.QueryAsync(new GetUser {UserId = userId})); 35 | 36 | [HttpGet] 37 | [SwaggerOperation("Browse users")] 38 | [ProducesResponseType(StatusCodes.Status200OK)] 39 | [ProducesResponseType(StatusCodes.Status401Unauthorized)] 40 | [ProducesResponseType(StatusCodes.Status403Forbidden)] 41 | public async Task>> BrowseAsync([FromQuery] BrowseUsers query) 42 | => Ok(await _dispatcher.QueryAsync(query)); 43 | 44 | [HttpPut("{userId:guid}/state")] 45 | [SwaggerOperation("Update user state")] 46 | [ProducesResponseType(StatusCodes.Status204NoContent)] 47 | [ProducesResponseType(StatusCodes.Status400BadRequest)] 48 | [ProducesResponseType(StatusCodes.Status401Unauthorized)] 49 | [ProducesResponseType(StatusCodes.Status403Forbidden)] 50 | public async Task UpdateStateAsync(Guid userId, UpdateUserState command) 51 | { 52 | await _dispatcher.SendAsync(command.Bind(x => x.UserId, userId)); 53 | return NoContent(); 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /src/Modules/Users/Modular.Modules.Users.Api/Modular.Modules.Users.Api.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | Always 15 | 16 | 17 | 18 | Always 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/Modules/Users/Modular.Modules.Users.Api/UsersModule.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Microsoft.AspNetCore.Builder; 3 | using Microsoft.Extensions.DependencyInjection; 4 | using Modular.Modules.Users.Core; 5 | using Modular.Abstractions.Modules; 6 | 7 | namespace Modular.Modules.Users.Api 8 | { 9 | internal class UsersModule : IModule 10 | { 11 | public string Name { get; } = "Users"; 12 | 13 | public IEnumerable Policies { get; } = new[] 14 | { 15 | "users" 16 | }; 17 | 18 | public void Register(IServiceCollection services) 19 | { 20 | services.AddCore(); 21 | } 22 | 23 | public void Use(IApplicationBuilder app) 24 | { 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /src/Modules/Users/Modular.Modules.Users.Api/module.users.development.json: -------------------------------------------------------------------------------- 1 | { 2 | "users": { 3 | } 4 | } -------------------------------------------------------------------------------- /src/Modules/Users/Modular.Modules.Users.Api/module.users.json: -------------------------------------------------------------------------------- 1 | { 2 | "users": { 3 | "module": { 4 | "name": "Users", 5 | "enabled": true 6 | }, 7 | "registration": { 8 | "enabled": true, 9 | "invalidEmailProviders": [ 10 | "discard", 11 | "dispostable", 12 | "mailinator" 13 | ] 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /src/Modules/Users/Modular.Modules.Users.Core/Commands/ChangePassword.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Modular.Abstractions.Commands; 3 | 4 | namespace Modular.Modules.Users.Core.Commands 5 | { 6 | internal record ChangePassword(Guid UserId, string CurrentPassword, string NewPassword) : ICommand; 7 | } -------------------------------------------------------------------------------- /src/Modules/Users/Modular.Modules.Users.Core/Commands/Handlers/ChangePasswordHandler.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | using Microsoft.AspNetCore.Identity; 4 | using Microsoft.Extensions.Logging; 5 | using Modular.Modules.Users.Core.Entities; 6 | using Modular.Modules.Users.Core.Exceptions; 7 | using Modular.Modules.Users.Core.Repositories; 8 | using Modular.Abstractions; 9 | using Modular.Abstractions.Commands; 10 | 11 | namespace Modular.Modules.Users.Core.Commands.Handlers 12 | { 13 | internal sealed class ChangePasswordHandler : ICommandHandler 14 | { 15 | private readonly IUserRepository _userRepository; 16 | private readonly IPasswordHasher _passwordHasher; 17 | private readonly ILogger _logger; 18 | 19 | public ChangePasswordHandler(IUserRepository userRepository, IPasswordHasher passwordHasher, 20 | ILogger logger) 21 | { 22 | _userRepository = userRepository; 23 | _passwordHasher = passwordHasher; 24 | _logger = logger; 25 | } 26 | 27 | public async Task HandleAsync(ChangePassword command, CancellationToken cancellationToken = default) 28 | { 29 | var user = await _userRepository.GetAsync(command.UserId) 30 | .NotNull(() => new UserNotFoundException(command.UserId)); 31 | 32 | if (_passwordHasher.VerifyHashedPassword(default, user.Password, command.CurrentPassword) == 33 | PasswordVerificationResult.Failed) 34 | { 35 | throw new InvalidPasswordException("current password is invalid"); 36 | } 37 | 38 | user.Password = _passwordHasher.HashPassword(default, command.NewPassword);; 39 | await _userRepository.UpdateAsync(user); 40 | _logger.LogInformation($"User with ID: '{user.Id}' has changed password."); 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /src/Modules/Users/Modular.Modules.Users.Core/Commands/Handlers/SignInHandler.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using Microsoft.AspNetCore.Identity; 5 | using Microsoft.Extensions.Logging; 6 | using Modular.Modules.Users.Core.Entities; 7 | using Modular.Modules.Users.Core.Events; 8 | using Modular.Modules.Users.Core.Exceptions; 9 | using Modular.Modules.Users.Core.Repositories; 10 | using Modular.Modules.Users.Core.Services; 11 | using Modular.Abstractions; 12 | using Modular.Abstractions.Auth; 13 | using Modular.Abstractions.Commands; 14 | using Modular.Abstractions.Messaging; 15 | 16 | namespace Modular.Modules.Users.Core.Commands.Handlers 17 | { 18 | internal sealed class SignInHandler : ICommandHandler 19 | { 20 | private readonly IUserRepository _userRepository; 21 | private readonly IAuthManager _authManager; 22 | private readonly IPasswordHasher _passwordHasher; 23 | private readonly IUserRequestStorage _userRequestStorage; 24 | private readonly IMessageBroker _messageBroker; 25 | private readonly ILogger _logger; 26 | 27 | public SignInHandler(IUserRepository userRepository, IAuthManager authManager, 28 | IPasswordHasher passwordHasher, IUserRequestStorage userRequestStorage, IMessageBroker messageBroker, 29 | ILogger logger) 30 | { 31 | _userRepository = userRepository; 32 | _authManager = authManager; 33 | _passwordHasher = passwordHasher; 34 | _userRequestStorage = userRequestStorage; 35 | _messageBroker = messageBroker; 36 | _logger = logger; 37 | } 38 | 39 | public async Task HandleAsync(SignIn command, CancellationToken cancellationToken = default) 40 | { 41 | var user = await _userRepository.GetAsync(command.Email.ToLowerInvariant()) 42 | .NotNull(() => new InvalidCredentialsException()); 43 | 44 | if (user.State != UserState.Active) 45 | { 46 | throw new UserNotActiveException(user.Id); 47 | } 48 | 49 | if (_passwordHasher.VerifyHashedPassword(default, user.Password, command.Password) == 50 | PasswordVerificationResult.Failed) 51 | { 52 | throw new InvalidCredentialsException(); 53 | } 54 | 55 | var claims = new Dictionary> 56 | { 57 | ["permissions"] = user.Role.Permissions 58 | }; 59 | 60 | var jwt = _authManager.CreateToken(user.Id, user.Role.Name, claims: claims); 61 | jwt.Email = user.Email; 62 | await _messageBroker.PublishAsync(new SignedIn(user.Id)); 63 | _logger.LogInformation($"User with ID: '{user.Id}' has signed in."); 64 | _userRequestStorage.SetToken(command.Id, jwt); 65 | } 66 | } 67 | } -------------------------------------------------------------------------------- /src/Modules/Users/Modular.Modules.Users.Core/Commands/Handlers/SignOutHandler.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | using Microsoft.Extensions.Logging; 4 | using Modular.Abstractions.Commands; 5 | 6 | namespace Modular.Modules.Users.Core.Commands.Handlers 7 | { 8 | internal sealed class SignOutHandler : ICommandHandler 9 | { 10 | private readonly ILogger _logger; 11 | 12 | public SignOutHandler(ILogger logger) 13 | { 14 | _logger = logger; 15 | } 16 | 17 | public async Task HandleAsync(SignOut command, CancellationToken cancellationToken = default) 18 | { 19 | 20 | await Task.CompletedTask; 21 | _logger.LogInformation($"User with ID: '{command.UserId}' has signed out."); 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /src/Modules/Users/Modular.Modules.Users.Core/Commands/Handlers/SignUpHandler.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using Microsoft.AspNetCore.Identity; 5 | using Microsoft.Extensions.Logging; 6 | using Modular.Modules.Users.Core.Entities; 7 | using Modular.Modules.Users.Core.Events; 8 | using Modular.Modules.Users.Core.Exceptions; 9 | using Modular.Modules.Users.Core.Repositories; 10 | using Modular.Abstractions; 11 | using Modular.Abstractions.Commands; 12 | using Modular.Abstractions.Messaging; 13 | using Modular.Abstractions.Time; 14 | 15 | namespace Modular.Modules.Users.Core.Commands.Handlers 16 | { 17 | internal sealed class SignUpHandler : ICommandHandler 18 | { 19 | private readonly IUserRepository _userRepository; 20 | private readonly IRoleRepository _roleRepository; 21 | private readonly IPasswordHasher _passwordHasher; 22 | private readonly IClock _clock; 23 | private readonly IMessageBroker _messageBroker; 24 | private readonly RegistrationOptions _registrationOptions; 25 | private readonly ILogger _logger; 26 | 27 | public SignUpHandler(IUserRepository userRepository, IRoleRepository roleRepository, 28 | IPasswordHasher passwordHasher, IClock clock, IMessageBroker messageBroker, 29 | RegistrationOptions registrationOptions, ILogger logger) 30 | { 31 | _userRepository = userRepository; 32 | _roleRepository = roleRepository; 33 | _passwordHasher = passwordHasher; 34 | _clock = clock; 35 | _messageBroker = messageBroker; 36 | _registrationOptions = registrationOptions; 37 | _logger = logger; 38 | } 39 | 40 | public async Task HandleAsync(SignUp command, CancellationToken cancellationToken = default) 41 | { 42 | if (!_registrationOptions.Enabled) 43 | { 44 | throw new SignUpDisabledException(); 45 | } 46 | 47 | var email = command.Email.ToLowerInvariant(); 48 | var provider = email.Split("@").Last(); 49 | if (_registrationOptions.InvalidEmailProviders?.Any(x => provider.Contains(x)) is true) 50 | { 51 | throw new InvalidEmailException(email); 52 | } 53 | 54 | var user = await _userRepository.GetAsync(email); 55 | if (user is not null) 56 | { 57 | throw new EmailInUseException(); 58 | } 59 | 60 | var roleName = string.IsNullOrWhiteSpace(command.Role) ? Role.Default : command.Role.ToLowerInvariant(); 61 | 62 | var role = await _roleRepository.GetAsync(roleName) 63 | .NotNull(() => new RoleNotFoundException(roleName)); 64 | 65 | var now = _clock.CurrentDate(); 66 | var password = _passwordHasher.HashPassword(default, command.Password); 67 | user = new User 68 | { 69 | Id = command.UserId, 70 | Email = email, 71 | Password = password, 72 | Role = role, 73 | CreatedAt = now, 74 | State = UserState.Active, 75 | }; 76 | await _userRepository.AddAsync(user); 77 | await _messageBroker.PublishAsync(new SignedUp(user.Id, email, role.Name)); 78 | _logger.LogInformation($"User with ID: '{user.Id}' has signed up."); 79 | } 80 | } 81 | } -------------------------------------------------------------------------------- /src/Modules/Users/Modular.Modules.Users.Core/Commands/Handlers/UpdateUserStateHandler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using Microsoft.Extensions.Logging; 5 | using Modular.Modules.Users.Core.Entities; 6 | using Modular.Modules.Users.Core.Events; 7 | using Modular.Modules.Users.Core.Exceptions; 8 | using Modular.Modules.Users.Core.Repositories; 9 | using Modular.Abstractions; 10 | using Modular.Abstractions.Commands; 11 | using Modular.Abstractions.Messaging; 12 | 13 | namespace Modular.Modules.Users.Core.Commands.Handlers 14 | { 15 | internal sealed class UpdateUserStateHandler : ICommandHandler 16 | { 17 | private readonly IUserRepository _userRepository; 18 | private readonly IMessageBroker _messageBroker; 19 | private readonly ILogger _logger; 20 | 21 | public UpdateUserStateHandler(IUserRepository userRepository, IMessageBroker messageBroker, 22 | ILogger logger) 23 | { 24 | _userRepository = userRepository; 25 | _messageBroker = messageBroker; 26 | _logger = logger; 27 | } 28 | 29 | public async Task HandleAsync(UpdateUserState command, CancellationToken cancellationToken = default) 30 | { 31 | if (!Enum.TryParse(command.State, true, out var state)) 32 | { 33 | throw new InvalidUserStateException(command.State); 34 | } 35 | 36 | var user = await _userRepository.GetAsync(command.UserId) 37 | .NotNull(() => new UserNotFoundException(command.UserId)); 38 | 39 | var previousState = user.State; 40 | if (previousState == state) 41 | { 42 | return; 43 | } 44 | 45 | if (user.RoleId == Role.Admin) 46 | { 47 | throw new UserStateCannotBeChangedException(command.State, command.UserId); 48 | } 49 | 50 | user.State = state; 51 | await _userRepository.UpdateAsync(user); 52 | await _messageBroker.PublishAsync(new UserStateUpdated(user.Id, state.ToString().ToLowerInvariant())); 53 | _logger.LogInformation($"Updated state for user with ID: '{user.Id}', '{previousState}' -> '{user.State}'."); 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /src/Modules/Users/Modular.Modules.Users.Core/Commands/SignIn.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel.DataAnnotations; 3 | using Modular.Abstractions.Commands; 4 | 5 | namespace Modular.Modules.Users.Core.Commands 6 | { 7 | internal record SignIn([Required] [EmailAddress] string Email, [Required] string Password) : ICommand 8 | { 9 | public Guid Id { get; init; } = Guid.NewGuid(); 10 | } 11 | } -------------------------------------------------------------------------------- /src/Modules/Users/Modular.Modules.Users.Core/Commands/SignOut.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Modular.Abstractions.Commands; 3 | 4 | namespace Modular.Modules.Users.Core.Commands 5 | { 6 | internal record SignOut(Guid UserId) : ICommand; 7 | } -------------------------------------------------------------------------------- /src/Modules/Users/Modular.Modules.Users.Core/Commands/SignUp.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel.DataAnnotations; 3 | using Modular.Abstractions.Commands; 4 | 5 | namespace Modular.Modules.Users.Core.Commands 6 | { 7 | internal record SignUp([Required] [EmailAddress] string Email, [Required] string Password, string Role) : ICommand 8 | { 9 | public Guid UserId { get; init; } = Guid.NewGuid(); 10 | } 11 | } -------------------------------------------------------------------------------- /src/Modules/Users/Modular.Modules.Users.Core/Commands/UpdateUserState.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Modular.Abstractions.Commands; 3 | 4 | namespace Modular.Modules.Users.Core.Commands 5 | { 6 | internal record UpdateUserState(Guid UserId, string State) : ICommand; 7 | } -------------------------------------------------------------------------------- /src/Modules/Users/Modular.Modules.Users.Core/DAL/Configurations/RoleConfiguration.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using Microsoft.EntityFrameworkCore; 5 | using Microsoft.EntityFrameworkCore.ChangeTracking; 6 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 7 | using Modular.Modules.Users.Core.Entities; 8 | 9 | namespace Modular.Modules.Users.Core.DAL.Configurations 10 | { 11 | internal class RoleConfiguration : IEntityTypeConfiguration 12 | { 13 | public void Configure(EntityTypeBuilder builder) 14 | { 15 | builder.HasKey(x => x.Name); 16 | builder.Property(x => x.Name).IsRequired().HasMaxLength(100); 17 | builder 18 | .Property(x => x.Permissions) 19 | .HasConversion(x => string.Join(',', x), x => x.Split(',', StringSplitOptions.None)); 20 | 21 | builder 22 | .Property(x => x.Permissions).Metadata.SetValueComparer( 23 | new ValueComparer>( 24 | (c1, c2) => c1.SequenceEqual(c2), 25 | c => c.Aggregate(0, (a, next) => HashCode.Combine(a, next.GetHashCode())))); 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /src/Modules/Users/Modular.Modules.Users.Core/DAL/Configurations/UserConfiguration.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 3 | using Modular.Modules.Users.Core.Entities; 4 | 5 | namespace Modular.Modules.Users.Core.DAL.Configurations 6 | { 7 | internal class UserConfiguration : IEntityTypeConfiguration 8 | { 9 | public void Configure(EntityTypeBuilder builder) 10 | { 11 | builder.HasIndex(x => x.Email).IsUnique(); 12 | builder.Property(x => x.Email).IsRequired().HasMaxLength(500); 13 | builder.Property(x => x.Password).IsRequired().HasMaxLength(500); 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /src/Modules/Users/Modular.Modules.Users.Core/DAL/Migrations/20210722170747_Users_Init.Designer.cs: -------------------------------------------------------------------------------- 1 | // 2 | using System; 3 | using Microsoft.EntityFrameworkCore; 4 | using Microsoft.EntityFrameworkCore.Infrastructure; 5 | using Microsoft.EntityFrameworkCore.Migrations; 6 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 7 | using Modular.Modules.Users.Core.DAL; 8 | using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; 9 | 10 | namespace Modular.Modules.Users.Core.DAL.Migrations 11 | { 12 | [DbContext(typeof(UsersDbContext))] 13 | [Migration("20210722170747_Users_Init")] 14 | partial class Users_Init 15 | { 16 | protected override void BuildTargetModel(ModelBuilder modelBuilder) 17 | { 18 | #pragma warning disable 612, 618 19 | modelBuilder 20 | .HasDefaultSchema("users") 21 | .HasAnnotation("Relational:MaxIdentifierLength", 63) 22 | .HasAnnotation("ProductVersion", "5.0.7") 23 | .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); 24 | 25 | modelBuilder.Entity("Modular.Modules.Users.Core.Entities.Role", b => 26 | { 27 | b.Property("Name") 28 | .HasMaxLength(100) 29 | .HasColumnType("character varying(100)"); 30 | 31 | b.Property("Permissions") 32 | .HasColumnType("text"); 33 | 34 | b.HasKey("Name"); 35 | 36 | b.ToTable("Roles"); 37 | }); 38 | 39 | modelBuilder.Entity("Modular.Modules.Users.Core.Entities.User", b => 40 | { 41 | b.Property("Id") 42 | .ValueGeneratedOnAdd() 43 | .HasColumnType("uuid"); 44 | 45 | b.Property("CreatedAt") 46 | .HasColumnType("timestamp without time zone"); 47 | 48 | b.Property("Email") 49 | .IsRequired() 50 | .HasMaxLength(500) 51 | .HasColumnType("character varying(500)"); 52 | 53 | b.Property("Password") 54 | .IsRequired() 55 | .HasMaxLength(500) 56 | .HasColumnType("character varying(500)"); 57 | 58 | b.Property("RoleId") 59 | .HasColumnType("character varying(100)"); 60 | 61 | b.Property("State") 62 | .HasColumnType("integer"); 63 | 64 | b.HasKey("Id"); 65 | 66 | b.HasIndex("Email") 67 | .IsUnique(); 68 | 69 | b.HasIndex("RoleId"); 70 | 71 | b.ToTable("Users"); 72 | }); 73 | 74 | modelBuilder.Entity("Modular.Infrastructure.Messaging.Outbox.InboxMessage", b => 75 | { 76 | b.Property("Id") 77 | .ValueGeneratedOnAdd() 78 | .HasColumnType("uuid"); 79 | 80 | b.Property("Name") 81 | .HasColumnType("text"); 82 | 83 | b.Property("ProcessedAt") 84 | .HasColumnType("timestamp without time zone"); 85 | 86 | b.Property("ReceivedAt") 87 | .HasColumnType("timestamp without time zone"); 88 | 89 | b.HasKey("Id"); 90 | 91 | b.ToTable("Inbox"); 92 | }); 93 | 94 | modelBuilder.Entity("Modular.Infrastructure.Messaging.Outbox.OutboxMessage", b => 95 | { 96 | b.Property("Id") 97 | .ValueGeneratedOnAdd() 98 | .HasColumnType("uuid"); 99 | 100 | b.Property("CorrelationId") 101 | .HasColumnType("uuid"); 102 | 103 | b.Property("CreatedAt") 104 | .HasColumnType("timestamp without time zone"); 105 | 106 | b.Property("Data") 107 | .HasColumnType("text"); 108 | 109 | b.Property("Name") 110 | .HasColumnType("text"); 111 | 112 | b.Property("SentAt") 113 | .HasColumnType("timestamp without time zone"); 114 | 115 | b.Property("TraceId") 116 | .HasColumnType("text"); 117 | 118 | b.Property("Type") 119 | .HasColumnType("text"); 120 | 121 | b.Property("UserId") 122 | .HasColumnType("uuid"); 123 | 124 | b.HasKey("Id"); 125 | 126 | b.ToTable("Outbox"); 127 | }); 128 | 129 | modelBuilder.Entity("Modular.Modules.Users.Core.Entities.User", b => 130 | { 131 | b.HasOne("Modular.Modules.Users.Core.Entities.Role", "Role") 132 | .WithMany("Users") 133 | .HasForeignKey("RoleId"); 134 | 135 | b.Navigation("Role"); 136 | }); 137 | 138 | modelBuilder.Entity("Modular.Modules.Users.Core.Entities.Role", b => 139 | { 140 | b.Navigation("Users"); 141 | }); 142 | #pragma warning restore 612, 618 143 | } 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/Modules/Users/Modular.Modules.Users.Core/DAL/Migrations/20210722170747_Users_Init.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.EntityFrameworkCore.Migrations; 3 | 4 | namespace Modular.Modules.Users.Core.DAL.Migrations 5 | { 6 | public partial class Users_Init : Migration 7 | { 8 | protected override void Up(MigrationBuilder migrationBuilder) 9 | { 10 | migrationBuilder.EnsureSchema( 11 | name: "users"); 12 | 13 | migrationBuilder.CreateTable( 14 | name: "Inbox", 15 | schema: "users", 16 | columns: table => new 17 | { 18 | Id = table.Column(type: "uuid", nullable: false), 19 | Name = table.Column(type: "text", nullable: true), 20 | ReceivedAt = table.Column(type: "timestamp without time zone", nullable: false), 21 | ProcessedAt = table.Column(type: "timestamp without time zone", nullable: true) 22 | }, 23 | constraints: table => 24 | { 25 | table.PrimaryKey("PK_Inbox", x => x.Id); 26 | }); 27 | 28 | migrationBuilder.CreateTable( 29 | name: "Outbox", 30 | schema: "users", 31 | columns: table => new 32 | { 33 | Id = table.Column(type: "uuid", nullable: false), 34 | CorrelationId = table.Column(type: "uuid", nullable: false), 35 | UserId = table.Column(type: "uuid", nullable: true), 36 | Name = table.Column(type: "text", nullable: true), 37 | Type = table.Column(type: "text", nullable: true), 38 | Data = table.Column(type: "text", nullable: true), 39 | TraceId = table.Column(type: "text", nullable: true), 40 | CreatedAt = table.Column(type: "timestamp without time zone", nullable: false), 41 | SentAt = table.Column(type: "timestamp without time zone", nullable: true) 42 | }, 43 | constraints: table => 44 | { 45 | table.PrimaryKey("PK_Outbox", x => x.Id); 46 | }); 47 | 48 | migrationBuilder.CreateTable( 49 | name: "Roles", 50 | schema: "users", 51 | columns: table => new 52 | { 53 | Name = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), 54 | Permissions = table.Column(type: "text", nullable: true) 55 | }, 56 | constraints: table => 57 | { 58 | table.PrimaryKey("PK_Roles", x => x.Name); 59 | }); 60 | 61 | migrationBuilder.CreateTable( 62 | name: "Users", 63 | schema: "users", 64 | columns: table => new 65 | { 66 | Id = table.Column(type: "uuid", nullable: false), 67 | Email = table.Column(type: "character varying(500)", maxLength: 500, nullable: false), 68 | Password = table.Column(type: "character varying(500)", maxLength: 500, nullable: false), 69 | RoleId = table.Column(type: "character varying(100)", nullable: true), 70 | State = table.Column(type: "integer", nullable: false), 71 | CreatedAt = table.Column(type: "timestamp without time zone", nullable: false) 72 | }, 73 | constraints: table => 74 | { 75 | table.PrimaryKey("PK_Users", x => x.Id); 76 | table.ForeignKey( 77 | name: "FK_Users_Roles_RoleId", 78 | column: x => x.RoleId, 79 | principalSchema: "users", 80 | principalTable: "Roles", 81 | principalColumn: "Name", 82 | onDelete: ReferentialAction.Restrict); 83 | }); 84 | 85 | migrationBuilder.CreateIndex( 86 | name: "IX_Users_Email", 87 | schema: "users", 88 | table: "Users", 89 | column: "Email", 90 | unique: true); 91 | 92 | migrationBuilder.CreateIndex( 93 | name: "IX_Users_RoleId", 94 | schema: "users", 95 | table: "Users", 96 | column: "RoleId"); 97 | } 98 | 99 | protected override void Down(MigrationBuilder migrationBuilder) 100 | { 101 | migrationBuilder.DropTable( 102 | name: "Inbox", 103 | schema: "users"); 104 | 105 | migrationBuilder.DropTable( 106 | name: "Outbox", 107 | schema: "users"); 108 | 109 | migrationBuilder.DropTable( 110 | name: "Users", 111 | schema: "users"); 112 | 113 | migrationBuilder.DropTable( 114 | name: "Roles", 115 | schema: "users"); 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/Modules/Users/Modular.Modules.Users.Core/DAL/Migrations/UsersDbContextModelSnapshot.cs: -------------------------------------------------------------------------------- 1 | // 2 | using System; 3 | using Microsoft.EntityFrameworkCore; 4 | using Microsoft.EntityFrameworkCore.Infrastructure; 5 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 6 | using Modular.Modules.Users.Core.DAL; 7 | using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; 8 | 9 | namespace Modular.Modules.Users.Core.DAL.Migrations 10 | { 11 | [DbContext(typeof(UsersDbContext))] 12 | partial class UsersDbContextModelSnapshot : ModelSnapshot 13 | { 14 | protected override void BuildModel(ModelBuilder modelBuilder) 15 | { 16 | #pragma warning disable 612, 618 17 | modelBuilder 18 | .HasDefaultSchema("users") 19 | .HasAnnotation("Relational:MaxIdentifierLength", 63) 20 | .HasAnnotation("ProductVersion", "5.0.7") 21 | .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); 22 | 23 | modelBuilder.Entity("Modular.Modules.Users.Core.Entities.Role", b => 24 | { 25 | b.Property("Name") 26 | .HasMaxLength(100) 27 | .HasColumnType("character varying(100)"); 28 | 29 | b.Property("Permissions") 30 | .HasColumnType("text"); 31 | 32 | b.HasKey("Name"); 33 | 34 | b.ToTable("Roles"); 35 | }); 36 | 37 | modelBuilder.Entity("Modular.Modules.Users.Core.Entities.User", b => 38 | { 39 | b.Property("Id") 40 | .ValueGeneratedOnAdd() 41 | .HasColumnType("uuid"); 42 | 43 | b.Property("CreatedAt") 44 | .HasColumnType("timestamp without time zone"); 45 | 46 | b.Property("Email") 47 | .IsRequired() 48 | .HasMaxLength(500) 49 | .HasColumnType("character varying(500)"); 50 | 51 | b.Property("Password") 52 | .IsRequired() 53 | .HasMaxLength(500) 54 | .HasColumnType("character varying(500)"); 55 | 56 | b.Property("RoleId") 57 | .HasColumnType("character varying(100)"); 58 | 59 | b.Property("State") 60 | .HasColumnType("integer"); 61 | 62 | b.HasKey("Id"); 63 | 64 | b.HasIndex("Email") 65 | .IsUnique(); 66 | 67 | b.HasIndex("RoleId"); 68 | 69 | b.ToTable("Users"); 70 | }); 71 | 72 | modelBuilder.Entity("Modular.Infrastructure.Messaging.Outbox.InboxMessage", b => 73 | { 74 | b.Property("Id") 75 | .ValueGeneratedOnAdd() 76 | .HasColumnType("uuid"); 77 | 78 | b.Property("Name") 79 | .HasColumnType("text"); 80 | 81 | b.Property("ProcessedAt") 82 | .HasColumnType("timestamp without time zone"); 83 | 84 | b.Property("ReceivedAt") 85 | .HasColumnType("timestamp without time zone"); 86 | 87 | b.HasKey("Id"); 88 | 89 | b.ToTable("Inbox"); 90 | }); 91 | 92 | modelBuilder.Entity("Modular.Infrastructure.Messaging.Outbox.OutboxMessage", b => 93 | { 94 | b.Property("Id") 95 | .ValueGeneratedOnAdd() 96 | .HasColumnType("uuid"); 97 | 98 | b.Property("CorrelationId") 99 | .HasColumnType("uuid"); 100 | 101 | b.Property("CreatedAt") 102 | .HasColumnType("timestamp without time zone"); 103 | 104 | b.Property("Data") 105 | .HasColumnType("text"); 106 | 107 | b.Property("Name") 108 | .HasColumnType("text"); 109 | 110 | b.Property("SentAt") 111 | .HasColumnType("timestamp without time zone"); 112 | 113 | b.Property("TraceId") 114 | .HasColumnType("text"); 115 | 116 | b.Property("Type") 117 | .HasColumnType("text"); 118 | 119 | b.Property("UserId") 120 | .HasColumnType("uuid"); 121 | 122 | b.HasKey("Id"); 123 | 124 | b.ToTable("Outbox"); 125 | }); 126 | 127 | modelBuilder.Entity("Modular.Modules.Users.Core.Entities.User", b => 128 | { 129 | b.HasOne("Modular.Modules.Users.Core.Entities.Role", "Role") 130 | .WithMany("Users") 131 | .HasForeignKey("RoleId"); 132 | 133 | b.Navigation("Role"); 134 | }); 135 | 136 | modelBuilder.Entity("Modular.Modules.Users.Core.Entities.Role", b => 137 | { 138 | b.Navigation("Users"); 139 | }); 140 | #pragma warning restore 612, 618 141 | } 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/Modules/Users/Modular.Modules.Users.Core/DAL/Repositories/RoleRepository.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading.Tasks; 3 | using Microsoft.EntityFrameworkCore; 4 | using Modular.Modules.Users.Core.Entities; 5 | using Modular.Modules.Users.Core.Repositories; 6 | 7 | namespace Modular.Modules.Users.Core.DAL.Repositories 8 | { 9 | internal class RoleRepository : IRoleRepository 10 | { 11 | private readonly UsersDbContext _context; 12 | private readonly DbSet _roles; 13 | 14 | public RoleRepository(UsersDbContext context) 15 | { 16 | _context = context; 17 | _roles = context.Roles; 18 | } 19 | 20 | public Task GetAsync(string name) => _roles.SingleOrDefaultAsync(x => x.Name == name); 21 | 22 | public async Task> GetAllAsync() => await _roles.ToListAsync(); 23 | 24 | public async Task AddAsync(Role role) 25 | { 26 | await _roles.AddAsync(role); 27 | await _context.SaveChangesAsync(); 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /src/Modules/Users/Modular.Modules.Users.Core/DAL/Repositories/UserRepository.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Microsoft.EntityFrameworkCore; 4 | using Modular.Modules.Users.Core.Entities; 5 | using Modular.Modules.Users.Core.Repositories; 6 | 7 | namespace Modular.Modules.Users.Core.DAL.Repositories 8 | { 9 | internal class UserRepository : IUserRepository 10 | { 11 | private readonly UsersDbContext _context; 12 | private readonly DbSet _users; 13 | 14 | public UserRepository(UsersDbContext context) 15 | { 16 | _context = context; 17 | _users = _context.Users; 18 | } 19 | 20 | public Task GetAsync(Guid id) 21 | => _users.Include(x => x.Role).SingleOrDefaultAsync(x => x.Id == id); 22 | 23 | public Task GetAsync(string email) 24 | => _users.Include(x => x.Role).SingleOrDefaultAsync(x => x.Email == email); 25 | 26 | public async Task AddAsync(User user) 27 | { 28 | await _users.AddAsync(user); 29 | await _context.SaveChangesAsync(); 30 | } 31 | 32 | public async Task UpdateAsync(User user) 33 | { 34 | _users.Update(user); 35 | await _context.SaveChangesAsync(); 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /src/Modules/Users/Modular.Modules.Users.Core/DAL/UsersDbContext.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Modular.Modules.Users.Core.Entities; 3 | using Modular.Infrastructure.Messaging.Outbox; 4 | 5 | namespace Modular.Modules.Users.Core.DAL 6 | { 7 | internal class UsersDbContext : DbContext 8 | { 9 | public DbSet Inbox { get; set; } 10 | public DbSet Outbox { get; set; } 11 | public DbSet Roles { get; set; } 12 | public DbSet Users { get; set; } 13 | 14 | public UsersDbContext(DbContextOptions options) : base(options) 15 | { 16 | } 17 | 18 | protected override void OnModelCreating(ModelBuilder modelBuilder) 19 | { 20 | modelBuilder.HasDefaultSchema("users"); 21 | modelBuilder.ApplyConfigurationsFromAssembly(GetType().Assembly); 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /src/Modules/Users/Modular.Modules.Users.Core/DAL/UsersInitializer.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading.Tasks; 3 | using Microsoft.EntityFrameworkCore; 4 | using Microsoft.Extensions.Logging; 5 | using Modular.Modules.Users.Core.Entities; 6 | using Modular.Infrastructure; 7 | 8 | namespace Modular.Modules.Users.Core.DAL 9 | { 10 | internal sealed class UsersInitializer : IInitializer 11 | { 12 | private readonly HashSet _permissions = new() 13 | { 14 | "first", "second", "third", "users" 15 | }; 16 | 17 | private readonly UsersDbContext _dbContext; 18 | private readonly ILogger _logger; 19 | 20 | public UsersInitializer(UsersDbContext dbContext, ILogger logger) 21 | { 22 | _dbContext = dbContext; 23 | _logger = logger; 24 | } 25 | 26 | public async Task InitAsync() 27 | { 28 | if (await _dbContext.Roles.AnyAsync()) 29 | { 30 | return; 31 | } 32 | 33 | await AddRolesAsync(); 34 | await _dbContext.SaveChangesAsync(); 35 | } 36 | 37 | private async Task AddRolesAsync() 38 | { 39 | await _dbContext.Roles.AddAsync(new Role 40 | { 41 | Name = "admin", 42 | Permissions = _permissions 43 | }); 44 | await _dbContext.Roles.AddAsync(new Role 45 | { 46 | Name = "user", 47 | Permissions = new List() 48 | }); 49 | 50 | _logger.LogInformation("Initialized roles."); 51 | } 52 | } 53 | } -------------------------------------------------------------------------------- /src/Modules/Users/Modular.Modules.Users.Core/DAL/UsersUnitOfWork.cs: -------------------------------------------------------------------------------- 1 | using Modular.Infrastructure.Postgres; 2 | 3 | namespace Modular.Modules.Users.Core.DAL 4 | { 5 | internal class UsersUnitOfWork : PostgresUnitOfWork 6 | { 7 | public UsersUnitOfWork(UsersDbContext dbContext) : base(dbContext) 8 | { 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /src/Modules/Users/Modular.Modules.Users.Core/DTO/UserDetailsDto.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Modular.Modules.Users.Core.DTO 4 | { 5 | public class UserDetailsDto : UserDto 6 | { 7 | public IEnumerable Permissions { get; set; } 8 | } 9 | } -------------------------------------------------------------------------------- /src/Modules/Users/Modular.Modules.Users.Core/DTO/UserDto.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Modular.Modules.Users.Core.DTO 4 | { 5 | public class UserDto 6 | { 7 | public Guid UserId { get; set; } 8 | public string Email { get; set; } 9 | public string Role { get; set; } 10 | public string State { get; set; } 11 | public DateTime CreatedAt { get; set; } 12 | } 13 | } -------------------------------------------------------------------------------- /src/Modules/Users/Modular.Modules.Users.Core/Entities/Role.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Modular.Modules.Users.Core.Entities 4 | { 5 | internal class Role 6 | { 7 | public string Name { get; set; } 8 | public IEnumerable Permissions { get; set; } 9 | public IEnumerable Users { get; set; } 10 | 11 | public static string Default => User; 12 | public const string User = "user"; 13 | public const string Admin = "admin"; 14 | } 15 | } -------------------------------------------------------------------------------- /src/Modules/Users/Modular.Modules.Users.Core/Entities/User.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Modular.Modules.Users.Core.Entities 4 | { 5 | internal class User 6 | { 7 | public Guid Id { get; set; } 8 | public string Email { get; set; } 9 | public string Password { get; set; } 10 | public Role Role { get; set; } 11 | public string RoleId { get; set; } 12 | public UserState State { get; set; } 13 | public DateTime CreatedAt { get; set; } 14 | } 15 | } -------------------------------------------------------------------------------- /src/Modules/Users/Modular.Modules.Users.Core/Entities/UserState.cs: -------------------------------------------------------------------------------- 1 | namespace Modular.Modules.Users.Core.Entities 2 | { 3 | internal enum UserState 4 | { 5 | Active = 1, 6 | Locked = 2 7 | } 8 | } -------------------------------------------------------------------------------- /src/Modules/Users/Modular.Modules.Users.Core/Events/SignedIn.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Modular.Abstractions.Events; 3 | 4 | namespace Modular.Modules.Users.Core.Events 5 | { 6 | internal record SignedIn(Guid UserId) : IEvent; 7 | } -------------------------------------------------------------------------------- /src/Modules/Users/Modular.Modules.Users.Core/Events/SignedUp.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Modular.Abstractions.Events; 3 | 4 | namespace Modular.Modules.Users.Core.Events 5 | { 6 | internal record SignedUp(Guid UserId, string Email, string Role) : IEvent; 7 | } -------------------------------------------------------------------------------- /src/Modules/Users/Modular.Modules.Users.Core/Events/UserStateUpdated.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Modular.Abstractions.Events; 3 | 4 | namespace Modular.Modules.Users.Core.Events 5 | { 6 | internal record UserStateUpdated(Guid UserId, string State) : IEvent; 7 | } -------------------------------------------------------------------------------- /src/Modules/Users/Modular.Modules.Users.Core/Exceptions/EmailInUseException.cs: -------------------------------------------------------------------------------- 1 | using Modular.Abstractions.Exceptions; 2 | 3 | namespace Modular.Modules.Users.Core.Exceptions 4 | { 5 | internal class EmailInUseException : ModularException 6 | { 7 | public EmailInUseException() : base("Email is already in use.") 8 | { 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /src/Modules/Users/Modular.Modules.Users.Core/Exceptions/InvalidCredentialsException.cs: -------------------------------------------------------------------------------- 1 | using Modular.Abstractions.Exceptions; 2 | 3 | namespace Modular.Modules.Users.Core.Exceptions 4 | { 5 | internal class InvalidCredentialsException : ModularException 6 | { 7 | public InvalidCredentialsException() : base("Invalid credentials.") 8 | { 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /src/Modules/Users/Modular.Modules.Users.Core/Exceptions/InvalidEmailException.cs: -------------------------------------------------------------------------------- 1 | using Modular.Abstractions.Exceptions; 2 | 3 | namespace Modular.Modules.Users.Core.Exceptions 4 | { 5 | internal class InvalidEmailException : ModularException 6 | { 7 | public string Email { get; } 8 | 9 | public InvalidEmailException(string email) : base($"State is invalid: '{email}'.") 10 | { 11 | Email = email; 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /src/Modules/Users/Modular.Modules.Users.Core/Exceptions/InvalidPasswordException.cs: -------------------------------------------------------------------------------- 1 | using Modular.Abstractions.Exceptions; 2 | 3 | namespace Modular.Modules.Users.Core.Exceptions 4 | { 5 | internal class InvalidPasswordException : ModularException 6 | { 7 | public string Reason { get; } 8 | 9 | public InvalidPasswordException(string reason) : base($"Invalid password: {reason}.") 10 | { 11 | Reason = reason; 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /src/Modules/Users/Modular.Modules.Users.Core/Exceptions/InvalidUserStateException.cs: -------------------------------------------------------------------------------- 1 | using Modular.Abstractions.Exceptions; 2 | 3 | namespace Modular.Modules.Users.Core.Exceptions 4 | { 5 | internal class InvalidUserStateException : ModularException 6 | { 7 | public string State { get; } 8 | 9 | public InvalidUserStateException(string state) : base($"User state is invalid: '{state}'.") 10 | { 11 | State = state; 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /src/Modules/Users/Modular.Modules.Users.Core/Exceptions/RoleNotFoundException.cs: -------------------------------------------------------------------------------- 1 | using Modular.Abstractions.Exceptions; 2 | 3 | namespace Modular.Modules.Users.Core.Exceptions 4 | { 5 | internal class RoleNotFoundException : ModularException 6 | { 7 | public RoleNotFoundException(string role) : base($"Role: '{role}' was not found.") 8 | { 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /src/Modules/Users/Modular.Modules.Users.Core/Exceptions/SignUpDisabledException.cs: -------------------------------------------------------------------------------- 1 | using Modular.Abstractions.Exceptions; 2 | 3 | namespace Modular.Modules.Users.Core.Exceptions 4 | { 5 | internal class SignUpDisabledException : ModularException 6 | { 7 | public SignUpDisabledException() : base("Sign up is disabled.") 8 | { 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /src/Modules/Users/Modular.Modules.Users.Core/Exceptions/UserNotActiveException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Modular.Abstractions.Exceptions; 3 | 4 | namespace Modular.Modules.Users.Core.Exceptions 5 | { 6 | internal class UserNotActiveException : ModularException 7 | { 8 | public Guid UserId { get; } 9 | 10 | public UserNotActiveException(Guid userId) : base($"User with ID: '{userId}' is not active.") 11 | { 12 | UserId = userId; 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /src/Modules/Users/Modular.Modules.Users.Core/Exceptions/UserNotFoundException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Modular.Abstractions.Exceptions; 3 | 4 | namespace Modular.Modules.Users.Core.Exceptions 5 | { 6 | internal class UserNotFoundException : ModularException 7 | { 8 | public string Email { get; } 9 | public Guid UserId { get; } 10 | 11 | public UserNotFoundException(Guid userId) : base($"User with ID: '{userId}' was not found.") 12 | { 13 | UserId = userId; 14 | } 15 | 16 | public UserNotFoundException(string email) : base($"User with email: '{email}' was not found.") 17 | { 18 | Email = email; 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /src/Modules/Users/Modular.Modules.Users.Core/Exceptions/UserStateCannotBeChangedException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Modular.Abstractions.Exceptions; 3 | 4 | namespace Modular.Modules.Users.Core.Exceptions 5 | { 6 | internal class UserStateCannotBeChangedException : ModularException 7 | { 8 | public string State { get; } 9 | public Guid UserId { get; } 10 | 11 | public UserStateCannotBeChangedException(string state, Guid userId) 12 | : base($"User state cannot be changed to: '{state}' for user with ID: '{userId}'.") 13 | { 14 | State = state; 15 | UserId = userId; 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /src/Modules/Users/Modular.Modules.Users.Core/Extensions.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | using Microsoft.AspNetCore.Identity; 3 | using Microsoft.Extensions.DependencyInjection; 4 | using Modular.Modules.Users.Core.DAL; 5 | using Modular.Modules.Users.Core.DAL.Repositories; 6 | using Modular.Modules.Users.Core.Entities; 7 | using Modular.Modules.Users.Core.Repositories; 8 | using Modular.Modules.Users.Core.Services; 9 | using Modular.Infrastructure; 10 | using Modular.Infrastructure.Messaging.Outbox; 11 | using Modular.Infrastructure.Postgres; 12 | 13 | [assembly: InternalsVisibleTo("Modular.Modules.Users.Api")] 14 | [assembly: InternalsVisibleTo("Modular.Modules.Users.Tests.Integration")] 15 | [assembly: InternalsVisibleTo("Modular.Modules.Users.Tests.Unit")] 16 | [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] 17 | 18 | namespace Modular.Modules.Users.Core 19 | { 20 | internal static class Extensions 21 | { 22 | public static IServiceCollection AddCore(this IServiceCollection services) 23 | { 24 | var registrationOptions = services.GetOptions("users:registration"); 25 | services.AddSingleton(registrationOptions); 26 | 27 | return services 28 | .AddSingleton() 29 | .AddScoped() 30 | .AddScoped() 31 | .AddSingleton, PasswordHasher>() 32 | .AddPostgres() 33 | .AddOutbox() 34 | .AddUnitOfWork() 35 | .AddInitializer(); 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /src/Modules/Users/Modular.Modules.Users.Core/Modular.Modules.Users.Core.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/Modules/Users/Modular.Modules.Users.Core/Queries/BrowseUsers.cs: -------------------------------------------------------------------------------- 1 | using Modular.Modules.Users.Core.DTO; 2 | using Modular.Abstractions.Queries; 3 | 4 | namespace Modular.Modules.Users.Core.Queries 5 | { 6 | internal class BrowseUsers : PagedQuery 7 | { 8 | public string Email { get; set; } 9 | public string Role { get; set; } 10 | public string State { get; set; } 11 | } 12 | } -------------------------------------------------------------------------------- /src/Modules/Users/Modular.Modules.Users.Core/Queries/GetUser.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Modular.Modules.Users.Core.DTO; 3 | using Modular.Abstractions.Queries; 4 | 5 | namespace Modular.Modules.Users.Core.Queries 6 | { 7 | internal class GetUser : IQuery 8 | { 9 | public Guid UserId { get; set; } 10 | } 11 | } -------------------------------------------------------------------------------- /src/Modules/Users/Modular.Modules.Users.Core/Queries/Handlers/BrowseUsersHandler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using Microsoft.EntityFrameworkCore; 6 | using Modular.Modules.Users.Core.DAL; 7 | using Modular.Modules.Users.Core.DTO; 8 | using Modular.Modules.Users.Core.Entities; 9 | using Modular.Abstractions.Queries; 10 | using Modular.Infrastructure.Postgres; 11 | 12 | namespace Modular.Modules.Users.Core.Queries.Handlers 13 | { 14 | internal sealed class BrowseUsersHandler : IQueryHandler> 15 | { 16 | private readonly UsersDbContext _dbContext; 17 | 18 | public BrowseUsersHandler(UsersDbContext dbContext) 19 | { 20 | _dbContext = dbContext; 21 | } 22 | 23 | public Task> HandleAsync(BrowseUsers query, CancellationToken cancellationToken = default) 24 | { 25 | var users = _dbContext.Users.AsQueryable(); 26 | if (!string.IsNullOrWhiteSpace(query.Email)) 27 | { 28 | users = users.Where(x => x.Email == query.Email); 29 | } 30 | 31 | if (!string.IsNullOrWhiteSpace(query.Role)) 32 | { 33 | users = users.Where(x => x.RoleId == query.Role); 34 | } 35 | 36 | if (!string.IsNullOrWhiteSpace(query.State) && Enum.TryParse(query.State, true, out var state)) 37 | { 38 | users = users.Where(x => x.State == state); 39 | } 40 | 41 | return users.AsNoTracking() 42 | .Include(x => x.Role) 43 | .OrderByDescending(x => x.CreatedAt) 44 | .Select(x => x.AsDto()) 45 | .PaginateAsync(query, cancellationToken); 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /src/Modules/Users/Modular.Modules.Users.Core/Queries/Handlers/Extensions.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Modular.Modules.Users.Core.DTO; 3 | using Modular.Modules.Users.Core.Entities; 4 | 5 | namespace Modular.Modules.Users.Core.Queries.Handlers 6 | { 7 | internal static class Extensions 8 | { 9 | private static readonly Dictionary States = new() 10 | { 11 | [UserState.Active] = UserState.Active.ToString().ToLowerInvariant(), 12 | [UserState.Locked] = UserState.Locked.ToString().ToLowerInvariant() 13 | }; 14 | 15 | public static UserDto AsDto(this User user) 16 | => user.Map(); 17 | 18 | public static UserDetailsDto AsDetailsDto(this User user) 19 | { 20 | var dto = user.Map(); 21 | dto.Permissions = user.Role.Permissions; 22 | 23 | return dto; 24 | } 25 | 26 | private static T Map(this User user) where T : UserDto, new() 27 | => new() 28 | { 29 | UserId = user.Id, 30 | Email = user.Email, 31 | State = States[user.State], 32 | Role = user.Role.Name, 33 | CreatedAt = user.CreatedAt 34 | }; 35 | } 36 | } -------------------------------------------------------------------------------- /src/Modules/Users/Modular.Modules.Users.Core/Queries/Handlers/GetUserHandler.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | using Microsoft.EntityFrameworkCore; 4 | using Modular.Modules.Users.Core.DAL; 5 | using Modular.Modules.Users.Core.DTO; 6 | using Modular.Abstractions.Queries; 7 | 8 | namespace Modular.Modules.Users.Core.Queries.Handlers 9 | { 10 | internal sealed class GetUserHandler : IQueryHandler 11 | { 12 | private readonly UsersDbContext _dbContext; 13 | 14 | public GetUserHandler(UsersDbContext dbContext) 15 | { 16 | _dbContext = dbContext; 17 | } 18 | 19 | public async Task HandleAsync(GetUser query, CancellationToken cancellationToken = default) 20 | { 21 | var user = await _dbContext.Users 22 | .AsNoTracking() 23 | .Include(x => x.Role) 24 | .SingleOrDefaultAsync(x => x.Id == query.UserId, cancellationToken); 25 | 26 | return user?.AsDetailsDto(); 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /src/Modules/Users/Modular.Modules.Users.Core/RegistrationOptions.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Modular.Modules.Users.Core 4 | { 5 | public class RegistrationOptions 6 | { 7 | public bool Enabled { get; set; } 8 | public IEnumerable InvalidEmailProviders { get; set; } 9 | } 10 | } -------------------------------------------------------------------------------- /src/Modules/Users/Modular.Modules.Users.Core/Repositories/IRoleRepository.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading.Tasks; 3 | using Modular.Modules.Users.Core.Entities; 4 | 5 | namespace Modular.Modules.Users.Core.Repositories 6 | { 7 | internal interface IRoleRepository 8 | { 9 | Task GetAsync(string name); 10 | Task> GetAllAsync(); 11 | Task AddAsync(Role role); 12 | } 13 | } -------------------------------------------------------------------------------- /src/Modules/Users/Modular.Modules.Users.Core/Repositories/IUserRepository.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Modular.Modules.Users.Core.Entities; 4 | 5 | namespace Modular.Modules.Users.Core.Repositories 6 | { 7 | internal interface IUserRepository 8 | { 9 | Task GetAsync(Guid id); 10 | Task GetAsync(string email); 11 | Task AddAsync(User user); 12 | Task UpdateAsync(User user); 13 | } 14 | } -------------------------------------------------------------------------------- /src/Modules/Users/Modular.Modules.Users.Core/Services/IUserRequestStorage.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Modular.Abstractions.Auth; 3 | 4 | namespace Modular.Modules.Users.Core.Services 5 | { 6 | internal interface IUserRequestStorage 7 | { 8 | void SetToken(Guid commandId, JsonWebToken jwt); 9 | JsonWebToken GetToken(Guid commandId); 10 | } 11 | } -------------------------------------------------------------------------------- /src/Modules/Users/Modular.Modules.Users.Core/Services/UserRequestStorage.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Modular.Abstractions.Auth; 3 | using Modular.Abstractions.Storage; 4 | 5 | namespace Modular.Modules.Users.Core.Services 6 | { 7 | internal sealed class UserRequestStorage : IUserRequestStorage 8 | { 9 | private readonly IRequestStorage _requestStorage; 10 | 11 | public UserRequestStorage(IRequestStorage requestStorage) 12 | { 13 | _requestStorage = requestStorage; 14 | } 15 | 16 | public void SetToken(Guid commandId, JsonWebToken jwt) 17 | => _requestStorage.Set(GetKey(commandId), jwt); 18 | 19 | public JsonWebToken GetToken(Guid commandId) 20 | => _requestStorage.Get(GetKey(commandId)); 21 | 22 | private static string GetKey(Guid commandId) => $"jwt:{commandId:N}"; 23 | } 24 | } -------------------------------------------------------------------------------- /src/Modules/Users/Users.rest: -------------------------------------------------------------------------------- 1 | @url = http://localhost:5000 2 | 3 | ### 4 | GET {{url}} 5 | 6 | ### 7 | @email = user1@modular.io 8 | @adminEmail = admin1@modular.io 9 | @password = Secret123! 10 | 11 | ### Register as the regular user 12 | POST {{url}}/account/sign-up 13 | Content-Type: application/json 14 | 15 | { 16 | "email": "{{email}}", 17 | "password": "{{password}}" 18 | } 19 | 20 | ### Login as the regular user 21 | # @name sign_in 22 | POST {{url}}/account/sign-in 23 | Content-Type: application/json 24 | 25 | { 26 | "email": "{{email}}", 27 | "password": "{{password}}" 28 | } 29 | 30 | 31 | ### 32 | @authCookie = {{sign_in.response.headers.$.set-cookie}} 33 | @userId = {{sign_in.response.body.$.id}} 34 | 35 | ### Get user account 36 | GET {{url}}/account 37 | Set-Cookie: {{authCookie}} 38 | 39 | ### Change current password 40 | PUT {{url}}/password 41 | Set-Cookie: {{authCookie}} 42 | Content-Type: application/json 43 | 44 | { 45 | "currentPassword": "{{password}}", 46 | "newPassword": "Secret1234!" 47 | } 48 | 49 | ### Register as the admin user 50 | POST {{url}}/account/sign-up 51 | Content-Type: application/json 52 | 53 | { 54 | "email": "{{adminEmail}}", 55 | "password": "{{password}}", 56 | "role": "admin" 57 | } 58 | 59 | ### Login as the admin user 60 | # @name sign_in 61 | POST {{url}}/account/sign-in 62 | Content-Type: application/json 63 | 64 | { 65 | "email": "{{adminEmail}}", 66 | "password": "{{password}}" 67 | } 68 | 69 | 70 | ### Browse users as admin 71 | GET {{url}}/users 72 | Set-Cookie: {{authCookie}} 73 | 74 | @userId = 00000000-0000-0000-0000-000000000000 75 | 76 | ### Get user details as admin 77 | GET {{url}}/users/{{userId}} 78 | Set-Cookie: {{authCookie}} 79 | 80 | ### Change user state as admin 81 | PUT {{url}}/users/{{userId}}/state 82 | Set-Cookie: {{authCookie}} 83 | Content-Type: application/json 84 | 85 | { 86 | "state": "active" 87 | } -------------------------------------------------------------------------------- /src/Shared/Modular.Shared.Tests/AuthHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Modular.Infrastructure.Auth; 4 | using Modular.Infrastructure.Time; 5 | 6 | namespace Modular.Shared.Tests 7 | { 8 | public static class AuthHelper 9 | { 10 | private static readonly AuthManager AuthManager; 11 | 12 | static AuthHelper() 13 | { 14 | var options = OptionsHelper.GetOptions("auth"); 15 | AuthManager = new AuthManager(options, new UtcClock()); 16 | } 17 | 18 | public static string GenerateJwt(Guid userId, string role = null, string audience = null, 19 | IDictionary> claims = null) 20 | => AuthManager.CreateToken(userId, role, audience, claims).AccessToken; 21 | } 22 | } -------------------------------------------------------------------------------- /src/Shared/Modular.Shared.Tests/DbHelper.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Microsoft.Extensions.Configuration; 3 | 4 | namespace Modular.Shared.Tests 5 | { 6 | public static class DbHelper 7 | { 8 | private static readonly IConfiguration Configuration = OptionsHelper.GetConfigurationRoot(); 9 | 10 | public static DbContextOptions GetOptions() where T : DbContext 11 | => new DbContextOptionsBuilder() 12 | .UseNpgsql(Configuration["postgres:connectionString"]) 13 | .EnableSensitiveDataLogging() 14 | .Options; 15 | } 16 | } -------------------------------------------------------------------------------- /src/Shared/Modular.Shared.Tests/Modular.Shared.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | false 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/Shared/Modular.Shared.Tests/OptionsHelper.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Configuration; 2 | 3 | namespace Modular.Shared.Tests 4 | { 5 | public static class OptionsHelper 6 | { 7 | private const string AppSettings = "appsettings.test.json"; 8 | 9 | public static TOptions GetOptions(string sectionName) where TOptions : class, new() 10 | { 11 | var options = new TOptions(); 12 | var configuration = GetConfigurationRoot(); 13 | var section = configuration.GetSection(sectionName); 14 | section.Bind(options); 15 | 16 | return options; 17 | } 18 | 19 | public static IConfigurationRoot GetConfigurationRoot() 20 | => new ConfigurationBuilder() 21 | .AddJsonFile(AppSettings) 22 | .AddEnvironmentVariables() 23 | .Build(); 24 | } 25 | } -------------------------------------------------------------------------------- /src/Shared/Modular.Shared.Tests/TestApplicationFactory.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Hosting; 2 | using Microsoft.AspNetCore.Mvc.Testing; 3 | using Modular.Bootstrapper; 4 | 5 | namespace Modular.Shared.Tests 6 | { 7 | public class TestApplicationFactory : WebApplicationFactory 8 | { 9 | protected override void ConfigureWebHost(IWebHostBuilder builder) 10 | { 11 | builder.UseEnvironment("test"); 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /src/Shared/Modular.Shared.Tests/WebApiTestBase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.Http; 3 | using System.Net.Http.Headers; 4 | using System.Text; 5 | using System.Text.Json; 6 | using System.Text.Json.Serialization; 7 | using System.Threading.Tasks; 8 | using Microsoft.AspNetCore.Hosting; 9 | using Microsoft.AspNetCore.Mvc.Testing; 10 | using Microsoft.Extensions.DependencyInjection; 11 | using Modular.Bootstrapper; 12 | using Xunit; 13 | 14 | namespace Modular.Shared.Tests 15 | { 16 | [Collection("tests")] 17 | public abstract class WebApiTestBase : IDisposable, IClassFixture> 18 | { 19 | private static readonly JsonSerializerOptions SerializerOptions = new() 20 | { 21 | PropertyNameCaseInsensitive = true, 22 | PropertyNamingPolicy = JsonNamingPolicy.CamelCase, 23 | Converters = {new JsonStringEnumConverter()} 24 | }; 25 | 26 | private string _route; 27 | 28 | protected void SetPath(string route) 29 | { 30 | if (string.IsNullOrWhiteSpace(route)) 31 | { 32 | _route = string.Empty; 33 | return; 34 | } 35 | 36 | if (route.StartsWith("/")) 37 | { 38 | route = route.Substring(1, route.Length - 1); 39 | } 40 | 41 | if (route.EndsWith("/")) 42 | { 43 | route = route.Substring(0, route.Length - 1); 44 | } 45 | 46 | _route = $"{route}/"; 47 | } 48 | 49 | protected static T Map(object data) => JsonSerializer.Deserialize(JsonSerializer.Serialize(data, SerializerOptions), SerializerOptions); 50 | 51 | protected Task GetAsync(string endpoint) 52 | => Client.GetAsync(GetEndpoint(endpoint)); 53 | 54 | protected async Task GetAsync(string endpoint) 55 | => await ReadAsync(await GetAsync(endpoint)); 56 | 57 | protected Task PostAsync(string endpoint, T command) 58 | => Client.PostAsync(GetEndpoint(endpoint), GetPayload(command)); 59 | 60 | protected Task PutAsync(string endpoint, T command) 61 | => Client.PutAsync(GetEndpoint(endpoint), GetPayload(command)); 62 | 63 | protected Task DeleteAsync(string endpoint) 64 | => Client.DeleteAsync(GetEndpoint(endpoint)); 65 | 66 | protected Task SendAsync(string method, string endpoint) 67 | => SendAsync(GetMethod(method), endpoint); 68 | 69 | protected Task SendAsync(HttpMethod method, string endpoint) 70 | => Client.SendAsync(new HttpRequestMessage(method, GetEndpoint(endpoint))); 71 | 72 | protected void Authenticate(Guid userId) 73 | { 74 | var jwt = AuthHelper.GenerateJwt(userId); 75 | Client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", jwt); 76 | } 77 | 78 | private static HttpMethod GetMethod(string method) 79 | => method.ToUpperInvariant() switch 80 | { 81 | "GET" => HttpMethod.Get, 82 | "POST" => HttpMethod.Post, 83 | "PUT" => HttpMethod.Put, 84 | "DELETE" => HttpMethod.Delete, 85 | _ => null 86 | }; 87 | 88 | private string GetEndpoint(string endpoint) => $"{_route}{endpoint}"; 89 | 90 | private static StringContent GetPayload(object value) 91 | => new(JsonSerializer.Serialize(value, SerializerOptions), Encoding.UTF8, "application/json"); 92 | 93 | protected static async Task ReadAsync(HttpResponseMessage response) 94 | => JsonSerializer.Deserialize(await response.Content.ReadAsStringAsync(), SerializerOptions); 95 | 96 | #region Arrange 97 | 98 | protected readonly HttpClient Client; 99 | private readonly WebApplicationFactory _factory; 100 | 101 | protected WebApiTestBase(WebApplicationFactory factory, string environment = "test") 102 | { 103 | _factory = factory.WithWebHostBuilder(builder => 104 | { 105 | builder.UseEnvironment(environment); 106 | builder.ConfigureServices(ConfigureServices); 107 | }); 108 | Client = _factory.CreateClient(); 109 | } 110 | 111 | protected virtual void ConfigureServices(IServiceCollection services) 112 | { 113 | } 114 | 115 | public virtual void Dispose() 116 | { 117 | } 118 | 119 | #endregion 120 | } 121 | } --------------------------------------------------------------------------------