├── .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 | 
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 | }
--------------------------------------------------------------------------------