├── .DS_Store ├── .dockerignore ├── .gitignore ├── IdentityService ├── .vscode │ ├── launch.json │ └── tasks.json ├── Api │ └── TestController.cs ├── Config.cs ├── DemoCorsPolicy.cs ├── DemoRedirectValidator.cs ├── IdentityServer4Demo.csproj ├── Program.cs ├── Properties │ └── launchSettings.json ├── Quickstart │ ├── Account │ │ ├── AccountController.cs │ │ ├── AccountOptions.cs │ │ ├── ExternalController.cs │ │ ├── ExternalProvider.cs │ │ ├── LoggedOutViewModel.cs │ │ ├── LoginInputModel.cs │ │ ├── LoginViewModel.cs │ │ ├── LogoutInputModel.cs │ │ ├── LogoutViewModel.cs │ │ └── RedirectViewModel.cs │ ├── Consent │ │ ├── ConsentController.cs │ │ ├── ConsentInputModel.cs │ │ ├── ConsentOptions.cs │ │ ├── ConsentViewModel.cs │ │ ├── ProcessConsentResult.cs │ │ └── ScopeViewModel.cs │ ├── Device │ │ ├── DeviceAuthorizationInputModel.cs │ │ ├── DeviceAuthorizationViewModel.cs │ │ └── DeviceController.cs │ ├── Diagnostics │ │ ├── DiagnosticsController.cs │ │ └── DiagnosticsViewModel.cs │ ├── Extensions.cs │ ├── Grants │ │ ├── GrantsController.cs │ │ └── GrantsViewModel.cs │ ├── Home │ │ ├── ErrorViewModel.cs │ │ └── HomeController.cs │ ├── SecurityHeadersAttribute.cs │ └── TestUsers.cs ├── SerilogMiddleware.cs ├── Startup.cs ├── Views │ ├── Account │ │ ├── LoggedOut.cshtml │ │ ├── Login.cshtml │ │ └── Logout.cshtml │ ├── Consent │ │ ├── Index.cshtml │ │ └── _ScopeListItem.cshtml │ ├── Device │ │ ├── Success.cshtml │ │ ├── UserCodeCapture.cshtml │ │ └── UserCodeConfirmation.cshtml │ ├── Diagnostics │ │ └── Index.cshtml │ ├── Grants │ │ └── Index.cshtml │ ├── Home │ │ └── Index.cshtml │ ├── Shared │ │ ├── Error.cshtml │ │ ├── Redirect.cshtml │ │ ├── _Layout.cshtml │ │ ├── _ScopeListItem.cshtml │ │ └── _ValidationSummary.cshtml │ ├── _ViewImports.cshtml │ └── _ViewStart.cshtml ├── appsettings.json ├── updateUI.ps1 ├── web.config └── wwwroot │ ├── css │ ├── site.css │ ├── site.less │ └── site.min.css │ ├── favicon.ico │ ├── icon.jpg │ ├── icon.png │ ├── js │ ├── signin-redirect.js │ └── signout-redirect.js │ └── lib │ ├── bootstrap │ ├── css │ │ ├── bootstrap.css │ │ ├── bootstrap.css.map │ │ └── bootstrap.min.css │ ├── fonts │ │ ├── glyphicons-halflings-regular.eot │ │ ├── glyphicons-halflings-regular.svg │ │ ├── glyphicons-halflings-regular.ttf │ │ ├── glyphicons-halflings-regular.woff │ │ └── glyphicons-halflings-regular.woff2 │ └── js │ │ ├── bootstrap.js │ │ └── bootstrap.min.js │ └── jquery │ ├── jquery.js │ ├── jquery.min.js │ └── jquery.min.map ├── LICENSE ├── OrdersService ├── .dockerignore ├── .vscode │ ├── launch.json │ └── tasks.json ├── AppSettings.cs ├── Authentication │ └── QueryStringAuthenticationMiddleware.cs ├── Controllers │ ├── HealthController.cs │ ├── OrderServiceException.cs │ └── OrdersController.cs ├── DTOs │ ├── Order.cs │ └── OrderItem.cs ├── Discovery │ ├── ConsulConfig.cs │ └── ConsulHostedService.cs ├── Dockerfile ├── Hubs │ └── OrdersHub.cs ├── Messages │ ├── NewOrderMessage.cs │ ├── Order.cs │ ├── OrderItem.cs │ └── ShippingCreatedMessage.cs ├── OrdersService.csproj ├── Program.cs ├── Properties │ └── launchSettings.json ├── Startup.cs ├── appsettings.Development.json ├── appsettings.json ├── docker-compose.override.yml └── docker-compose.yml ├── README.md ├── ShippingService ├── .deployment ├── DOCKERFILE ├── app │ ├── app.ts │ ├── config │ │ ├── config.ts │ │ └── settings.ts │ ├── controllers │ │ └── statusController.ts │ └── messages │ │ ├── newOrderMessage.ts │ │ ├── order.ts │ │ └── shippingCreatedMessage.ts ├── deploy.sh ├── package-lock.json ├── package.json ├── server.js └── tsconfig.json └── ShoppingClient ├── .editorconfig ├── LICENSE ├── README.md ├── angular.json ├── buildAssets ├── desktop │ ├── appMenu.js │ ├── icon.png │ ├── index.js │ └── package.json ├── mobile │ └── config.xml └── resources │ ├── icon.icns │ ├── icon.ico │ ├── icon.png │ └── splash.png ├── package-lock.json ├── package.json ├── src ├── assets │ ├── .gitkeep │ └── logo.svg ├── cordova.js ├── environments │ ├── environment.hmr.ts │ ├── environment.prod.ts │ └── environment.ts ├── favicon.ico ├── hmr.ts ├── index.html ├── main.ts ├── modules │ └── app │ │ ├── components │ │ ├── header │ │ │ ├── header.html │ │ │ ├── header.scss │ │ │ └── header.ts │ │ ├── home │ │ │ ├── home.html │ │ │ ├── home.scss │ │ │ └── home.ts │ │ ├── list │ │ │ ├── orderList.html │ │ │ └── orderList.ts │ │ ├── login │ │ │ ├── login.html │ │ │ ├── login.scss │ │ │ └── login.ts │ │ ├── menu │ │ │ ├── menu.html │ │ │ ├── menu.scss │ │ │ └── menu.ts │ │ └── root │ │ │ ├── root.html │ │ │ ├── root.scss │ │ │ └── root.ts │ │ ├── guards │ │ └── isAuthenticated.ts │ │ ├── module.ts │ │ ├── routes.ts │ │ └── services │ │ ├── authInterceptor.ts │ │ ├── desktopIntegrationService.ts │ │ ├── ordersService.ts │ │ ├── platformService.ts │ │ ├── pushService.ts │ │ └── windowRef.ts ├── polyfills.ts ├── styles │ ├── _backdrop.scss │ ├── _links.scss │ ├── _loader.scss │ ├── _menu.scss │ ├── _table.scss │ ├── _utilities.scss │ ├── _variables.scss │ └── global.scss └── tsconfig.app.json ├── tsconfig.json └── tslint.json /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thinktecture/pragmatic-microservices-dotnetcore/881e43b4afd9ab17822238d1853ecf4d11567276/.DS_Store -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | **/bin/ 2 | **/obj/ 3 | **/*.csproj.user -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | x64/ 19 | x86/ 20 | bld/ 21 | [Bb]in/ 22 | [Oo]bj/ 23 | [Ll]og/ 24 | 25 | # Visual Studio 2015 cache/options directory 26 | .vs/ 27 | # Uncomment if you have tasks that create the project's static files in wwwroot 28 | #wwwroot/ 29 | 30 | # MSTest test Results 31 | [Tt]est[Rr]esult*/ 32 | [Bb]uild[Ll]og.* 33 | 34 | # NUNIT 35 | *.VisualState.xml 36 | TestResult.xml 37 | 38 | # Build Results of an ATL Project 39 | [Dd]ebugPS/ 40 | [Rr]eleasePS/ 41 | dlldata.c 42 | 43 | # DNX 44 | project.lock.json 45 | artifacts/ 46 | 47 | *_i.c 48 | *_p.c 49 | *_i.h 50 | *.ilk 51 | *.meta 52 | *.obj 53 | *.pch 54 | *.pdb 55 | *.pgc 56 | *.pgd 57 | *.rsp 58 | *.sbr 59 | *.tlb 60 | *.tli 61 | *.tlh 62 | *.tmp 63 | *.tmp_proj 64 | *.log 65 | *.vspscc 66 | *.vssscc 67 | .builds 68 | *.pidb 69 | *.svclog 70 | *.scc 71 | 72 | # Chutzpah Test files 73 | _Chutzpah* 74 | 75 | # Visual C++ cache files 76 | ipch/ 77 | *.aps 78 | *.ncb 79 | *.opendb 80 | *.opensdf 81 | *.sdf 82 | *.cachefile 83 | *.VC.db 84 | *.VC.VC.opendb 85 | 86 | # Visual Studio profiler 87 | *.psess 88 | *.vsp 89 | *.vspx 90 | *.sap 91 | 92 | # TFS 2012 Local Workspace 93 | $tf/ 94 | 95 | # Guidance Automation Toolkit 96 | *.gpState 97 | 98 | # ReSharper is a .NET coding add-in 99 | _ReSharper*/ 100 | *.[Rr]e[Ss]harper 101 | *.DotSettings.user 102 | 103 | # JustCode is a .NET coding add-in 104 | .JustCode 105 | 106 | # TeamCity is a build add-in 107 | _TeamCity* 108 | 109 | # DotCover is a Code Coverage Tool 110 | *.dotCover 111 | 112 | # NCrunch 113 | _NCrunch_* 114 | .*crunch*.local.xml 115 | nCrunchTemp_* 116 | 117 | # MightyMoose 118 | *.mm.* 119 | AutoTest.Net/ 120 | 121 | # Web workbench (sass) 122 | .sass-cache/ 123 | 124 | # Installshield output folder 125 | [Ee]xpress/ 126 | 127 | # DocProject is a documentation generator add-in 128 | DocProject/buildhelp/ 129 | DocProject/Help/*.HxT 130 | DocProject/Help/*.HxC 131 | DocProject/Help/*.hhc 132 | DocProject/Help/*.hhk 133 | DocProject/Help/*.hhp 134 | DocProject/Help/Html2 135 | DocProject/Help/html 136 | 137 | # Click-Once directory 138 | publish/ 139 | 140 | # Publish Web Output 141 | *.[Pp]ublish.xml 142 | *.azurePubxml 143 | # TODO: Comment the next line if you want to checkin your web deploy settings 144 | # but database connection strings (with potential passwords) will be unencrypted 145 | *.pubxml 146 | *.publishproj 147 | 148 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 149 | # checkin your Azure Web App publish settings, but sensitive information contained 150 | # in these scripts will be unencrypted 151 | PublishScripts/ 152 | 153 | # NuGet Packages 154 | *.nupkg 155 | # The packages folder can be ignored because of Package Restore 156 | **/packages/* 157 | # except build/, which is used as an MSBuild target. 158 | !**/packages/build/ 159 | # Uncomment if necessary however generally it will be regenerated when needed 160 | #!**/packages/repositories.config 161 | # NuGet v3's project.json files produces more ignoreable files 162 | *.nuget.props 163 | *.nuget.targets 164 | 165 | # Microsoft Azure Build Output 166 | csx/ 167 | *.build.csdef 168 | 169 | # Microsoft Azure Emulator 170 | ecf/ 171 | rcf/ 172 | 173 | # Windows Store app package directories and files 174 | AppPackages/ 175 | BundleArtifacts/ 176 | Package.StoreAssociation.xml 177 | _pkginfo.txt 178 | 179 | # Visual Studio cache files 180 | # files ending in .cache can be ignored 181 | *.[Cc]ache 182 | # but keep track of directories ending in .cache 183 | !*.[Cc]ache/ 184 | 185 | # Others 186 | ClientBin/ 187 | ~$* 188 | *~ 189 | *.dbmdl 190 | *.dbproj.schemaview 191 | *.pfx 192 | *.publishsettings 193 | node_modules/ 194 | orleans.codegen.cs 195 | 196 | # Since there are multiple workflows, uncomment next line to ignore bower_components 197 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 198 | #bower_components/ 199 | 200 | # RIA/Silverlight projects 201 | Generated_Code/ 202 | 203 | # Backup & report files from converting an old project file 204 | # to a newer Visual Studio version. Backup files are not needed, 205 | # because we have git ;-) 206 | _UpgradeReport_Files/ 207 | Backup*/ 208 | UpgradeLog*.XML 209 | UpgradeLog*.htm 210 | 211 | # SQL Server files 212 | *.mdf 213 | *.ldf 214 | 215 | # Business Intelligence projects 216 | *.rdl.data 217 | *.bim.layout 218 | *.bim_*.settings 219 | 220 | # Microsoft Fakes 221 | FakesAssemblies/ 222 | 223 | # GhostDoc plugin setting file 224 | *.GhostDoc.xml 225 | 226 | # Node.js Tools for Visual Studio 227 | .ntvs_analysis.dat 228 | 229 | # Visual Studio 6 build log 230 | *.plg 231 | 232 | # Visual Studio 6 workspace options file 233 | *.opt 234 | 235 | # Visual Studio LightSwitch build output 236 | **/*.HTMLClient/GeneratedArtifacts 237 | **/*.DesktopClient/GeneratedArtifacts 238 | **/*.DesktopClient/ModelManifest.xml 239 | **/*.Server/GeneratedArtifacts 240 | **/*.Server/ModelManifest.xml 241 | _Pvt_Extensions 242 | 243 | # Paket dependency manager 244 | .paket/paket.exe 245 | paket-files/ 246 | 247 | # FAKE - F# Make 248 | .fake/ 249 | 250 | # JetBrains Rider 251 | .idea/ 252 | *.sln.iml 253 | dist/ 254 | IdentityServerWithAspNetIdentity/out/ 255 | MyOrdersAppService/MyOrdersAppService.NetCore/client/ 256 | IdentityServer/out/ 257 | OrdersClientService/MyOrdersAppService.NetCore/client/ 258 | /IdentityServer/IdentityServerWithAspNetIdentity/identityserver4_log.txt 259 | /OrdersService/.vscode/solution-explorer 260 | -------------------------------------------------------------------------------- /IdentityService/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to find out which attributes exist for C# debugging 3 | // Use hover for the description of the existing attributes 4 | // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": ".NET Core Launch (web)", 9 | "type": "coreclr", 10 | "request": "launch", 11 | "preLaunchTask": "build", 12 | // If you have changed target frameworks, make sure to update the program path. 13 | "program": "${workspaceFolder}/bin/Debug/netcoreapp2.2/IdentityServer4Demo.dll", 14 | "args": [], 15 | "cwd": "${workspaceFolder}", 16 | "stopAtEntry": false, 17 | // Enable launching a web browser when ASP.NET Core starts. For more information: https://aka.ms/VSCode-CS-LaunchJson-WebBrowser 18 | "serverReadyAction": { 19 | "action": "openExternally", 20 | "pattern": "^\\s*Now listening on:\\s+(https?://\\S+)" 21 | }, 22 | "env": { 23 | "ASPNETCORE_ENVIRONMENT": "Development" 24 | }, 25 | "sourceFileMap": { 26 | "/Views": "${workspaceFolder}/Views" 27 | } 28 | }, 29 | { 30 | "name": ".NET Core Attach", 31 | "type": "coreclr", 32 | "request": "attach", 33 | "processId": "${command:pickProcess}" 34 | } 35 | ] 36 | } -------------------------------------------------------------------------------- /IdentityService/.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "build", 6 | "command": "dotnet", 7 | "type": "process", 8 | "args": [ 9 | "build", 10 | "${workspaceFolder}/IdentityServer4Demo.csproj", 11 | "/property:GenerateFullPaths=true", 12 | "/consoleloggerparameters:NoSummary" 13 | ], 14 | "problemMatcher": "$msCompile" 15 | }, 16 | { 17 | "label": "publish", 18 | "command": "dotnet", 19 | "type": "process", 20 | "args": [ 21 | "publish", 22 | "${workspaceFolder}/IdentityServer4Demo.csproj", 23 | "/property:GenerateFullPaths=true", 24 | "/consoleloggerparameters:NoSummary" 25 | ], 26 | "problemMatcher": "$msCompile" 27 | }, 28 | { 29 | "label": "watch", 30 | "command": "dotnet", 31 | "type": "process", 32 | "args": [ 33 | "watch", 34 | "run", 35 | "${workspaceFolder}/IdentityServer4Demo.csproj", 36 | "/property:GenerateFullPaths=true", 37 | "/consoleloggerparameters:NoSummary" 38 | ], 39 | "problemMatcher": "$msCompile" 40 | } 41 | ] 42 | } -------------------------------------------------------------------------------- /IdentityService/Api/TestController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authorization; 2 | using Microsoft.AspNetCore.Mvc; 3 | using System.Linq; 4 | using IdentityServer4.AccessTokenValidation; 5 | 6 | namespace IdentityServer4Demo.Api 7 | { 8 | [Route("/api/test")] 9 | [Authorize(AuthenticationSchemes = IdentityServerAuthenticationDefaults.AuthenticationScheme)] 10 | public class TestController : ControllerBase 11 | { 12 | public IActionResult Get() 13 | { 14 | var claims = User.Claims.Select(c => new { c.Type, c.Value }); 15 | return new JsonResult(claims); 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /IdentityService/Config.cs: -------------------------------------------------------------------------------- 1 | using IdentityServer4.Models; 2 | using System.Collections.Generic; 3 | 4 | namespace IdentityServer4Demo 5 | { 6 | public class Config 7 | { 8 | public static IEnumerable GetIdentityResources() 9 | { 10 | return new List 11 | { 12 | new IdentityResources.OpenId(), 13 | new IdentityResources.Profile(), 14 | new IdentityResources.Email(), 15 | }; 16 | } 17 | 18 | public static IEnumerable GetApis() 19 | { 20 | return new List 21 | { 22 | new ApiResource("api", "Demo API") 23 | }; 24 | } 25 | 26 | public static IEnumerable GetClients() 27 | { 28 | return new List 29 | { 30 | // resource owner password grant client 31 | new Client 32 | { 33 | ClientId = "resourceowner", 34 | AllowedGrantTypes = GrantTypes.ResourceOwnerPassword, 35 | AllowOfflineAccess = true, 36 | ClientSecrets = 37 | { 38 | new Secret("no-really-a-secret".Sha256()) 39 | }, 40 | AllowedScopes = 41 | { 42 | "api", 43 | "openid", 44 | "profile", 45 | "email" 46 | }, 47 | AllowedCorsOrigins = 48 | { 49 | "http://localhost:4200", 50 | "http://localhost:4500" 51 | } 52 | }, 53 | // native clients 54 | new Client 55 | { 56 | ClientId = "native.hybrid", 57 | ClientName = "Native Client (Hybrid with PKCE)", 58 | 59 | RedirectUris = { "https://notused" }, 60 | PostLogoutRedirectUris = { "https://notused" }, 61 | 62 | RequireClientSecret = false, 63 | 64 | AllowedGrantTypes = GrantTypes.Hybrid, 65 | RequirePkce = true, 66 | AllowedScopes = { "openid", "profile", "email", "api" }, 67 | 68 | AllowOfflineAccess = true, 69 | RefreshTokenUsage = TokenUsage.ReUse 70 | }, 71 | new Client 72 | { 73 | ClientId = "server.hybrid", 74 | ClientName = "Server-based Client (Hybrid)", 75 | 76 | RedirectUris = { "https://notused" }, 77 | PostLogoutRedirectUris = { "https://notused" }, 78 | 79 | ClientSecrets = { new Secret("secret".Sha256()) }, 80 | 81 | AllowedGrantTypes = GrantTypes.Hybrid, 82 | AllowedScopes = { "openid", "profile", "email", "api" }, 83 | 84 | AllowOfflineAccess = true, 85 | RefreshTokenUsage = TokenUsage.ReUse 86 | }, 87 | new Client 88 | { 89 | ClientId = "server.hybrid.short", 90 | ClientName = "Server-based Client (Hybrid)", 91 | 92 | RedirectUris = { "https://notused" }, 93 | PostLogoutRedirectUris = { "https://notused" }, 94 | 95 | ClientSecrets = { new Secret("secret".Sha256()) }, 96 | 97 | AllowedGrantTypes = GrantTypes.Hybrid, 98 | AllowedScopes = { "openid", "profile", "email", "api" }, 99 | 100 | AllowOfflineAccess = true, 101 | RefreshTokenUsage = TokenUsage.ReUse, 102 | AccessTokenLifetime = 70, 103 | }, 104 | new Client 105 | { 106 | ClientId = "native.code", 107 | ClientName = "Native Client (Code with PKCE)", 108 | 109 | RedirectUris = { "https://notused" }, 110 | PostLogoutRedirectUris = { "https://notused" }, 111 | 112 | RequireClientSecret = false, 113 | 114 | AllowedGrantTypes = GrantTypes.Code, 115 | RequirePkce = true, 116 | AllowedScopes = { "openid", "profile", "email", "api" }, 117 | 118 | AllowOfflineAccess = true, 119 | RefreshTokenUsage = TokenUsage.ReUse 120 | }, 121 | new Client 122 | { 123 | ClientId = "server.code", 124 | ClientName = "Service Client (Code)", 125 | 126 | RedirectUris = { "https://notused" }, 127 | PostLogoutRedirectUris = { "https://notused" }, 128 | 129 | ClientSecrets = { new Secret("secret".Sha256()) }, 130 | 131 | AllowedGrantTypes = GrantTypes.Code, 132 | AllowedScopes = { "openid", "profile", "email", "api" }, 133 | 134 | AllowOfflineAccess = true, 135 | RefreshTokenUsage = TokenUsage.ReUse 136 | }, 137 | 138 | // server to server 139 | new Client 140 | { 141 | ClientId = "client", 142 | ClientSecrets = { new Secret("secret".Sha256()) }, 143 | 144 | AllowedGrantTypes = GrantTypes.ClientCredentials, 145 | AllowedScopes = { "api" }, 146 | }, 147 | 148 | // SPA per new security guidance 149 | new Client 150 | { 151 | ClientId = "spa", 152 | ClientName = "SPA (Code + PKCE)", 153 | 154 | RequireClientSecret = false, 155 | 156 | RedirectUris = { "https://notused" }, 157 | PostLogoutRedirectUris = { "https://notused" }, 158 | 159 | AllowedGrantTypes = GrantTypes.Code, 160 | AllowedScopes = { "openid", "profile", "email", "api" }, 161 | 162 | AllowOfflineAccess = true, 163 | RefreshTokenUsage = TokenUsage.ReUse 164 | }, 165 | new Client 166 | { 167 | ClientId = "spa.short", 168 | ClientName = "SPA (Code + PKCE)", 169 | 170 | RequireClientSecret = false, 171 | 172 | RedirectUris = { "https://notused" }, 173 | PostLogoutRedirectUris = { "https://notused" }, 174 | 175 | AllowedGrantTypes = GrantTypes.Code, 176 | AllowedScopes = { "openid", "profile", "email", "api" }, 177 | 178 | AllowOfflineAccess = true, 179 | RefreshTokenUsage = TokenUsage.OneTimeOnly, 180 | AccessTokenLifetime = 70 181 | }, 182 | 183 | // implicit (e.g. SPA or OIDC authentication) 184 | new Client 185 | { 186 | ClientId = "implicit", 187 | ClientName = "Implicit Client", 188 | AllowAccessTokensViaBrowser = true, 189 | 190 | RedirectUris = { "https://notused" }, 191 | PostLogoutRedirectUris = { "https://notused" }, 192 | FrontChannelLogoutUri = "http://localhost:5000/signout-idsrv", // for testing identityserver on localhost 193 | 194 | AllowedGrantTypes = GrantTypes.Implicit, 195 | AllowedScopes = { "openid", "profile", "email", "api" }, 196 | }, 197 | 198 | // implicit using reference tokens (e.g. SPA or OIDC authentication) 199 | new Client 200 | { 201 | ClientId = "implicit.reference", 202 | ClientName = "Implicit Client using reference tokens", 203 | AllowAccessTokensViaBrowser = true, 204 | 205 | AccessTokenType = AccessTokenType.Reference, 206 | 207 | RedirectUris = { "https://notused" }, 208 | PostLogoutRedirectUris = { "https://notused" }, 209 | 210 | AllowedGrantTypes = GrantTypes.Implicit, 211 | AllowedScopes = { "openid", "profile", "email", "api" }, 212 | }, 213 | 214 | // implicit using reference tokens (e.g. SPA or OIDC authentication) 215 | new Client 216 | { 217 | ClientId = "implicit.shortlived", 218 | ClientName = "Implicit Client using short-lived tokens", 219 | AllowAccessTokensViaBrowser = true, 220 | 221 | AccessTokenLifetime = 70, 222 | 223 | RedirectUris = { "https://notused" }, 224 | PostLogoutRedirectUris = { "https://notused" }, 225 | 226 | AllowedGrantTypes = GrantTypes.Implicit, 227 | AllowedScopes = { "openid", "profile", "email", "api" }, 228 | }, 229 | // device flow 230 | new Client 231 | { 232 | ClientId = "device", 233 | ClientName = "Device Flow Client", 234 | 235 | AllowedGrantTypes = GrantTypes.DeviceFlow, 236 | RequireClientSecret = false, 237 | 238 | AllowOfflineAccess = true, 239 | AllowedScopes = { "openid", "profile", "email", "api" } 240 | } 241 | }; 242 | } 243 | } 244 | } 245 | -------------------------------------------------------------------------------- /IdentityService/DemoCorsPolicy.cs: -------------------------------------------------------------------------------- 1 | using IdentityServer4.Services; 2 | using System.Threading.Tasks; 3 | 4 | namespace IdentityServer4Demo 5 | { 6 | // allows arbitrary CORS origins - only for demo purposes. NEVER USE IN PRODUCTION 7 | public class DemoCorsPolicy : ICorsPolicyService 8 | { 9 | public Task IsOriginAllowedAsync(string origin) 10 | { 11 | return Task.FromResult(true); 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /IdentityService/DemoRedirectValidator.cs: -------------------------------------------------------------------------------- 1 | using IdentityServer4.Validation; 2 | using System.Threading.Tasks; 3 | using IdentityServer4.Models; 4 | 5 | namespace IdentityServer4Demo 6 | { 7 | // allows arbitrary redirect URIs - only for demo purposes. NEVER USE IN PRODUCTION 8 | public class DemoRedirectValidator : IRedirectUriValidator 9 | { 10 | public Task IsPostLogoutRedirectUriValidAsync(string requestedUri, Client client) 11 | { 12 | return Task.FromResult(true); 13 | } 14 | 15 | public Task IsRedirectUriValidAsync(string requestedUri, Client client) 16 | { 17 | return Task.FromResult(true); 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /IdentityService/IdentityServer4Demo.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netcoreapp2.2 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /IdentityService/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore; 2 | using Microsoft.AspNetCore.Hosting; 3 | using Microsoft.Azure.KeyVault; 4 | using Microsoft.Azure.Services.AppAuthentication; 5 | using Microsoft.Extensions.Configuration; 6 | using Microsoft.Extensions.Configuration.AzureKeyVault; 7 | using Serilog; 8 | using Serilog.Events; 9 | using System; 10 | 11 | namespace IdentityServer4Demo 12 | { 13 | public class Program 14 | { 15 | public static void Main(string[] args) 16 | { 17 | Console.Title = "IdentityServer"; 18 | 19 | BuildWebHostBuilder(args).Build().Run(); 20 | } 21 | 22 | public static IWebHostBuilder BuildWebHostBuilder(string[] args) 23 | { 24 | return WebHost.CreateDefaultBuilder(args) 25 | .UseStartup() 26 | .ConfigureAppConfiguration((ctx, builder) => 27 | { 28 | //var config = builder.Build(); 29 | //var tokenProvider = new AzureServiceTokenProvider(); 30 | //var kvClient = new KeyVaultClient((authority, resource, scope) => tokenProvider.KeyVaultTokenCallback(authority, resource, scope)); 31 | 32 | //builder.AddAzureKeyVault(config["KeyVault:BaseUrl"], kvClient, new DefaultKeyVaultSecretManager()); 33 | }) 34 | .UseSerilog((ctx, config) => 35 | { 36 | config.MinimumLevel.Debug() 37 | .MinimumLevel.Debug() 38 | .MinimumLevel.Override("Microsoft", LogEventLevel.Warning) 39 | .MinimumLevel.Override("System", LogEventLevel.Warning) 40 | .MinimumLevel.Override("Microsoft.AspNetCore.Authentication", LogEventLevel.Information) 41 | .Enrich.FromLogContext(); 42 | 43 | if (ctx.HostingEnvironment.IsDevelopment()) 44 | { 45 | config.WriteTo.Console(outputTemplate: "[{Timestamp:HH:mm:ss} {Level}] {SourceContext}{NewLine}{Message:lj}{NewLine}{Exception}{NewLine}"); 46 | } 47 | else if (ctx.HostingEnvironment.IsProduction()) 48 | { 49 | config.WriteTo.File(@"D:\home\LogFiles\Application\identityserver.txt", 50 | fileSizeLimitBytes: 1_000_000, 51 | rollOnFileSizeLimit: true, 52 | shared: true, 53 | flushToDiskInterval: TimeSpan.FromSeconds(1)); 54 | } 55 | }); 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /IdentityService/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:24997/", 7 | "sslPort": 0 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": true, 14 | "environmentVariables": { 15 | "ASPNETCORE_ENVIRONMENT": "Development" 16 | } 17 | }, 18 | "IdentityServer4Demo": { 19 | "commandName": "Project", 20 | "launchUrl": "http://localhost:5000", 21 | "environmentVariables": { 22 | "ASPNETCORE_ENVIRONMENT": "Development" 23 | } 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /IdentityService/Quickstart/Account/AccountOptions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. 3 | 4 | 5 | using System; 6 | 7 | namespace IdentityServer4.Quickstart.UI 8 | { 9 | public class AccountOptions 10 | { 11 | public static bool AllowLocalLogin = true; 12 | public static bool AllowRememberLogin = true; 13 | public static TimeSpan RememberMeLoginDuration = TimeSpan.FromDays(30); 14 | 15 | public static bool ShowLogoutPrompt = true; 16 | public static bool AutomaticRedirectAfterSignOut = false; 17 | 18 | // specify the Windows authentication scheme being used 19 | public static readonly string WindowsAuthenticationSchemeName = Microsoft.AspNetCore.Server.IISIntegration.IISDefaults.AuthenticationScheme; 20 | // if user uses windows auth, should we load the groups from windows 21 | public static bool IncludeWindowsGroups = false; 22 | 23 | public static string InvalidCredentialsErrorMessage = "Invalid username or password"; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /IdentityService/Quickstart/Account/ExternalProvider.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. 3 | 4 | 5 | namespace IdentityServer4.Quickstart.UI 6 | { 7 | public class ExternalProvider 8 | { 9 | public string DisplayName { get; set; } 10 | public string AuthenticationScheme { get; set; } 11 | } 12 | } -------------------------------------------------------------------------------- /IdentityService/Quickstart/Account/LoggedOutViewModel.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. 3 | 4 | 5 | namespace IdentityServer4.Quickstart.UI 6 | { 7 | public class LoggedOutViewModel 8 | { 9 | public string PostLogoutRedirectUri { get; set; } 10 | public string ClientName { get; set; } 11 | public string SignOutIframeUrl { get; set; } 12 | 13 | public bool AutomaticRedirectAfterSignOut { get; set; } = false; 14 | 15 | public string LogoutId { get; set; } 16 | public bool TriggerExternalSignout => ExternalAuthenticationScheme != null; 17 | public string ExternalAuthenticationScheme { get; set; } 18 | } 19 | } -------------------------------------------------------------------------------- /IdentityService/Quickstart/Account/LoginInputModel.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. 3 | 4 | 5 | using System.ComponentModel.DataAnnotations; 6 | 7 | namespace IdentityServer4.Quickstart.UI 8 | { 9 | public class LoginInputModel 10 | { 11 | [Required] 12 | public string Username { get; set; } 13 | [Required] 14 | public string Password { get; set; } 15 | public bool RememberLogin { get; set; } 16 | public string ReturnUrl { get; set; } 17 | } 18 | } -------------------------------------------------------------------------------- /IdentityService/Quickstart/Account/LoginViewModel.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. 3 | 4 | 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | 9 | namespace IdentityServer4.Quickstart.UI 10 | { 11 | public class LoginViewModel : LoginInputModel 12 | { 13 | public bool AllowRememberLogin { get; set; } = true; 14 | public bool EnableLocalLogin { get; set; } = true; 15 | 16 | public IEnumerable ExternalProviders { get; set; } = Enumerable.Empty(); 17 | public IEnumerable VisibleExternalProviders => ExternalProviders.Where(x => !String.IsNullOrWhiteSpace(x.DisplayName)); 18 | 19 | public bool IsExternalLoginOnly => EnableLocalLogin == false && ExternalProviders?.Count() == 1; 20 | public string ExternalLoginScheme => IsExternalLoginOnly ? ExternalProviders?.SingleOrDefault()?.AuthenticationScheme : null; 21 | } 22 | } -------------------------------------------------------------------------------- /IdentityService/Quickstart/Account/LogoutInputModel.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. 3 | 4 | 5 | namespace IdentityServer4.Quickstart.UI 6 | { 7 | public class LogoutInputModel 8 | { 9 | public string LogoutId { get; set; } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /IdentityService/Quickstart/Account/LogoutViewModel.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. 3 | 4 | 5 | namespace IdentityServer4.Quickstart.UI 6 | { 7 | public class LogoutViewModel : LogoutInputModel 8 | { 9 | public bool ShowLogoutPrompt { get; set; } = true; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /IdentityService/Quickstart/Account/RedirectViewModel.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. 3 | 4 | 5 | 6 | namespace IdentityServer4.Quickstart.UI 7 | { 8 | public class RedirectViewModel 9 | { 10 | public string RedirectUrl { get; set; } 11 | } 12 | } -------------------------------------------------------------------------------- /IdentityService/Quickstart/Consent/ConsentInputModel.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. 3 | 4 | 5 | using System.Collections.Generic; 6 | 7 | namespace IdentityServer4.Quickstart.UI 8 | { 9 | public class ConsentInputModel 10 | { 11 | public string Button { get; set; } 12 | public IEnumerable ScopesConsented { get; set; } 13 | public bool RememberConsent { get; set; } 14 | public string ReturnUrl { get; set; } 15 | } 16 | } -------------------------------------------------------------------------------- /IdentityService/Quickstart/Consent/ConsentOptions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. 3 | 4 | 5 | namespace IdentityServer4.Quickstart.UI 6 | { 7 | public class ConsentOptions 8 | { 9 | public static bool EnableOfflineAccess = true; 10 | public static string OfflineAccessDisplayName = "Offline Access"; 11 | public static string OfflineAccessDescription = "Access to your applications and resources, even when you are offline"; 12 | 13 | public static readonly string MustChooseOneErrorMessage = "You must pick at least one permission"; 14 | public static readonly string InvalidSelectionErrorMessage = "Invalid selection"; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /IdentityService/Quickstart/Consent/ConsentViewModel.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. 3 | 4 | 5 | using System.Collections.Generic; 6 | 7 | namespace IdentityServer4.Quickstart.UI 8 | { 9 | public class ConsentViewModel : ConsentInputModel 10 | { 11 | public string ClientName { get; set; } 12 | public string ClientUrl { get; set; } 13 | public string ClientLogoUrl { get; set; } 14 | public bool AllowRememberConsent { get; set; } 15 | 16 | public IEnumerable IdentityScopes { get; set; } 17 | public IEnumerable ResourceScopes { get; set; } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /IdentityService/Quickstart/Consent/ProcessConsentResult.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. 3 | 4 | 5 | namespace IdentityServer4.Quickstart.UI 6 | { 7 | public class ProcessConsentResult 8 | { 9 | public bool IsRedirect => RedirectUri != null; 10 | public string RedirectUri { get; set; } 11 | public string ClientId { get; set; } 12 | 13 | public bool ShowView => ViewModel != null; 14 | public ConsentViewModel ViewModel { get; set; } 15 | 16 | public bool HasValidationError => ValidationError != null; 17 | public string ValidationError { get; set; } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /IdentityService/Quickstart/Consent/ScopeViewModel.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. 3 | 4 | 5 | namespace IdentityServer4.Quickstart.UI 6 | { 7 | public class ScopeViewModel 8 | { 9 | public string Name { get; set; } 10 | public string DisplayName { get; set; } 11 | public string Description { get; set; } 12 | public bool Emphasize { get; set; } 13 | public bool Required { get; set; } 14 | public bool Checked { get; set; } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /IdentityService/Quickstart/Device/DeviceAuthorizationInputModel.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. 3 | 4 | 5 | namespace IdentityServer4.Quickstart.UI.Device 6 | { 7 | public class DeviceAuthorizationInputModel : ConsentInputModel 8 | { 9 | public string UserCode { get; set; } 10 | } 11 | } -------------------------------------------------------------------------------- /IdentityService/Quickstart/Device/DeviceAuthorizationViewModel.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. 3 | 4 | 5 | namespace IdentityServer4.Quickstart.UI.Device 6 | { 7 | public class DeviceAuthorizationViewModel : ConsentViewModel 8 | { 9 | public string UserCode { get; set; } 10 | public bool ConfirmUserCode { get; set; } 11 | } 12 | } -------------------------------------------------------------------------------- /IdentityService/Quickstart/Device/DeviceController.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. 3 | 4 | 5 | using System; 6 | using System.Linq; 7 | using System.Threading.Tasks; 8 | using IdentityServer4.Events; 9 | using IdentityServer4.Extensions; 10 | using IdentityServer4.Models; 11 | using IdentityServer4.Services; 12 | using IdentityServer4.Stores; 13 | using Microsoft.AspNetCore.Authorization; 14 | using Microsoft.AspNetCore.Mvc; 15 | using Microsoft.Extensions.Logging; 16 | 17 | namespace IdentityServer4.Quickstart.UI.Device 18 | { 19 | [Authorize] 20 | [SecurityHeaders] 21 | public class DeviceController : Controller 22 | { 23 | private readonly IDeviceFlowInteractionService _interaction; 24 | private readonly IClientStore _clientStore; 25 | private readonly IResourceStore _resourceStore; 26 | private readonly IEventService _events; 27 | private readonly ILogger _logger; 28 | 29 | public DeviceController( 30 | IDeviceFlowInteractionService interaction, 31 | IClientStore clientStore, 32 | IResourceStore resourceStore, 33 | IEventService eventService, 34 | ILogger logger) 35 | { 36 | _interaction = interaction; 37 | _clientStore = clientStore; 38 | _resourceStore = resourceStore; 39 | _events = eventService; 40 | _logger = logger; 41 | } 42 | 43 | [HttpGet] 44 | public async Task Index([FromQuery(Name = "user_code")] string userCode) 45 | { 46 | if (string.IsNullOrWhiteSpace(userCode)) return View("UserCodeCapture"); 47 | 48 | var vm = await BuildViewModelAsync(userCode); 49 | if (vm == null) return View("Error"); 50 | 51 | vm.ConfirmUserCode = true; 52 | return View("UserCodeConfirmation", vm); 53 | } 54 | 55 | [HttpPost] 56 | [ValidateAntiForgeryToken] 57 | public async Task UserCodeCapture(string userCode) 58 | { 59 | var vm = await BuildViewModelAsync(userCode); 60 | if (vm == null) return View("Error"); 61 | 62 | return View("UserCodeConfirmation", vm); 63 | } 64 | 65 | [HttpPost] 66 | [ValidateAntiForgeryToken] 67 | public async Task Callback(DeviceAuthorizationInputModel model) 68 | { 69 | if (model == null) throw new ArgumentNullException(nameof(model)); 70 | 71 | var result = await ProcessConsent(model); 72 | if (result.HasValidationError) return View("Error"); 73 | 74 | return View("Success"); 75 | } 76 | 77 | private async Task ProcessConsent(DeviceAuthorizationInputModel model) 78 | { 79 | var result = new ProcessConsentResult(); 80 | 81 | var request = await _interaction.GetAuthorizationContextAsync(model.UserCode); 82 | if (request == null) return result; 83 | 84 | ConsentResponse grantedConsent = null; 85 | 86 | // user clicked 'no' - send back the standard 'access_denied' response 87 | if (model.Button == "no") 88 | { 89 | grantedConsent = ConsentResponse.Denied; 90 | 91 | // emit event 92 | await _events.RaiseAsync(new ConsentDeniedEvent(User.GetSubjectId(), request.ClientId, request.ScopesRequested)); 93 | } 94 | // user clicked 'yes' - validate the data 95 | else if (model.Button == "yes") 96 | { 97 | // if the user consented to some scope, build the response model 98 | if (model.ScopesConsented != null && model.ScopesConsented.Any()) 99 | { 100 | var scopes = model.ScopesConsented; 101 | if (ConsentOptions.EnableOfflineAccess == false) 102 | { 103 | scopes = scopes.Where(x => x != IdentityServerConstants.StandardScopes.OfflineAccess); 104 | } 105 | 106 | grantedConsent = new ConsentResponse 107 | { 108 | RememberConsent = model.RememberConsent, 109 | ScopesConsented = scopes.ToArray() 110 | }; 111 | 112 | // emit event 113 | await _events.RaiseAsync(new ConsentGrantedEvent(User.GetSubjectId(), request.ClientId, request.ScopesRequested, grantedConsent.ScopesConsented, grantedConsent.RememberConsent)); 114 | } 115 | else 116 | { 117 | result.ValidationError = ConsentOptions.MustChooseOneErrorMessage; 118 | } 119 | } 120 | else 121 | { 122 | result.ValidationError = ConsentOptions.InvalidSelectionErrorMessage; 123 | } 124 | 125 | if (grantedConsent != null) 126 | { 127 | // communicate outcome of consent back to identityserver 128 | await _interaction.HandleRequestAsync(model.UserCode, grantedConsent); 129 | 130 | // indicate that's it ok to redirect back to authorization endpoint 131 | result.RedirectUri = model.ReturnUrl; 132 | result.ClientId = request.ClientId; 133 | } 134 | else 135 | { 136 | // we need to redisplay the consent UI 137 | result.ViewModel = await BuildViewModelAsync(model.UserCode, model); 138 | } 139 | 140 | return result; 141 | } 142 | 143 | private async Task BuildViewModelAsync(string userCode, DeviceAuthorizationInputModel model = null) 144 | { 145 | var request = await _interaction.GetAuthorizationContextAsync(userCode); 146 | if (request != null) 147 | { 148 | var client = await _clientStore.FindEnabledClientByIdAsync(request.ClientId); 149 | if (client != null) 150 | { 151 | var resources = await _resourceStore.FindEnabledResourcesByScopeAsync(request.ScopesRequested); 152 | if (resources != null && (resources.IdentityResources.Any() || resources.ApiResources.Any())) 153 | { 154 | return CreateConsentViewModel(userCode, model, client, resources); 155 | } 156 | else 157 | { 158 | _logger.LogError("No scopes matching: {0}", request.ScopesRequested.Aggregate((x, y) => x + ", " + y)); 159 | } 160 | } 161 | else 162 | { 163 | _logger.LogError("Invalid client id: {0}", request.ClientId); 164 | } 165 | } 166 | 167 | return null; 168 | } 169 | 170 | private DeviceAuthorizationViewModel CreateConsentViewModel(string userCode, DeviceAuthorizationInputModel model, Client client, Resources resources) 171 | { 172 | var vm = new DeviceAuthorizationViewModel 173 | { 174 | UserCode = userCode, 175 | 176 | RememberConsent = model?.RememberConsent ?? true, 177 | ScopesConsented = model?.ScopesConsented ?? Enumerable.Empty(), 178 | 179 | ClientName = client.ClientName ?? client.ClientId, 180 | ClientUrl = client.ClientUri, 181 | ClientLogoUrl = client.LogoUri, 182 | AllowRememberConsent = client.AllowRememberConsent 183 | }; 184 | 185 | vm.IdentityScopes = resources.IdentityResources.Select(x => CreateScopeViewModel(x, vm.ScopesConsented.Contains(x.Name) || model == null)).ToArray(); 186 | vm.ResourceScopes = resources.ApiResources.SelectMany(x => x.Scopes).Select(x => CreateScopeViewModel(x, vm.ScopesConsented.Contains(x.Name) || model == null)).ToArray(); 187 | if (ConsentOptions.EnableOfflineAccess && resources.OfflineAccess) 188 | { 189 | vm.ResourceScopes = vm.ResourceScopes.Union(new[] 190 | { 191 | GetOfflineAccessScope(vm.ScopesConsented.Contains(IdentityServerConstants.StandardScopes.OfflineAccess) || model == null) 192 | }); 193 | } 194 | 195 | return vm; 196 | } 197 | 198 | private ScopeViewModel CreateScopeViewModel(IdentityResource identity, bool check) 199 | { 200 | return new ScopeViewModel 201 | { 202 | Name = identity.Name, 203 | DisplayName = identity.DisplayName, 204 | Description = identity.Description, 205 | Emphasize = identity.Emphasize, 206 | Required = identity.Required, 207 | Checked = check || identity.Required 208 | }; 209 | } 210 | 211 | public ScopeViewModel CreateScopeViewModel(Scope scope, bool check) 212 | { 213 | return new ScopeViewModel 214 | { 215 | Name = scope.Name, 216 | DisplayName = scope.DisplayName, 217 | Description = scope.Description, 218 | Emphasize = scope.Emphasize, 219 | Required = scope.Required, 220 | Checked = check || scope.Required 221 | }; 222 | } 223 | private ScopeViewModel GetOfflineAccessScope(bool check) 224 | { 225 | return new ScopeViewModel 226 | { 227 | Name = IdentityServerConstants.StandardScopes.OfflineAccess, 228 | DisplayName = ConsentOptions.OfflineAccessDisplayName, 229 | Description = ConsentOptions.OfflineAccessDescription, 230 | Emphasize = true, 231 | Checked = check 232 | }; 233 | } 234 | } 235 | } -------------------------------------------------------------------------------- /IdentityService/Quickstart/Diagnostics/DiagnosticsController.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. 3 | 4 | 5 | using System.Linq; 6 | using System.Threading.Tasks; 7 | using Microsoft.AspNetCore.Authentication; 8 | using Microsoft.AspNetCore.Authorization; 9 | using Microsoft.AspNetCore.Mvc; 10 | 11 | namespace IdentityServer4.Quickstart.UI 12 | { 13 | [SecurityHeaders] 14 | [Authorize] 15 | public class DiagnosticsController : Controller 16 | { 17 | public async Task Index() 18 | { 19 | var model = new DiagnosticsViewModel(await HttpContext.AuthenticateAsync()); 20 | return View(model); 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /IdentityService/Quickstart/Diagnostics/DiagnosticsViewModel.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. 3 | 4 | 5 | using IdentityModel; 6 | using Microsoft.AspNetCore.Authentication; 7 | using Newtonsoft.Json; 8 | using System.Collections.Generic; 9 | using System.Text; 10 | 11 | namespace IdentityServer4.Quickstart.UI 12 | { 13 | public class DiagnosticsViewModel 14 | { 15 | public DiagnosticsViewModel(AuthenticateResult result) 16 | { 17 | AuthenticateResult = result; 18 | 19 | if (result.Properties.Items.ContainsKey("client_list")) 20 | { 21 | var encoded = result.Properties.Items["client_list"]; 22 | var bytes = Base64Url.Decode(encoded); 23 | var value = Encoding.UTF8.GetString(bytes); 24 | 25 | Clients = JsonConvert.DeserializeObject(value); 26 | } 27 | } 28 | 29 | public AuthenticateResult AuthenticateResult { get; } 30 | public IEnumerable Clients { get; } = new List(); 31 | } 32 | } -------------------------------------------------------------------------------- /IdentityService/Quickstart/Extensions.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using IdentityServer4.Stores; 3 | 4 | namespace IdentityServer4.Quickstart.UI 5 | { 6 | public static class Extensions 7 | { 8 | /// 9 | /// Determines whether the client is configured to use PKCE. 10 | /// 11 | /// The store. 12 | /// The client identifier. 13 | /// 14 | public static async Task IsPkceClientAsync(this IClientStore store, string client_id) 15 | { 16 | if (!string.IsNullOrWhiteSpace(client_id)) 17 | { 18 | var client = await store.FindEnabledClientByIdAsync(client_id); 19 | return client?.RequirePkce == true; 20 | } 21 | 22 | return false; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /IdentityService/Quickstart/Grants/GrantsController.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. 3 | 4 | 5 | using IdentityServer4.Services; 6 | using IdentityServer4.Stores; 7 | using Microsoft.AspNetCore.Mvc; 8 | using System.Collections.Generic; 9 | using System.Linq; 10 | using System.Threading.Tasks; 11 | using Microsoft.AspNetCore.Authorization; 12 | using IdentityServer4.Events; 13 | using IdentityServer4.Extensions; 14 | 15 | namespace IdentityServer4.Quickstart.UI 16 | { 17 | /// 18 | /// This sample controller allows a user to revoke grants given to clients 19 | /// 20 | [SecurityHeaders] 21 | [Authorize] 22 | public class GrantsController : Controller 23 | { 24 | private readonly IIdentityServerInteractionService _interaction; 25 | private readonly IClientStore _clients; 26 | private readonly IResourceStore _resources; 27 | private readonly IEventService _events; 28 | 29 | public GrantsController(IIdentityServerInteractionService interaction, 30 | IClientStore clients, 31 | IResourceStore resources, 32 | IEventService events) 33 | { 34 | _interaction = interaction; 35 | _clients = clients; 36 | _resources = resources; 37 | _events = events; 38 | } 39 | 40 | /// 41 | /// Show list of grants 42 | /// 43 | [HttpGet] 44 | public async Task Index() 45 | { 46 | return View("Index", await BuildViewModelAsync()); 47 | } 48 | 49 | /// 50 | /// Handle postback to revoke a client 51 | /// 52 | [HttpPost] 53 | [ValidateAntiForgeryToken] 54 | public async Task Revoke(string clientId) 55 | { 56 | await _interaction.RevokeUserConsentAsync(clientId); 57 | await _events.RaiseAsync(new GrantsRevokedEvent(User.GetSubjectId(), clientId)); 58 | 59 | return RedirectToAction("Index"); 60 | } 61 | 62 | private async Task BuildViewModelAsync() 63 | { 64 | var grants = await _interaction.GetAllUserConsentsAsync(); 65 | 66 | var list = new List(); 67 | foreach(var grant in grants) 68 | { 69 | var client = await _clients.FindClientByIdAsync(grant.ClientId); 70 | if (client != null) 71 | { 72 | var resources = await _resources.FindResourcesByScopeAsync(grant.Scopes); 73 | 74 | var item = new GrantViewModel() 75 | { 76 | ClientId = client.ClientId, 77 | ClientName = client.ClientName ?? client.ClientId, 78 | ClientLogoUrl = client.LogoUri, 79 | ClientUrl = client.ClientUri, 80 | Created = grant.CreationTime, 81 | Expires = grant.Expiration, 82 | IdentityGrantNames = resources.IdentityResources.Select(x => x.DisplayName ?? x.Name).ToArray(), 83 | ApiGrantNames = resources.ApiResources.Select(x => x.DisplayName ?? x.Name).ToArray() 84 | }; 85 | 86 | list.Add(item); 87 | } 88 | } 89 | 90 | return new GrantsViewModel 91 | { 92 | Grants = list 93 | }; 94 | } 95 | } 96 | } -------------------------------------------------------------------------------- /IdentityService/Quickstart/Grants/GrantsViewModel.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. 3 | 4 | 5 | using System; 6 | using System.Collections.Generic; 7 | 8 | namespace IdentityServer4.Quickstart.UI 9 | { 10 | public class GrantsViewModel 11 | { 12 | public IEnumerable Grants { get; set; } 13 | } 14 | 15 | public class GrantViewModel 16 | { 17 | public string ClientId { get; set; } 18 | public string ClientName { get; set; } 19 | public string ClientUrl { get; set; } 20 | public string ClientLogoUrl { get; set; } 21 | public DateTime Created { get; set; } 22 | public DateTime? Expires { get; set; } 23 | public IEnumerable IdentityGrantNames { get; set; } 24 | public IEnumerable ApiGrantNames { get; set; } 25 | } 26 | } -------------------------------------------------------------------------------- /IdentityService/Quickstart/Home/ErrorViewModel.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. 3 | 4 | 5 | using IdentityServer4.Models; 6 | 7 | namespace IdentityServer4.Quickstart.UI 8 | { 9 | public class ErrorViewModel 10 | { 11 | public ErrorMessage Error { get; set; } 12 | } 13 | } -------------------------------------------------------------------------------- /IdentityService/Quickstart/Home/HomeController.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. 3 | 4 | 5 | using IdentityServer4.Services; 6 | using Microsoft.AspNetCore.Mvc; 7 | using System.Threading.Tasks; 8 | 9 | namespace IdentityServer4.Quickstart.UI 10 | { 11 | [SecurityHeaders] 12 | public class HomeController : Controller 13 | { 14 | private readonly IIdentityServerInteractionService _interaction; 15 | 16 | public HomeController(IIdentityServerInteractionService interaction) 17 | { 18 | _interaction = interaction; 19 | } 20 | 21 | public IActionResult Index() 22 | { 23 | return View(); 24 | } 25 | 26 | /// 27 | /// Shows the error page 28 | /// 29 | public async Task Error(string errorId) 30 | { 31 | var vm = new ErrorViewModel(); 32 | 33 | // retrieve error details from identityserver 34 | var message = await _interaction.GetErrorContextAsync(errorId); 35 | if (message != null) 36 | { 37 | vm.Error = message; 38 | } 39 | 40 | return View("Error", vm); 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /IdentityService/Quickstart/SecurityHeadersAttribute.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. 3 | 4 | 5 | using Microsoft.AspNetCore.Mvc; 6 | using Microsoft.AspNetCore.Mvc.Filters; 7 | 8 | namespace IdentityServer4.Quickstart.UI 9 | { 10 | public class SecurityHeadersAttribute : ActionFilterAttribute 11 | { 12 | public override void OnResultExecuting(ResultExecutingContext context) 13 | { 14 | var result = context.Result; 15 | if (result is ViewResult) 16 | { 17 | // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Content-Type-Options 18 | if (!context.HttpContext.Response.Headers.ContainsKey("X-Content-Type-Options")) 19 | { 20 | context.HttpContext.Response.Headers.Add("X-Content-Type-Options", "nosniff"); 21 | } 22 | 23 | // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options 24 | if (!context.HttpContext.Response.Headers.ContainsKey("X-Frame-Options")) 25 | { 26 | context.HttpContext.Response.Headers.Add("X-Frame-Options", "SAMEORIGIN"); 27 | } 28 | 29 | // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy 30 | var csp = "default-src 'self'; object-src 'none'; frame-ancestors 'none'; sandbox allow-forms allow-same-origin allow-scripts; base-uri 'self';"; 31 | // also consider adding upgrade-insecure-requests once you have HTTPS in place for production 32 | //csp += "upgrade-insecure-requests;"; 33 | // also an example if you need client images to be displayed from twitter 34 | // csp += "img-src 'self' https://pbs.twimg.com;"; 35 | 36 | // once for standards compliant browsers 37 | if (!context.HttpContext.Response.Headers.ContainsKey("Content-Security-Policy")) 38 | { 39 | context.HttpContext.Response.Headers.Add("Content-Security-Policy", csp); 40 | } 41 | // and once again for IE 42 | if (!context.HttpContext.Response.Headers.ContainsKey("X-Content-Security-Policy")) 43 | { 44 | context.HttpContext.Response.Headers.Add("X-Content-Security-Policy", csp); 45 | } 46 | 47 | // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy 48 | var referrer_policy = "no-referrer"; 49 | if (!context.HttpContext.Response.Headers.ContainsKey("Referrer-Policy")) 50 | { 51 | context.HttpContext.Response.Headers.Add("Referrer-Policy", referrer_policy); 52 | } 53 | } 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /IdentityService/Quickstart/TestUsers.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Brock Allen & Dominick Baier. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. 3 | 4 | 5 | using IdentityModel; 6 | using IdentityServer4.Test; 7 | using System.Collections.Generic; 8 | using System.Security.Claims; 9 | 10 | namespace IdentityServer4.Quickstart.UI 11 | { 12 | public class TestUsers 13 | { 14 | public static List Users = new List 15 | { 16 | new TestUser { SubjectId = "1", Username = "alice", Password = "alice", 17 | Claims = 18 | { 19 | new Claim(JwtClaimTypes.Name, "Alice Smith"), 20 | new Claim(JwtClaimTypes.GivenName, "Alice"), 21 | new Claim(JwtClaimTypes.FamilyName, "Smith"), 22 | new Claim(JwtClaimTypes.Email, "AliceSmith@email.com"), 23 | new Claim(JwtClaimTypes.EmailVerified, "true", ClaimValueTypes.Boolean), 24 | new Claim(JwtClaimTypes.WebSite, "http://alice.com"), 25 | new Claim(JwtClaimTypes.Address, @"{ 'street_address': 'One Hacker Way', 'locality': 'Heidelberg', 'postal_code': 69118, 'country': 'Germany' }", IdentityServer4.IdentityServerConstants.ClaimValueTypes.Json) 26 | } 27 | }, 28 | new TestUser { SubjectId = "11", Username = "bob", Password = "bob", 29 | Claims = 30 | { 31 | new Claim(JwtClaimTypes.Name, "Bob Smith"), 32 | new Claim(JwtClaimTypes.GivenName, "Bob"), 33 | new Claim(JwtClaimTypes.FamilyName, "Smith"), 34 | new Claim(JwtClaimTypes.Email, "BobSmith@email.com"), 35 | new Claim(JwtClaimTypes.EmailVerified, "true", ClaimValueTypes.Boolean), 36 | new Claim(JwtClaimTypes.WebSite, "http://bob.com"), 37 | new Claim(JwtClaimTypes.Address, @"{ 'street_address': 'One Hacker Way', 'locality': 'Heidelberg', 'postal_code': 69118, 'country': 'Germany' }", IdentityServer4.IdentityServerConstants.ClaimValueTypes.Json), 38 | new Claim("location", "somewhere") 39 | } 40 | } 41 | }; 42 | } 43 | } -------------------------------------------------------------------------------- /IdentityService/SerilogMiddleware.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Http; 2 | using Serilog; 3 | using Serilog.Events; 4 | using System; 5 | using System.Diagnostics; 6 | using System.Linq; 7 | using System.Threading.Tasks; 8 | 9 | namespace Datalust.SerilogMiddlewareExample.Diagnostics 10 | { 11 | class SerilogMiddleware 12 | { 13 | const string MessageTemplate = "HTTP {RequestMethod} {RequestPath} responded {StatusCode} in {Elapsed:0.0000} ms"; 14 | 15 | static readonly ILogger Log = Serilog.Log.ForContext(); 16 | 17 | readonly RequestDelegate _next; 18 | 19 | public SerilogMiddleware(RequestDelegate next) 20 | { 21 | if (next == null) throw new ArgumentNullException(nameof(next)); 22 | _next = next; 23 | } 24 | 25 | public async Task Invoke(HttpContext httpContext) 26 | { 27 | if (httpContext == null) throw new ArgumentNullException(nameof(httpContext)); 28 | 29 | var start = Stopwatch.GetTimestamp(); 30 | try 31 | { 32 | await _next(httpContext); 33 | var elapsedMs = GetElapsedMilliseconds(start, Stopwatch.GetTimestamp()); 34 | 35 | var statusCode = httpContext.Response?.StatusCode; 36 | var level = statusCode > 499 ? LogEventLevel.Error : LogEventLevel.Information; 37 | 38 | var log = level == LogEventLevel.Error ? LogForErrorContext(httpContext) : Log; 39 | log.Write(level, MessageTemplate, httpContext.Request.Method, httpContext.Request.Path, statusCode, elapsedMs); 40 | } 41 | // Never caught, because `LogException()` returns false. 42 | catch (Exception ex) when (LogException(httpContext, GetElapsedMilliseconds(start, Stopwatch.GetTimestamp()), ex)) { } 43 | } 44 | 45 | static bool LogException(HttpContext httpContext, double elapsedMs, Exception ex) 46 | { 47 | LogForErrorContext(httpContext) 48 | .Error(ex, MessageTemplate, httpContext.Request.Method, httpContext.Request.Path, 500, elapsedMs); 49 | 50 | return false; 51 | } 52 | 53 | static ILogger LogForErrorContext(HttpContext httpContext) 54 | { 55 | var request = httpContext.Request; 56 | 57 | var result = Log 58 | .ForContext("RequestHeaders", request.Headers.ToDictionary(h => h.Key, h => h.Value.ToString()), destructureObjects: true) 59 | .ForContext("RequestHost", request.Host) 60 | .ForContext("RequestProtocol", request.Protocol); 61 | 62 | if (request.HasFormContentType) 63 | result = result.ForContext("RequestForm", request.Form.ToDictionary(v => v.Key, v => v.Value.ToString())); 64 | 65 | return result; 66 | } 67 | 68 | static double GetElapsedMilliseconds(long start, long stop) 69 | { 70 | return (stop - start) * 1000 / (double)Stopwatch.Frequency; 71 | } 72 | } 73 | } -------------------------------------------------------------------------------- /IdentityService/Startup.cs: -------------------------------------------------------------------------------- 1 | using Datalust.SerilogMiddlewareExample.Diagnostics; 2 | using IdentityServer4; 3 | using IdentityServer4.AccessTokenValidation; 4 | using IdentityServer4.Quickstart.UI; 5 | using IdentityServer4.Services; 6 | using IdentityServer4.Validation; 7 | using Microsoft.AspNetCore.Builder; 8 | using Microsoft.Extensions.Configuration; 9 | using Microsoft.Extensions.DependencyInjection; 10 | using Microsoft.IdentityModel.Tokens; 11 | 12 | namespace IdentityServer4Demo 13 | { 14 | public class Startup 15 | { 16 | public IConfiguration Configuration { get; } 17 | 18 | public Startup(IConfiguration configuration) 19 | { 20 | Configuration = configuration; 21 | } 22 | 23 | public void ConfigureServices(IServiceCollection services) 24 | { 25 | services.AddMvc(); 26 | 27 | services.AddIdentityServer(options => 28 | { 29 | options.Events.RaiseErrorEvents = true; 30 | options.Events.RaiseFailureEvents = true; 31 | options.Events.RaiseInformationEvents = true; 32 | options.Events.RaiseSuccessEvents = true; 33 | }) 34 | .AddInMemoryApiResources(Config.GetApis()) 35 | .AddInMemoryIdentityResources(Config.GetIdentityResources()) 36 | .AddInMemoryClients(Config.GetClients()) 37 | .AddTestUsers(TestUsers.Users) 38 | .AddDeveloperSigningCredential(persistKey: false); 39 | 40 | services.AddAuthentication() 41 | .AddOpenIdConnect("aad", "Sign-in with Azure AD", options => 42 | { 43 | options.Authority = "https://login.microsoftonline.com/common"; 44 | options.ClientId = "https://leastprivilegelabs.onmicrosoft.com/38196330-e766-4051-ad10-14596c7e97d3"; 45 | 46 | options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme; 47 | options.SignOutScheme = IdentityServerConstants.SignoutScheme; 48 | 49 | options.ResponseType = "id_token"; 50 | options.CallbackPath = "/signin-aad"; 51 | options.SignedOutCallbackPath = "/signout-callback-aad"; 52 | options.RemoteSignOutPath = "/signout-aad"; 53 | 54 | options.TokenValidationParameters = new TokenValidationParameters 55 | { 56 | ValidateIssuer = false, 57 | ValidAudience = "165b99fd-195f-4d93-a111-3e679246e6a9", 58 | 59 | NameClaimType = "name", 60 | RoleClaimType = "role" 61 | }; 62 | }) 63 | .AddIdentityServerAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme, options => 64 | { 65 | options.Authority = "https://demo.identityserver.io"; 66 | 67 | options.ApiName = "api"; 68 | options.ApiSecret = "secret"; 69 | }); 70 | 71 | // preserve OIDC state in cache (solves problems with AAD and URL lenghts) 72 | services.AddOidcStateDataFormatterCache("aad"); 73 | 74 | // add CORS policy for non-IdentityServer endpoints 75 | services.AddCors(options => 76 | { 77 | options.AddPolicy("api", policy => 78 | { 79 | policy.AllowAnyOrigin().AllowAnyHeader().AllowAnyMethod(); 80 | }); 81 | }); 82 | 83 | // demo versions (never use in production) 84 | services.AddTransient(); 85 | services.AddTransient(); 86 | } 87 | 88 | public void Configure(IApplicationBuilder app) 89 | { 90 | app.UseDeveloperExceptionPage(); 91 | 92 | app.UseCors("api"); 93 | 94 | app.UseMiddleware(); 95 | 96 | app.UseStaticFiles(); 97 | app.UseIdentityServer(); 98 | app.UseMvcWithDefaultRoute(); 99 | } 100 | } 101 | } -------------------------------------------------------------------------------- /IdentityService/Views/Account/LoggedOut.cshtml: -------------------------------------------------------------------------------- 1 | @model LoggedOutViewModel 2 | 3 | @{ 4 | // set this so the layout rendering sees an anonymous user 5 | ViewData["signed-out"] = true; 6 | } 7 | 8 | 27 | 28 | @section scripts 29 | { 30 | @if (Model.AutomaticRedirectAfterSignOut) 31 | { 32 | 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /IdentityService/Views/Account/Login.cshtml: -------------------------------------------------------------------------------- 1 | @model LoginViewModel 2 | 3 | -------------------------------------------------------------------------------- /IdentityService/Views/Account/Logout.cshtml: -------------------------------------------------------------------------------- 1 | @model LogoutViewModel 2 | 3 |
4 | 7 | 8 |
9 |
10 |

Would you like to logout of IdentityServer?

11 |
12 | 13 |
14 |
15 | 16 |
17 |
18 |
19 |
20 |
21 |
-------------------------------------------------------------------------------- /IdentityService/Views/Consent/Index.cshtml: -------------------------------------------------------------------------------- 1 | @model ConsentViewModel 2 | 3 | -------------------------------------------------------------------------------- /IdentityService/Views/Consent/_ScopeListItem.cshtml: -------------------------------------------------------------------------------- 1 | @model ScopeViewModel 2 | 3 |
  • 4 | 24 | @if (Model.Required) 25 | { 26 | (required) 27 | } 28 | @if (Model.Description != null) 29 | { 30 | 33 | } 34 |
  • -------------------------------------------------------------------------------- /IdentityService/Views/Device/Success.cshtml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /IdentityService/Views/Device/UserCodeCapture.cshtml: -------------------------------------------------------------------------------- 1 | @model string 2 | 3 | -------------------------------------------------------------------------------- /IdentityService/Views/Device/UserCodeConfirmation.cshtml: -------------------------------------------------------------------------------- 1 | @model IdentityServer4.Quickstart.UI.Device.DeviceAuthorizationViewModel 2 | 3 | -------------------------------------------------------------------------------- /IdentityService/Views/Diagnostics/Index.cshtml: -------------------------------------------------------------------------------- 1 | @model DiagnosticsViewModel 2 | 3 |

    Authentication cookie

    4 | 5 |

    Claims

    6 |
    7 | @foreach (var claim in Model.AuthenticateResult.Principal.Claims) 8 | { 9 |
    @claim.Type
    10 |
    @claim.Value
    11 | } 12 |
    13 | 14 |

    Properties

    15 |
    16 | @foreach (var prop in Model.AuthenticateResult.Properties.Items) 17 | { 18 |
    @prop.Key
    19 |
    @prop.Value
    20 | } 21 |
    22 | 23 | @if (Model.Clients.Any()) 24 | { 25 |

    Clients

    26 |
      27 | @foreach (var client in Model.Clients) 28 | { 29 |
    • @client
    • 30 | } 31 |
    32 | } -------------------------------------------------------------------------------- /IdentityService/Views/Grants/Index.cshtml: -------------------------------------------------------------------------------- 1 | @model GrantsViewModel 2 | 3 |
    4 | 12 | 13 | @if (Model.Grants.Any() == false) 14 | { 15 |
    16 |
    17 |
    18 | You have not given access to any applications 19 |
    20 |
    21 |
    22 | } 23 | else 24 | { 25 | foreach (var grant in Model.Grants) 26 | { 27 |
    28 |
    29 | @if (grant.ClientLogoUrl != null) 30 | { 31 | 32 | } 33 |
    34 |
    35 |
    @grant.ClientName
    36 |
    37 | Created: @grant.Created.ToString("yyyy-MM-dd") 38 |
    39 | @if (grant.Expires.HasValue) 40 | { 41 |
    42 | Expires: @grant.Expires.Value.ToString("yyyy-MM-dd") 43 |
    44 | } 45 | @if (grant.IdentityGrantNames.Any()) 46 | { 47 |
    48 |
    Identity Grants
    49 |
      50 | @foreach (var name in grant.IdentityGrantNames) 51 | { 52 |
    • @name
    • 53 | } 54 |
    55 |
    56 | } 57 | @if (grant.ApiGrantNames.Any()) 58 | { 59 |
    60 |
    API Grants
    61 |
      62 | @foreach (var name in grant.ApiGrantNames) 63 | { 64 |
    • @name
    • 65 | } 66 |
    67 |
    68 | } 69 |
    70 |
    71 |
    72 | 73 | 74 |
    75 |
    76 |
    77 | } 78 | } 79 |
    -------------------------------------------------------------------------------- /IdentityService/Views/Home/Index.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | var version = typeof(IdentityServer4.Hosting.IdentityServerMiddleware).Assembly.GetName().Version.ToString(); 3 | } 4 | 5 |
    6 | 15 | 16 |
    17 |
    18 |

    19 | IdentityServer publishes a 20 | discovery document 21 | where you can find metadata and links to all the endpoints, key material, etc. 22 |

    23 |
    24 |
    25 |

    26 | You can use the following clients (see here for the code definition). 27 |

    28 |

    29 | Click here to manage your stored grants. 30 |

    31 |
    32 |
    33 |
    34 |
    35 |

    36 | client id: native.hybrid
    37 | grant type: hybrid with PKCE
    38 | allowed scopes: openid profile email api offline_access 39 |

    40 | 41 |

    42 | client id: native.code
    43 | grant type: authorization code with PKCE
    44 | allowed scopes: openid profile email api offline_access 45 |

    46 | 47 |

    48 | client id: server.hybrid
    49 | client secret: secret
    50 | grant type: hybrid
    51 | allowed scopes: openid profile email api offline_access 52 |

    53 | 54 |

    55 | client id: server.hybrid.short (access token lifetime of 70 seconds)
    56 | client secret: secret
    57 | grant type: hybrid
    58 | allowed scopes: openid profile email api offline_access 59 |

    60 | 61 |

    62 | client id: server.code
    63 | client secret: secret
    64 | grant type: authorization code
    65 | allowed scopes: openid profile email api offline_access 66 |

    67 | 68 |

    69 | client id: spa
    70 | grant type: authorization code with PKCE
    71 | allowed scopes: openid profile email api offline_access 72 |

    73 |

    74 | client id: spa.short (access token lifetime 70 seconds)
    75 | grant type: authorization code with PKCE
    76 | allowed scopes: openid profile email api offline_access 77 |

    78 | 79 |

    80 | client id: implicit
    81 | grant type: implicit
    82 | allowed scopes: openid profile email api 83 |

    84 | 85 |

    86 | client id: implicit.reference
    87 | grant type: implicit
    88 | allowed scopes: openid profile email api 89 |

    90 | 91 |

    92 | client id: implicit.shortlived
    93 | grant type: implicit
    94 | allowed scopes: openid profile email api 95 |

    96 | 97 |

    98 | client id: client
    99 | client secret: secret
    100 | grant type: client credentials
    101 | allowed scopes: api 102 |

    103 | 104 |

    105 | client id: device
    106 | grant type: urn:ietf:params:oauth:grant-type:device_code
    107 | allowed scopes: openid profile email api 108 |

    109 | 110 |

    111 | You can call a test API at https://demo.identityserver.io/api/test. 112 |

    113 |
    114 |
    115 |
    116 | -------------------------------------------------------------------------------- /IdentityService/Views/Shared/Error.cshtml: -------------------------------------------------------------------------------- 1 | @model ErrorViewModel 2 | 3 | @{ 4 | var error = Model?.Error?.Error; 5 | var errorDescription = Model?.Error?.ErrorDescription; 6 | var request_id = Model?.Error?.RequestId; 7 | } 8 | 9 |
    10 | 13 | 14 |
    15 |
    16 |
    17 | Sorry, there was an error 18 | 19 | @if (error != null) 20 | { 21 | 22 | 23 | : @error 24 | 25 | 26 | 27 | if (errorDescription != null) 28 | { 29 |
    @errorDescription
    30 | } 31 | } 32 |
    33 | 34 | @if (request_id != null) 35 | { 36 |
    Request Id: @request_id
    37 | } 38 |
    39 |
    40 |
    41 | -------------------------------------------------------------------------------- /IdentityService/Views/Shared/Redirect.cshtml: -------------------------------------------------------------------------------- 1 | @model RedirectViewModel 2 | 3 |

    You are now being returned to the application.

    4 |

    Once complete, you may close this tab

    5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /IdentityService/Views/Shared/_Layout.cshtml: -------------------------------------------------------------------------------- 1 | @using IdentityServer4.Extensions 2 | @{ 3 | string name = null; 4 | if (!true.Equals(ViewData["signed-out"])) 5 | { 6 | name = Context.User?.GetDisplayName(); 7 | } 8 | } 9 | 10 | 11 | 12 | 13 | 14 | 15 | IdentityServer4 16 | 17 | 18 | 19 | 20 | 21 | 22 | 52 | 53 |
    54 | @RenderBody() 55 |
    56 | 57 | 58 | 59 | @RenderSection("scripts", required: false) 60 | 61 | 62 | -------------------------------------------------------------------------------- /IdentityService/Views/Shared/_ScopeListItem.cshtml: -------------------------------------------------------------------------------- 1 | @model ScopeViewModel 2 | 3 |
  • 4 | 24 | @if (Model.Required) 25 | { 26 | (required) 27 | } 28 | @if (Model.Description != null) 29 | { 30 | 33 | } 34 |
  • -------------------------------------------------------------------------------- /IdentityService/Views/Shared/_ValidationSummary.cshtml: -------------------------------------------------------------------------------- 1 | @if (ViewContext.ModelState.IsValid == false) 2 | { 3 |
    4 | Error 5 |
    6 |
    7 | } -------------------------------------------------------------------------------- /IdentityService/Views/_ViewImports.cshtml: -------------------------------------------------------------------------------- 1 | @using IdentityServer4.Quickstart.UI 2 | @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers 3 | -------------------------------------------------------------------------------- /IdentityService/Views/_ViewStart.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | Layout = "_Layout"; 3 | } 4 | -------------------------------------------------------------------------------- /IdentityService/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "KeyVault": { 3 | "BaseUrl": "https://identityserverdemo.vault.azure.net/" 4 | } 5 | } -------------------------------------------------------------------------------- /IdentityService/updateUI.ps1: -------------------------------------------------------------------------------- 1 | iex ((New-Object System.Net.WebClient).DownloadString('https://raw.githubusercontent.com/IdentityServer/IdentityServer4.Quickstart.UI/master/getmaster.ps1')) -------------------------------------------------------------------------------- /IdentityService/web.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /IdentityService/wwwroot/css/site.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin-top: 65px; 3 | } 4 | .navbar-header { 5 | position: relative; 6 | top: -4px; 7 | } 8 | .navbar-brand > .icon-banner { 9 | position: relative; 10 | top: -2px; 11 | display: inline; 12 | } 13 | .icon { 14 | position: relative; 15 | top: -10px; 16 | } 17 | .logged-out iframe { 18 | display: none; 19 | width: 0; 20 | height: 0; 21 | } 22 | .page-consent .client-logo { 23 | float: left; 24 | } 25 | .page-consent .client-logo img { 26 | width: 80px; 27 | height: 80px; 28 | } 29 | .page-consent .consent-buttons { 30 | margin-top: 25px; 31 | } 32 | .page-consent .consent-form .consent-scopecheck { 33 | display: inline-block; 34 | margin-right: 5px; 35 | } 36 | .page-consent .consent-form .consent-description { 37 | margin-left: 25px; 38 | } 39 | .page-consent .consent-form .consent-description label { 40 | font-weight: normal; 41 | } 42 | .page-consent .consent-form .consent-remember { 43 | padding-left: 16px; 44 | } 45 | .grants .page-header { 46 | margin-bottom: 10px; 47 | } 48 | .grants .grant { 49 | margin-top: 20px; 50 | padding-bottom: 20px; 51 | border-bottom: 1px solid lightgray; 52 | } 53 | .grants .grant img { 54 | width: 100px; 55 | height: 100px; 56 | } 57 | .grants .grant .clientname { 58 | font-size: 140%; 59 | font-weight: bold; 60 | } 61 | .grants .grant .granttype { 62 | font-size: 120%; 63 | font-weight: bold; 64 | } 65 | .grants .grant .created { 66 | font-size: 120%; 67 | font-weight: bold; 68 | } 69 | .grants .grant .expires { 70 | font-size: 120%; 71 | font-weight: bold; 72 | } 73 | .grants .grant li { 74 | list-style-type: none; 75 | display: inline; 76 | } 77 | .grants .grant li:after { 78 | content: ', '; 79 | } 80 | .grants .grant li:last-child:after { 81 | content: ''; 82 | } -------------------------------------------------------------------------------- /IdentityService/wwwroot/css/site.less: -------------------------------------------------------------------------------- 1 | body { 2 | margin-top: 65px; 3 | } 4 | 5 | .navbar-header { 6 | position: relative; 7 | top: -4px; 8 | } 9 | 10 | .navbar-brand > .icon-banner { 11 | position: relative; 12 | top: -2px; 13 | display: inline; 14 | } 15 | 16 | .icon { 17 | position: relative; 18 | top: -10px; 19 | } 20 | 21 | .logged-out iframe { 22 | display: none; 23 | width: 0; 24 | height: 0; 25 | } 26 | 27 | .page-consent { 28 | .client-logo { 29 | float: left; 30 | 31 | img { 32 | width: 80px; 33 | height: 80px; 34 | } 35 | } 36 | 37 | .consent-buttons { 38 | margin-top: 25px; 39 | } 40 | 41 | .consent-form { 42 | .consent-scopecheck { 43 | display: inline-block; 44 | margin-right: 5px; 45 | } 46 | 47 | .consent-scopecheck[disabled] { 48 | //visibility:hidden; 49 | } 50 | 51 | .consent-description { 52 | margin-left: 25px; 53 | 54 | label { 55 | font-weight: normal; 56 | } 57 | } 58 | 59 | .consent-remember { 60 | padding-left: 16px; 61 | } 62 | } 63 | } 64 | 65 | .grants { 66 | .page-header { 67 | margin-bottom: 10px; 68 | } 69 | 70 | .grant { 71 | margin-top: 20px; 72 | padding-bottom: 20px; 73 | border-bottom: 1px solid lightgray; 74 | 75 | img { 76 | width: 100px; 77 | height: 100px; 78 | } 79 | 80 | .clientname { 81 | font-size: 140%; 82 | font-weight: bold; 83 | } 84 | 85 | .granttype { 86 | font-size: 120%; 87 | font-weight: bold; 88 | } 89 | 90 | .created { 91 | font-size: 120%; 92 | font-weight: bold; 93 | } 94 | 95 | .expires { 96 | font-size: 120%; 97 | font-weight: bold; 98 | } 99 | 100 | li { 101 | list-style-type: none; 102 | display: inline; 103 | 104 | &:after { 105 | content: ', '; 106 | } 107 | 108 | &:last-child:after { 109 | content: ''; 110 | } 111 | 112 | .displayname { 113 | } 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /IdentityService/wwwroot/css/site.min.css: -------------------------------------------------------------------------------- 1 | body{margin-top:65px;}.navbar-header{position:relative;top:-4px;}.navbar-brand>.icon-banner{position:relative;top:-2px;display:inline;}.icon{position:relative;top:-10px;}.logged-out iframe{display:none;width:0;height:0;}.page-consent .client-logo{float:left;}.page-consent .client-logo img{width:80px;height:80px;}.page-consent .consent-buttons{margin-top:25px;}.page-consent .consent-form .consent-scopecheck{display:inline-block;margin-right:5px;}.page-consent .consent-form .consent-description{margin-left:25px;}.page-consent .consent-form .consent-description label{font-weight:normal;}.page-consent .consent-form .consent-remember{padding-left:16px;}.grants .page-header{margin-bottom:10px;}.grants .grant{margin-top:20px;padding-bottom:20px;border-bottom:1px solid #d3d3d3;}.grants .grant img{width:100px;height:100px;}.grants .grant .clientname{font-size:140%;font-weight:bold;}.grants .grant .granttype{font-size:120%;font-weight:bold;}.grants .grant .created{font-size:120%;font-weight:bold;}.grants .grant .expires{font-size:120%;font-weight:bold;}.grants .grant li{list-style-type:none;display:inline;}.grants .grant li:after{content:', ';}.grants .grant li:last-child:after{content:'';} -------------------------------------------------------------------------------- /IdentityService/wwwroot/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thinktecture/pragmatic-microservices-dotnetcore/881e43b4afd9ab17822238d1853ecf4d11567276/IdentityService/wwwroot/favicon.ico -------------------------------------------------------------------------------- /IdentityService/wwwroot/icon.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thinktecture/pragmatic-microservices-dotnetcore/881e43b4afd9ab17822238d1853ecf4d11567276/IdentityService/wwwroot/icon.jpg -------------------------------------------------------------------------------- /IdentityService/wwwroot/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thinktecture/pragmatic-microservices-dotnetcore/881e43b4afd9ab17822238d1853ecf4d11567276/IdentityService/wwwroot/icon.png -------------------------------------------------------------------------------- /IdentityService/wwwroot/js/signin-redirect.js: -------------------------------------------------------------------------------- 1 | window.location.href = document.querySelector("meta[http-equiv=refresh]").getAttribute("data-url"); 2 | -------------------------------------------------------------------------------- /IdentityService/wwwroot/js/signout-redirect.js: -------------------------------------------------------------------------------- 1 | window.addEventListener("load", function () { 2 | var a = document.querySelector("a.PostLogoutRedirectUri"); 3 | if (a) { 4 | window.location = a.href; 5 | } 6 | }); 7 | -------------------------------------------------------------------------------- /IdentityService/wwwroot/lib/bootstrap/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thinktecture/pragmatic-microservices-dotnetcore/881e43b4afd9ab17822238d1853ecf4d11567276/IdentityService/wwwroot/lib/bootstrap/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /IdentityService/wwwroot/lib/bootstrap/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thinktecture/pragmatic-microservices-dotnetcore/881e43b4afd9ab17822238d1853ecf4d11567276/IdentityService/wwwroot/lib/bootstrap/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /IdentityService/wwwroot/lib/bootstrap/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thinktecture/pragmatic-microservices-dotnetcore/881e43b4afd9ab17822238d1853ecf4d11567276/IdentityService/wwwroot/lib/bootstrap/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /IdentityService/wwwroot/lib/bootstrap/fonts/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thinktecture/pragmatic-microservices-dotnetcore/881e43b4afd9ab17822238d1853ecf4d11567276/IdentityService/wwwroot/lib/bootstrap/fonts/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Christian Weyer 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 | -------------------------------------------------------------------------------- /OrdersService/.dockerignore: -------------------------------------------------------------------------------- 1 | .dockerignore 2 | .env 3 | .git 4 | .gitignore 5 | .vs 6 | .vscode 7 | docker-compose.yml 8 | docker-compose.*.yml 9 | */bin 10 | */obj 11 | -------------------------------------------------------------------------------- /OrdersService/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": ".NET Core Attach", 9 | "type": "coreclr", 10 | "request": "attach", 11 | "processId": "${command:pickProcess}" 12 | }, 13 | { 14 | "name": ".NET Core Launch (web)", 15 | "type": "coreclr", 16 | "request": "launch", 17 | "preLaunchTask": "build", 18 | "program": "${workspaceFolder}/bin/Debug/netcoreapp2.1/OrdersService.dll", 19 | "args": [], 20 | "cwd": "${workspaceFolder}", 21 | "stopAtEntry": false, 22 | "internalConsoleOptions": "openOnSessionStart", 23 | "launchBrowser": { 24 | "enabled": true, 25 | "args": "${auto-detect-url}", 26 | "windows": { 27 | "command": "cmd.exe", 28 | "args": "/C start ${auto-detect-url}" 29 | }, 30 | "osx": { 31 | "command": "open" 32 | }, 33 | "linux": { 34 | "command": "xdg-open" 35 | } 36 | }, 37 | "env": { 38 | "ASPNETCORE_ENVIRONMENT": "Development" 39 | }, 40 | "sourceFileMap": { 41 | "/Views": "${workspaceFolder}/Views" 42 | } 43 | }, 44 | { 45 | "name": ".NET Core Attach", 46 | "type": "coreclr", 47 | "request": "attach", 48 | "processId": "${command:pickProcess}" 49 | } 50 | ] 51 | } 52 | -------------------------------------------------------------------------------- /OrdersService/.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "build", 6 | "command": "dotnet", 7 | "type": "process", 8 | "args": [ 9 | "build", 10 | "${workspaceFolder}/OrdersService.csproj" 11 | ], 12 | "problemMatcher": "$msCompile" 13 | } 14 | ] 15 | } -------------------------------------------------------------------------------- /OrdersService/AppSettings.cs: -------------------------------------------------------------------------------- 1 |  2 | namespace OrdersService 3 | { 4 | public class AppSettings 5 | { 6 | public string RabbitMqConnectionString { get; set; } 7 | public string WebApiBaseUrl { get; set; } 8 | public string WebApiHealthUrl { get; set; } 9 | public string SeqBaseUrl { get; set; } 10 | public string IdSrvBaseUrl { get; set; } 11 | public string SelfHostBaseUrl { get; set; } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /OrdersService/Authentication/QueryStringAuthenticationMiddleware.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Builder; 2 | using Microsoft.AspNetCore.Http; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace OrdersService.Authentication 7 | { 8 | public static class QueryStringAuthorizationMiddlewareExtensions 9 | { 10 | public static IApplicationBuilder UseQueryStringAuthorization( 11 | this IApplicationBuilder builder) 12 | { 13 | return builder.UseMiddleware(); 14 | } 15 | } 16 | 17 | public class QueryStringAuthenticationMiddleware 18 | { 19 | private readonly RequestDelegate _next; 20 | private static string _queryStringName = "authorization"; 21 | private static string _authorizationHeaderName = "Authorization"; 22 | 23 | public QueryStringAuthenticationMiddleware(RequestDelegate next) 24 | { 25 | _next = next; 26 | } 27 | 28 | public Task Invoke(HttpContext context) 29 | { 30 | var authorizationQueryStringValue = context.Request.Query[_queryStringName]; 31 | 32 | if (!string.IsNullOrWhiteSpace(authorizationQueryStringValue) && 33 | !context.Request.Headers.ContainsKey(_authorizationHeaderName)) 34 | { 35 | context.Request.Headers.Append(_authorizationHeaderName, "Bearer " + authorizationQueryStringValue); 36 | } 37 | 38 | return this._next(context); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /OrdersService/Controllers/HealthController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authorization; 2 | using Microsoft.AspNetCore.Mvc; 3 | 4 | namespace OrdersService.Controllers 5 | { 6 | [Route("api/[controller]")] 7 | public class HealthController : Controller 8 | { 9 | [HttpGet] 10 | [Route("ping")] 11 | public string Ping() 12 | { 13 | return "OK"; 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /OrdersService/Controllers/OrderServiceException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.Serialization; 3 | 4 | namespace OrdersService.Controllers 5 | { 6 | [Serializable] 7 | public class OrderServiceException : Exception 8 | { 9 | // 10 | // For guidelines regarding the creation of new exception types, see 11 | // http://msdn.microsoft.com/library/default.asp?url=/library/en-us/cpgenref/html/cpconerrorraisinghandlingguidelines.asp 12 | // and 13 | // http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dncscol/html/csharp07192001.asp 14 | // 15 | 16 | public OrderServiceException() 17 | { 18 | } 19 | 20 | public OrderServiceException(string message) : base(message) 21 | { 22 | } 23 | 24 | public OrderServiceException(string message, Exception inner) : base(message, inner) 25 | { 26 | } 27 | 28 | protected OrderServiceException( 29 | SerializationInfo info, 30 | StreamingContext context) : base(info, context) 31 | { 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /OrdersService/Controllers/OrdersController.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using EasyNetQ; 3 | using Microsoft.AspNetCore.Authorization; 4 | using Microsoft.AspNetCore.Mvc; 5 | using Microsoft.AspNetCore.SignalR; 6 | using Microsoft.Extensions.Options; 7 | using OrdersService.Hubs; 8 | using OrdersService.Messages; 9 | using Serilog; 10 | using System; 11 | using System.Collections.Concurrent; 12 | using System.Collections.Generic; 13 | using System.Linq; 14 | using System.Security.Claims; 15 | 16 | namespace OrdersService.Controllers 17 | { 18 | [Route("api/[controller]")] 19 | [Authorize] 20 | public class OrdersController : ControllerBase 21 | { 22 | private static readonly ConcurrentDictionary Datastore; 23 | private readonly AppSettings _settings; 24 | private readonly IHubContext _orderHubContext; 25 | 26 | static OrdersController() 27 | { 28 | Datastore = new ConcurrentDictionary(); 29 | } 30 | 31 | public OrdersController(IOptions appSettingsOptions, IHubContext ordersHubContext) 32 | { 33 | _settings = appSettingsOptions.Value; 34 | _orderHubContext = ordersHubContext; 35 | } 36 | 37 | [HttpGet] 38 | public List GetOrders() 39 | { 40 | try 41 | { 42 | return Datastore.Values.OrderByDescending(o => o.Created).ToList(); 43 | } 44 | catch (Exception e) 45 | { 46 | string message = "We could not retrieve the list of orders."; 47 | Log.Error(message + $" Reason: {0}", e); 48 | 49 | throw new OrderServiceException(message); 50 | } 51 | } 52 | 53 | [HttpPost] 54 | public ActionResult AddNewOrder([FromBody] DTOs.Order newOrder) 55 | { 56 | if (!ModelState.IsValid) 57 | { 58 | return BadRequest(ModelState); 59 | } 60 | 61 | var orderId = Guid.NewGuid(); 62 | newOrder.Id = orderId; 63 | newOrder.Created = DateTime.UtcNow; 64 | 65 | try 66 | { 67 | Datastore.TryAdd(orderId, newOrder); 68 | } 69 | catch (Exception e) 70 | { 71 | string message = "We could not add the new order."; 72 | Log.Error(message + $" Reason: {0}", e); 73 | 74 | throw new OrderServiceException(message); 75 | } 76 | 77 | // TODO: Retry & exception handling 78 | using (var bus = RabbitHutch.CreateBus(_settings.RabbitMqConnectionString)) 79 | { 80 | var identity = User.Identity as ClaimsIdentity; 81 | var subjectId = identity?.Claims.FirstOrDefault(c => c.Type == "sub")?.Value; 82 | 83 | var message = new NewOrderMessage 84 | { 85 | UserId = subjectId, 86 | Order = Mapper.Map(newOrder) 87 | }; 88 | 89 | // TODO: Exception handling 90 | bus.Publish(message); 91 | 92 | _orderHubContext.Clients.Group(message.UserId).SendAsync("orderCreated"); 93 | } 94 | 95 | return Ok(); 96 | } 97 | } 98 | } -------------------------------------------------------------------------------- /OrdersService/DTOs/Order.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace OrdersService.DTOs 5 | { 6 | public class Order 7 | { 8 | public Guid Id { get; set; } 9 | public string Description { get; set; } 10 | public DateTime Created { get; set; } 11 | public List Items { get; set; } 12 | } 13 | } -------------------------------------------------------------------------------- /OrdersService/DTOs/OrderItem.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace OrdersService.DTOs 4 | { 5 | public class OrderItem 6 | { 7 | public Guid Id { get; set; } 8 | public int Quantity { get; set; } 9 | } 10 | } -------------------------------------------------------------------------------- /OrdersService/Discovery/ConsulConfig.cs: -------------------------------------------------------------------------------- 1 | namespace OrdersService.Discovery 2 | { 3 | public class ConsulConfig 4 | { 5 | public string Address { get; set; } 6 | public string ServiceName { get; set; } 7 | public string ServiceID { get; set; } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /OrdersService/Discovery/ConsulHostedService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using Consul; 6 | using Microsoft.AspNetCore.Hosting.Server; 7 | using Microsoft.AspNetCore.Hosting.Server.Features; 8 | using Microsoft.Extensions.Hosting; 9 | using Microsoft.Extensions.Logging; 10 | using Microsoft.Extensions.Options; 11 | using Serilog; 12 | 13 | namespace OrdersService.Discovery 14 | { 15 | public class ConsulHostedService : IHostedService 16 | { 17 | private CancellationTokenSource _cts; 18 | private readonly IConsulClient _consulClient; 19 | private readonly IOptions _consulConfig; 20 | private readonly IServer _server; 21 | private string _registrationID; 22 | 23 | public ConsulHostedService(IConsulClient consulClient, IOptions consulConfig, IServer server) 24 | { 25 | _server = server; 26 | _consulConfig = consulConfig; 27 | _consulClient = consulClient; 28 | 29 | } 30 | public async Task StartAsync(CancellationToken cancellationToken) 31 | { 32 | // Create a linked token so we can trigger cancellation outside of this token's cancellation 33 | _cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); 34 | 35 | var features = _server.Features; 36 | var addresses = features.Get(); 37 | var address = addresses.Addresses.First(); 38 | 39 | var uri = new Uri(address); 40 | _registrationID = $"{_consulConfig.Value.ServiceID}-{uri.Port}"; 41 | 42 | var registration = new AgentServiceRegistration() 43 | { 44 | ID = _registrationID, 45 | Name = _consulConfig.Value.ServiceName, 46 | Address = $"{uri.Scheme}://{uri.Host}", 47 | Port = uri.Port, 48 | Tags = new[] { "Orders" }, 49 | Check = new AgentServiceCheck() 50 | { 51 | HTTP = $"{uri.Scheme}://{uri.Host}:{uri.Port}/api/health/ping", 52 | Timeout = TimeSpan.FromSeconds(3), 53 | Interval = TimeSpan.FromSeconds(10) 54 | } 55 | }; 56 | 57 | Log.Information("Registering in Consul"); 58 | 59 | await _consulClient.Agent.ServiceDeregister(registration.ID, _cts.Token); 60 | await _consulClient.Agent.ServiceRegister(registration, _cts.Token); 61 | } 62 | 63 | public async Task StopAsync(CancellationToken cancellationToken) 64 | { 65 | _cts.Cancel(); 66 | 67 | Log.Information("Deregistering from Consul"); 68 | 69 | try 70 | { 71 | await _consulClient.Agent.ServiceDeregister(_registrationID, cancellationToken); 72 | } 73 | catch (Exception ex) 74 | { 75 | Log.Error(ex, $"Deregisteration failed"); 76 | } 77 | } 78 | } 79 | } -------------------------------------------------------------------------------- /OrdersService/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM microsoft/dotnet:2.1-aspnetcore-runtime AS base 2 | WORKDIR /app 3 | EXPOSE 80 4 | 5 | FROM microsoft/dotnet:2.1-sdk AS build 6 | WORKDIR /src 7 | COPY OrdersService.csproj ./ 8 | RUN dotnet restore 9 | COPY . . 10 | WORKDIR /src/ 11 | RUN dotnet build -c Release -o /app 12 | 13 | FROM build AS publish 14 | RUN dotnet publish -c Release -o /app 15 | 16 | FROM base AS final 17 | WORKDIR /app 18 | COPY --from=publish /app . 19 | ENTRYPOINT ["dotnet", "OrdersService.dll"] 20 | -------------------------------------------------------------------------------- /OrdersService/Hubs/OrdersHub.cs: -------------------------------------------------------------------------------- 1 |  2 | using System.Threading.Tasks; 3 | using Microsoft.AspNetCore.Authorization; 4 | using Microsoft.AspNetCore.SignalR; 5 | using System.Security.Claims; 6 | using System.Linq; 7 | 8 | namespace OrdersService.Hubs 9 | { 10 | [Authorize] 11 | public class OrdersHub : Hub 12 | { 13 | public override Task OnConnectedAsync() 14 | { 15 | var identity = Context.User.Identity as ClaimsIdentity; 16 | var subjectId = identity?.Claims.FirstOrDefault(c => c.Type == "sub")?.Value; 17 | 18 | Groups.AddToGroupAsync(Context.ConnectionId, subjectId); 19 | 20 | return base.OnConnectedAsync(); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /OrdersService/Messages/NewOrderMessage.cs: -------------------------------------------------------------------------------- 1 | namespace OrdersService.Messages 2 | { 3 | public class NewOrderMessage 4 | { 5 | public Order Order { get; set; } 6 | public string UserId { get; set; } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /OrdersService/Messages/Order.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace OrdersService.Messages 5 | { 6 | public class Order 7 | { 8 | public Guid Id { get; set; } 9 | public DateTime Created { get; set; } 10 | public List Items { get; set; } 11 | } 12 | } -------------------------------------------------------------------------------- /OrdersService/Messages/OrderItem.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace OrdersService.Messages 4 | { 5 | public class OrderItem 6 | { 7 | public Guid Id { get; set; } 8 | public int Quantity { get; set; } 9 | } 10 | } -------------------------------------------------------------------------------- /OrdersService/Messages/ShippingCreatedMessage.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace OrdersService.Messages 4 | { 5 | public class ShippingCreatedMessage 6 | { 7 | public Guid Id { get; set; } 8 | public DateTime Created { get; set; } 9 | public Guid OrderId { get; set; } 10 | public string UserId { get; set; } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /OrdersService/OrdersService.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | netcoreapp2.1 4 | OrdersService 5 | OrdersService 6 | docker-compose.dcproj 7 | 2.8 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /OrdersService/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore; 2 | using Microsoft.AspNetCore.Hosting; 3 | using Serilog; 4 | using Serilog.Events; 5 | using System; 6 | 7 | namespace OrdersService 8 | { 9 | public class Program 10 | { 11 | public static void Main(string[] args) 12 | { 13 | ConfigureLogging(); 14 | 15 | try 16 | { 17 | Log.Information("Starting web host"); 18 | 19 | BuildWebHost(args).Run(); 20 | } 21 | catch (Exception ex) 22 | { 23 | Log.Fatal(ex, "Host terminated unexpectedly"); 24 | } 25 | finally 26 | { 27 | Log.CloseAndFlush(); 28 | } 29 | 30 | Console.WriteLine("Press ENTER for final termination."); 31 | Console.ReadLine(); 32 | } 33 | 34 | public static IWebHost BuildWebHost(string[] args) => 35 | WebHost.CreateDefaultBuilder(args) 36 | .UseStartup() 37 | .UseSerilog() 38 | .Build(); 39 | 40 | private static void ConfigureLogging() 41 | { 42 | Log.Logger = new LoggerConfiguration() 43 | .MinimumLevel.Debug() 44 | .MinimumLevel.Override("Microsoft", LogEventLevel.Information) 45 | .Enrich.FromLogContext() 46 | .WriteTo.Console() 47 | //.WriteTo.Seq("http://localhost:5341") 48 | .CreateLogger(); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /OrdersService/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:53277/", 7 | "sslPort": 0 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": true, 14 | "launchUrl": "api/values", 15 | "environmentVariables": { 16 | "ASPNETCORE_ENVIRONMENT": "Development" 17 | } 18 | }, 19 | "OrdersService": { 20 | "commandName": "Project", 21 | "launchBrowser": true, 22 | "launchUrl": "swagger", 23 | "environmentVariables": { 24 | "ASPNETCORE_ENVIRONMENT": "Development" 25 | }, 26 | "applicationUrl": "http://localhost:53278/" 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /OrdersService/Startup.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using Consul; 3 | using EasyNetQ; 4 | using EasyNetQ.Topology; 5 | using IdentityServer4.AccessTokenValidation; 6 | using Microsoft.AspNetCore.Builder; 7 | using Microsoft.AspNetCore.Hosting; 8 | using Microsoft.AspNetCore.SignalR; 9 | using Microsoft.Extensions.Configuration; 10 | using Microsoft.Extensions.DependencyInjection; 11 | using OrdersService.Authentication; 12 | using OrdersService.Discovery; 13 | using OrdersService.Hubs; 14 | using OrdersService.Messages; 15 | using Polly; 16 | using Serilog; 17 | using Swashbuckle.AspNetCore.Swagger; 18 | using System; 19 | using System.Collections.Generic; 20 | using System.IdentityModel.Tokens.Jwt; 21 | 22 | namespace OrdersService 23 | { 24 | public class Startup 25 | { 26 | public static IConfiguration Configuration { get; set; } 27 | 28 | private IBus _bus; 29 | private IHubContext _ordersHubContext; 30 | 31 | public Startup(IConfiguration configuration) 32 | { 33 | Configuration = configuration; 34 | } 35 | 36 | public void ConfigureServices(IServiceCollection services) 37 | { 38 | services.AddOptions(); 39 | services.Configure(Configuration.GetSection("appSettings")); 40 | services.Configure(Configuration.GetSection("consulConfig")); 41 | 42 | services.AddSingleton(p => new ConsulClient(consulConfig => 43 | { 44 | var address = Configuration["consulConfig:address"]; 45 | consulConfig.Address = new Uri(address); 46 | })); 47 | services.AddSingleton(); 48 | 49 | services 50 | .AddMvc(); 51 | 52 | services.AddSwaggerGen(c => 53 | { 54 | c.SwaggerDoc("v1", new Info { Title = "Orders API", Version = "v1" }); 55 | }); 56 | 57 | services.AddSignalR(); 58 | 59 | //services.AddCors(); 60 | services.AddCors(o => o.AddPolicy("CorsPolicy", builder => 61 | { 62 | builder.AllowAnyOrigin() 63 | .AllowAnyMethod() 64 | .AllowAnyHeader() 65 | .AllowCredentials() 66 | .WithOrigins("http://localhost:4200"); 67 | })); 68 | 69 | services.AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme) 70 | .AddIdentityServerAuthentication(options => 71 | { 72 | options.Authority = Configuration["appSettings:idSrvBaseUrl"]; 73 | options.RequireHttpsMetadata = false; 74 | options.ApiName = "api"; 75 | options.SupportedTokens = SupportedTokens.Jwt; 76 | }); 77 | } 78 | 79 | public void Configure(IApplicationBuilder app, IHostingEnvironment env, IApplicationLifetime appLifetime) 80 | { 81 | appLifetime.ApplicationStarted.Register(OnStarted); 82 | appLifetime.ApplicationStopping.Register(OnStopping); 83 | appLifetime.ApplicationStopped.Register(OnStopped); 84 | 85 | Console.CancelKeyPress += (sender, eventArgs) => 86 | { 87 | appLifetime.StopApplication(); 88 | // Don't terminate the process immediately, wait for the Main thread to exit gracefully. 89 | eventArgs.Cancel = true; 90 | }; 91 | 92 | JwtSecurityTokenHandler.DefaultInboundClaimTypeMap = new Dictionary(); 93 | 94 | if (env.IsDevelopment()) 95 | { 96 | app.UseDeveloperExceptionPage(); 97 | } 98 | 99 | app.UseCors("CorsPolicy"); 100 | 101 | app.UseQueryStringAuthorization(); 102 | app.UseAuthentication(); 103 | 104 | app.UseMvc(); 105 | 106 | app.UseSwagger(); 107 | app.UseSwaggerUI(c => 108 | { 109 | c.SwaggerEndpoint("/swagger/v1/swagger.json", "Orders API V1"); 110 | }); 111 | 112 | app.UseSignalR(routes => 113 | { 114 | routes.MapHub("/ordersHub"); 115 | }); 116 | 117 | _ordersHubContext = app.ApplicationServices.GetService>(); 118 | } 119 | 120 | private void OnStarted() 121 | { 122 | InitializeMapper(); 123 | SetupQueues(); 124 | ListenOnQueues(); 125 | } 126 | 127 | private void OnStopping() 128 | { 129 | _bus?.Dispose(); 130 | } 131 | 132 | private void OnStopped() 133 | { 134 | } 135 | 136 | private static void SetupQueues() 137 | { 138 | var retryPolicy = Policy.Handle() 139 | .Retry(3, (exception, retryCount) => 140 | { 141 | Log.Warning($"Tried to connect to RMQ - {0} time(s) - reason: {1}", retryCount, exception); 142 | }); 143 | 144 | try 145 | { 146 | retryPolicy.Execute(() => 147 | { 148 | using (var advancedBus = RabbitHutch.CreateBus(Configuration["appSettings:rabbitMqConnectionString"]).Advanced) 149 | { 150 | var newOrderQueue = advancedBus.QueueDeclare("OrdersService.Messages.NewOrderMessage, OrdersService_shipping"); 151 | var newOrderExchange = advancedBus.ExchangeDeclare("OrdersService.Messages.NewOrderMessage, OrdersService", ExchangeType.Topic); 152 | advancedBus.Bind(newOrderExchange, newOrderQueue, String.Empty); 153 | 154 | var shippingCreatedQueue = advancedBus.QueueDeclare("OrdersService.Messages.ShippingCreatedMessage, OrdersService_shipping"); 155 | var shippingCreatedExchange = advancedBus.ExchangeDeclare("OrdersService.Messages.ShippingCreatedMessage, OrdersService", ExchangeType.Topic); 156 | advancedBus.Bind(shippingCreatedExchange, shippingCreatedQueue, String.Empty); 157 | } 158 | }); 159 | } 160 | catch (Exception e) 161 | { 162 | Log.Error($"Could not connect to queuing system - reason: {0}", e); 163 | throw; 164 | } 165 | } 166 | 167 | private void ListenOnQueues() 168 | { 169 | _bus = RabbitHutch.CreateBus(Configuration["appSettings:rabbitMqConnectionString"]); 170 | 171 | _bus.Subscribe("shipping", msg => 172 | { 173 | Log.Information("###Shipping created: " + msg.Created + " for " + msg.OrderId); 174 | 175 | _ordersHubContext.Clients.Group(msg.UserId).SendAsync("shippingCreated", msg.OrderId); 176 | }); 177 | } 178 | 179 | private static void InitializeMapper() 180 | { 181 | Mapper.Initialize(cfg => 182 | { 183 | cfg.CreateMap(); 184 | cfg.CreateMap() 185 | .ForMember(d => d.Items, opt => opt.MapFrom(s => s.Items)); 186 | }); 187 | Mapper.AssertConfigurationIsValid(); 188 | } 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /OrdersService/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "IncludeScopes": false, 4 | "LogLevel": { 5 | "Default": "Debug", 6 | "System": "Information", 7 | "Microsoft": "Information" 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /OrdersService/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "appSettings": { 3 | "rabbitMqConnectionString": "host=localhost", 4 | "seqBaseUrl": "http://localhost:5341", 5 | "idSrvBaseUrl": "https://tt-identityserver4-demo.azurewebsites.net" 6 | }, 7 | "consulConfig": { 8 | "address": "http://127.0.0.1:8500", 9 | "serviceName": "orders-service", 10 | "serviceID": "order-service-v1-final-01" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /OrdersService/docker-compose.override.yml: -------------------------------------------------------------------------------- 1 | version: '3.4' 2 | 3 | services: 4 | ordersservice: 5 | environment: 6 | - ASPNETCORE_ENVIRONMENT=Development 7 | ports: 8 | - "80" 9 | -------------------------------------------------------------------------------- /OrdersService/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.4' 2 | 3 | services: 4 | ordersservice: 5 | image: ${DOCKER_REGISTRY}ordersservice 6 | build: 7 | context: . 8 | dockerfile: Dockerfile 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pragmatic Microservices with .NET Core - without the fuzz and frameworks 2 | -------------------------------------------------------------------------------- /ShippingService/.deployment: -------------------------------------------------------------------------------- 1 | [config] 2 | command = bash deploy.sh -------------------------------------------------------------------------------- /ShippingService/DOCKERFILE: -------------------------------------------------------------------------------- 1 | FROM node:latest 2 | 3 | WORKDIR /usr/app 4 | COPY package.json . 5 | COPY package-lock.json . 6 | COPY tsconfig.json . 7 | 8 | RUN npm install 9 | 10 | COPY server.js . 11 | COPY app . 12 | 13 | RUN npm run build 14 | 15 | EXPOSE 3000 16 | 17 | CMD ["npm", "run", "start-server"] -------------------------------------------------------------------------------- /ShippingService/app/app.ts: -------------------------------------------------------------------------------- 1 | import * as Consul from "consul"; 2 | import RegisterOptions = Consul.Agent.Service.RegisterOptions; 3 | import Service = Consul.Agent.Service; 4 | 5 | import { IBusConfig, IConsumerDispose, RabbitHutch } from "easynodeq"; 6 | import { NewOrderMessage } from "./messages/newOrderMessage"; 7 | import { ShippingCreatedMessage } from "./messages/shippingCreatedMessage"; 8 | import * as uuid from "node-uuid"; 9 | 10 | import * as restify from "restify"; 11 | import * as corsMiddleware from "restify-cors-middleware"; 12 | import StatusController from "./controllers/statusController"; 13 | import { settings } from "./config/settings"; 14 | 15 | var nodeCleanup = require("node-cleanup"); 16 | 17 | // Consul service agent interaction 18 | let serviceId = "shipping-service-v2-final-01"; 19 | 20 | nodeCleanup(function(exitCode, signal) { 21 | console.log("Deregistering with Consul..."); 22 | service.deregister(serviceId, err => {}); 23 | }); 24 | 25 | let registerOptions: RegisterOptions = { 26 | name: "shipping-service", 27 | id: serviceId, 28 | address: "http://localhost", 29 | port: settings.port, 30 | check: { 31 | http: "http://localhost:" + settings.port + "/api/ping", 32 | interval: "30s" 33 | } 34 | }; 35 | 36 | let consul: Consul.Consul = new Consul(); 37 | let service: Consul.Agent.Service = consul.agent.service; 38 | 39 | console.log("Registering with Consul..."); 40 | service.register(registerOptions, data => { 41 | if (data) { 42 | console.log("From Consul: " + data); 43 | } 44 | }); 45 | 46 | // RabbitMQ subscriber 47 | let busConfig: IBusConfig = { 48 | heartbeat: 5, 49 | prefetch: 50, 50 | rpcTimeout: 10000, 51 | url: "amqp://localhost:5672", 52 | vhost: "" 53 | }; 54 | 55 | let bus = RabbitHutch.CreateBus(busConfig); 56 | bus.Subscribe(NewOrderMessage, "shipping", (message: NewOrderMessage) => { 57 | console.log("#Got an Order message:"); 58 | console.log(message); 59 | 60 | setTimeout(() => { 61 | var messageId = uuid.v4(); 62 | 63 | bus 64 | .Publish( 65 | new ShippingCreatedMessage( 66 | messageId, 67 | new Date(), 68 | message.Order.Id, 69 | message.UserId 70 | ) 71 | ) 72 | .then(success => 73 | console.log( 74 | `#Message ${messageId} was ${success ? "" : "not "}published` 75 | ) 76 | ); 77 | }, 5000); 78 | }); 79 | 80 | // Restify Web API 81 | export let server = restify.createServer({ 82 | name: settings.name 83 | }); 84 | 85 | const cors = corsMiddleware({ 86 | origins: ["*"], 87 | allowHeaders: ["*"] 88 | }); 89 | 90 | server.pre(cors.preflight); 91 | server.use(cors.actual); 92 | 93 | server.get("/api/ping", new StatusController().get); 94 | 95 | server.listen(settings.port, function() { 96 | console.log("Shipping Service running - listening at %s", server.url); 97 | }); 98 | -------------------------------------------------------------------------------- /ShippingService/app/config/config.ts: -------------------------------------------------------------------------------- 1 | export interface Config { 2 | name: string; 3 | port: number; 4 | version: string; 5 | } 6 | -------------------------------------------------------------------------------- /ShippingService/app/config/settings.ts: -------------------------------------------------------------------------------- 1 | import { Config } from "./config"; 2 | 3 | export let settings: Config = { 4 | name: "shipping-service", 5 | version: "1.0.0", 6 | port: +process.env.PORT || 3000 7 | }; 8 | -------------------------------------------------------------------------------- /ShippingService/app/controllers/statusController.ts: -------------------------------------------------------------------------------- 1 | import * as restify from "restify"; 2 | 3 | export default class StatusController { 4 | public get(req: restify.Request, res: restify.Response, next: restify.Next) { 5 | res.send(200, "OK"); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /ShippingService/app/messages/newOrderMessage.ts: -------------------------------------------------------------------------------- 1 | import { Order } from "./order"; 2 | 3 | export class NewOrderMessage { 4 | public static TypeID: string = 5 | "OrdersService.Messages.NewOrderMessage, OrdersService"; 6 | public TypeID: string = 7 | "OrdersService.Messages.NewOrderMessage, OrdersService"; 8 | 9 | constructor(public UserId: any, public Order: Order) {} 10 | } 11 | -------------------------------------------------------------------------------- /ShippingService/app/messages/order.ts: -------------------------------------------------------------------------------- 1 | export interface Order { 2 | Id: any; 3 | Created: Date; 4 | Items: any[]; 5 | } 6 | -------------------------------------------------------------------------------- /ShippingService/app/messages/shippingCreatedMessage.ts: -------------------------------------------------------------------------------- 1 | export class ShippingCreatedMessage { 2 | public static TypeID: string = 3 | "OrdersService.Messages.ShippingCreatedMessage, OrdersService"; 4 | public TypeID: string = 5 | "OrdersService.Messages.ShippingCreatedMessage, OrdersService"; 6 | 7 | constructor( 8 | public Id: any, 9 | public Created: Date, 10 | public OrderId: any, 11 | public UserId: any 12 | ) {} 13 | } 14 | -------------------------------------------------------------------------------- /ShippingService/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # ---------------------- 4 | # KUDU Deployment Script 5 | # Version: 1.0.9 6 | # ---------------------- 7 | 8 | # Helpers 9 | # ------- 10 | 11 | exitWithMessageOnError () { 12 | if [ ! $? -eq 0 ]; then 13 | echo "An error has occurred during web site deployment." 14 | echo $1 15 | exit 1 16 | fi 17 | } 18 | 19 | # Prerequisites 20 | # ------------- 21 | 22 | # Verify node.js installed 23 | hash node 2>/dev/null 24 | exitWithMessageOnError "Missing node.js executable, please install node.js, if already installed make sure it can be reached from current environment." 25 | 26 | # Setup 27 | # ----- 28 | 29 | SCRIPT_DIR="${BASH_SOURCE[0]%\\*}" 30 | SCRIPT_DIR="${SCRIPT_DIR%/*}" 31 | ARTIFACTS=$SCRIPT_DIR/../artifacts 32 | KUDU_SYNC_CMD=${KUDU_SYNC_CMD//\"} 33 | 34 | if [[ ! -n "$DEPLOYMENT_SOURCE" ]]; then 35 | DEPLOYMENT_SOURCE=$SCRIPT_DIR 36 | fi 37 | 38 | if [[ ! -n "$NEXT_MANIFEST_PATH" ]]; then 39 | NEXT_MANIFEST_PATH=$ARTIFACTS/manifest 40 | 41 | if [[ ! -n "$PREVIOUS_MANIFEST_PATH" ]]; then 42 | PREVIOUS_MANIFEST_PATH=$NEXT_MANIFEST_PATH 43 | fi 44 | fi 45 | 46 | if [[ ! -n "$DEPLOYMENT_TARGET" ]]; then 47 | DEPLOYMENT_TARGET=$ARTIFACTS/wwwroot 48 | else 49 | KUDU_SERVICE=true 50 | fi 51 | 52 | if [[ ! -n "$KUDU_SYNC_CMD" ]]; then 53 | # Install kudu sync 54 | echo Installing Kudu Sync 55 | npm install kudusync -g --silent 56 | exitWithMessageOnError "npm failed" 57 | 58 | if [[ ! -n "$KUDU_SERVICE" ]]; then 59 | # In case we are running locally this is the correct location of kuduSync 60 | KUDU_SYNC_CMD=kuduSync 61 | else 62 | # In case we are running on kudu service this is the correct location of kuduSync 63 | KUDU_SYNC_CMD=$APPDATA/npm/node_modules/kuduSync/bin/kuduSync 64 | fi 65 | fi 66 | 67 | # Node Helpers 68 | # ------------ 69 | 70 | selectNodeVersion () { 71 | if [[ -n "$KUDU_SELECT_NODE_VERSION_CMD" ]]; then 72 | SELECT_NODE_VERSION="$KUDU_SELECT_NODE_VERSION_CMD \"$DEPLOYMENT_SOURCE\" \"$DEPLOYMENT_TARGET\" \"$DEPLOYMENT_TEMP\"" 73 | eval $SELECT_NODE_VERSION 74 | exitWithMessageOnError "select node version failed" 75 | 76 | if [[ -e "$DEPLOYMENT_TEMP/__nodeVersion.tmp" ]]; then 77 | NODE_EXE=`cat "$DEPLOYMENT_TEMP/__nodeVersion.tmp"` 78 | exitWithMessageOnError "getting node version failed" 79 | fi 80 | 81 | if [[ -e "$DEPLOYMENT_TEMP/__npmVersion.tmp" ]]; then 82 | NPM_JS_PATH=`cat "$DEPLOYMENT_TEMP/__npmVersion.tmp"` 83 | exitWithMessageOnError "getting npm version failed" 84 | fi 85 | 86 | if [[ ! -n "$NODE_EXE" ]]; then 87 | NODE_EXE=node 88 | fi 89 | 90 | NPM_CMD="\"$NODE_EXE\" \"$NPM_JS_PATH\"" 91 | else 92 | NPM_CMD=npm 93 | NODE_EXE=node 94 | fi 95 | } 96 | 97 | ################################################################################################################################## 98 | # Deployment 99 | # ---------- 100 | 101 | echo Handling node.js deployment. 102 | 103 | # 1. KuduSync 104 | if [[ "$IN_PLACE_DEPLOYMENT" -ne "1" ]]; then 105 | "$KUDU_SYNC_CMD" -v 50 -f "$DEPLOYMENT_SOURCE" -t "$DEPLOYMENT_TARGET" -n "$NEXT_MANIFEST_PATH" -p "$PREVIOUS_MANIFEST_PATH" -i ".git;.hg;.deployment;deploy.sh" 106 | exitWithMessageOnError "Kudu Sync failed" 107 | fi 108 | 109 | # 2. Select node version 110 | selectNodeVersion 111 | 112 | # 3. Install npm packages 113 | if [ -e "$DEPLOYMENT_TARGET/package.json" ]; then 114 | cd "$DEPLOYMENT_TARGET" 115 | eval $NPM_CMD install --production 116 | exitWithMessageOnError "npm failed" 117 | cd - > /dev/null 118 | fi 119 | 120 | ################################################################################################################################## 121 | echo "Finished successfully." 122 | -------------------------------------------------------------------------------- /ShippingService/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "shipping-service", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "server.js", 6 | "author": "", 7 | "license": "MIT", 8 | "scripts": { 9 | "docker-build": "docker build -t shipping-service .", 10 | "docker-run": "docker run shipping-service", 11 | "run": "nodemon --exec ts-node app/app.ts", 12 | "start": "tsc", 13 | "build": "tsc", 14 | "start-server": "node server.js" 15 | }, 16 | "dependencies": { 17 | "consul": "^0.34.1", 18 | "easynodeq": "0.2.8", 19 | "node-cleanup": "^2.1.2", 20 | "uuid": "^3.3.2", 21 | "restify": "^7.3.0", 22 | "restify-cors-middleware": "^1.1.1" 23 | }, 24 | "devDependencies": { 25 | "@types/node": "^10.10.1", 26 | "nodemon": "^1.18.9", 27 | "ts-node": "^7.0.1", 28 | "typescript": "^2.9.2" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /ShippingService/server.js: -------------------------------------------------------------------------------- 1 | // This file is used for Azure web app hosting. 2 | // It must be present in this folder, so Azure can find and start it. 3 | 4 | "use strict"; 5 | 6 | require("./dist/app.js"); 7 | -------------------------------------------------------------------------------- /ShippingService/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2017", 4 | "module": "commonjs", 5 | "sourceMap": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "outDir": "dist" 9 | }, 10 | "exclude": [ 11 | "logs", 12 | "node_modules" 13 | ] 14 | } -------------------------------------------------------------------------------- /ShoppingClient/.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /ShoppingClient/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Thinktecture AG 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 | -------------------------------------------------------------------------------- /ShoppingClient/README.md: -------------------------------------------------------------------------------- 1 | # Simple Orders monitoring application - for Azure Serverless Microservices demo scenario -------------------------------------------------------------------------------- /ShoppingClient/angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "sample-app": { 7 | "root": "", 8 | "sourceRoot": "src", 9 | "projectType": "application", 10 | "architect": { 11 | "build": { 12 | "builder": "@angular-devkit/build-angular:browser", 13 | "options": { 14 | "outputPath": "dist", 15 | "index": "src/index.html", 16 | "main": "src/main.ts", 17 | "tsConfig": "src/tsconfig.app.json", 18 | "polyfills": "src/polyfills.ts", 19 | "assets": [ 20 | "src/assets", 21 | "src/favicon.ico", 22 | "src/cordova.js" 23 | ], 24 | "styles": [ 25 | "src/styles/global.scss", 26 | "node_modules/font-awesome/css/font-awesome.css" 27 | ], 28 | "scripts": [] 29 | }, 30 | "configurations": { 31 | "hmr": { 32 | "fileReplacements": [ 33 | { 34 | "replace": "src/environments/environment.ts", 35 | "with": "src/environments/environment.hmr.ts" 36 | } 37 | ] 38 | }, 39 | "production": { 40 | "optimization": true, 41 | "outputHashing": "all", 42 | "sourceMap": false, 43 | "extractCss": true, 44 | "namedChunks": false, 45 | "aot": true, 46 | "extractLicenses": true, 47 | "vendorChunk": false, 48 | "buildOptimizer": true, 49 | "fileReplacements": [ 50 | { 51 | "replace": "src/environments/environment.ts", 52 | "with": "src/environments/environment.prod.ts" 53 | } 54 | ] 55 | } 56 | } 57 | }, 58 | "serve": { 59 | "builder": "@angular-devkit/build-angular:dev-server", 60 | "options": { 61 | "browserTarget": "sample-app:build" 62 | }, 63 | "configurations": { 64 | "hmr": { 65 | "browserTarget": "sample-app:build:hmr" 66 | }, 67 | "production": { 68 | "browserTarget": "sample-app:build:production" 69 | } 70 | } 71 | }, 72 | "extract-i18n": { 73 | "builder": "@angular-devkit/build-angular:extract-i18n", 74 | "options": { 75 | "browserTarget": "sample-app:build" 76 | } 77 | }, 78 | "lint": { 79 | "builder": "@angular-devkit/build-angular:tslint", 80 | "options": { 81 | "tsConfig": [ 82 | "src/tsconfig.app.json", 83 | "src/tsconfig.spec.json" 84 | ], 85 | "exclude": [] 86 | } 87 | } 88 | } 89 | }, 90 | "sample-app-e2e": { 91 | "root": "e2e", 92 | "sourceRoot": "e2e", 93 | "projectType": "application" 94 | } 95 | }, 96 | "defaultProject": "sample-app", 97 | "schematics": { 98 | "@schematics/angular:component": { 99 | "prefix": "app", 100 | "styleext": "scss" 101 | }, 102 | "@schematics/angular:directive": { 103 | "prefix": "app" 104 | } 105 | } 106 | } -------------------------------------------------------------------------------- /ShoppingClient/buildAssets/desktop/appMenu.js: -------------------------------------------------------------------------------- 1 | const {Menu} = require('electron'); 2 | const electron = require('electron'); 3 | const app = electron.app; 4 | 5 | var exports = module.exports = {}; 6 | 7 | const template = [ 8 | { 9 | label: 'Edit', 10 | submenu: [ 11 | { 12 | role: 'undo' 13 | }, 14 | { 15 | role: 'redo' 16 | }, 17 | { 18 | type: 'separator' 19 | }, 20 | { 21 | role: 'cut' 22 | }, 23 | { 24 | role: 'copy' 25 | }, 26 | { 27 | role: 'paste' 28 | }, 29 | { 30 | role: 'pasteandmatchstyle' 31 | }, 32 | { 33 | role: 'delete' 34 | }, 35 | { 36 | role: 'selectall' 37 | } 38 | ] 39 | }, 40 | { 41 | label: 'View', 42 | submenu: [ 43 | { 44 | label: 'Toggle Developer Tools', 45 | accelerator: process.platform === 'darwin' ? 'Alt+Command+I' : 'Ctrl+Shift+I', 46 | click (item, focusedWindow) { 47 | if (focusedWindow) focusedWindow.webContents.toggleDevTools() 48 | } 49 | }, 50 | { 51 | type: 'separator' 52 | }, 53 | { 54 | role: 'resetzoom' 55 | }, 56 | { 57 | role: 'zoomin' 58 | }, 59 | { 60 | role: 'zoomout' 61 | }, 62 | { 63 | type: 'separator' 64 | }, 65 | { 66 | role: 'togglefullscreen' 67 | } 68 | ] 69 | }, 70 | { 71 | role: 'window', 72 | submenu: [ 73 | { 74 | role: 'minimize' 75 | }, 76 | { 77 | role: 'close' 78 | } 79 | ] 80 | }, 81 | { 82 | role: 'help', 83 | submenu: [ 84 | { 85 | label: 'Learn More', 86 | click () { require('electron').shell.openExternal('http://www.thinktecture.com') } 87 | } 88 | ] 89 | } 90 | ]; 91 | 92 | function buildMenu() { 93 | if (process.platform === 'darwin') { 94 | const name = app.getName(); 95 | 96 | template.unshift({ 97 | label: name, 98 | submenu: [ 99 | { 100 | role: 'about' 101 | }, 102 | { 103 | type: 'separator' 104 | }, 105 | { 106 | role: 'services', 107 | submenu: [] 108 | }, 109 | { 110 | type: 'separator' 111 | }, 112 | { 113 | role: 'hide' 114 | }, 115 | { 116 | role: 'hideothers' 117 | }, 118 | { 119 | role: 'unhide' 120 | }, 121 | { 122 | type: 'separator' 123 | }, 124 | { 125 | role: 'quit' 126 | } 127 | ] 128 | }); 129 | 130 | // Edit menu. 131 | template[1].submenu.push( 132 | { 133 | type: 'separator' 134 | }, 135 | { 136 | label: 'Speech', 137 | submenu: [ 138 | { 139 | role: 'startspeaking' 140 | }, 141 | { 142 | role: 'stopspeaking' 143 | } 144 | ] 145 | } 146 | ); 147 | 148 | // Window menu. 149 | template[3].submenu = [ 150 | { 151 | label: 'Close', 152 | accelerator: 'CmdOrCtrl+W', 153 | role: 'close' 154 | }, 155 | { 156 | label: 'Minimize', 157 | accelerator: 'CmdOrCtrl+M', 158 | role: 'minimize' 159 | }, 160 | { 161 | label: 'Zoom', 162 | role: 'zoom' 163 | }, 164 | { 165 | type: 'separator' 166 | }, 167 | { 168 | label: 'Bring All to Front', 169 | role: 'front' 170 | } 171 | ]; 172 | } 173 | 174 | const menu = Menu.buildFromTemplate(template); 175 | Menu.setApplicationMenu(menu); 176 | }; 177 | 178 | exports.buildMenu = buildMenu; 179 | -------------------------------------------------------------------------------- /ShoppingClient/buildAssets/desktop/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thinktecture/pragmatic-microservices-dotnetcore/881e43b4afd9ab17822238d1853ecf4d11567276/ShoppingClient/buildAssets/desktop/icon.png -------------------------------------------------------------------------------- /ShoppingClient/buildAssets/desktop/index.js: -------------------------------------------------------------------------------- 1 | const { app, BrowserWindow, globalShortcut, Menu, Tray } = require('electron'); 2 | const appMenu = require('./appMenu'); 3 | 4 | const path = require('path'); 5 | const url = require('url'); 6 | 7 | let win; 8 | let trayApp; 9 | 10 | function createWindow() { 11 | win = new BrowserWindow({ 12 | minWidth: 1080, 13 | minHeight: 600, 14 | width: 1080, 15 | height: 600 16 | }); 17 | 18 | win.loadURL(url.format({ 19 | pathname: path.join(__dirname, 'web/index.html'), 20 | protocol: 'file:', 21 | slashes: true 22 | })); 23 | 24 | buildTrayIcon(); 25 | appMenu.buildMenu(); 26 | 27 | globalShortcut.register('CmdOrCtrl+Shift+i', () => { 28 | win.webContents.toggleDevTools(); 29 | }); 30 | 31 | globalShortcut.register('CmdOrCtrl+Shift+b', function () { 32 | if (process.platform == 'darwin') { 33 | app.dock.bounce('critical'); 34 | } 35 | app.dock.setBadge('EVIL:)'); 36 | }); 37 | 38 | win.on('closed', () => { 39 | win = null; 40 | }); 41 | } 42 | 43 | app.on('ready', createWindow); 44 | 45 | app.on('window-all-closed', () => { 46 | globalShortcut.unregisterAll(); 47 | if (process.platform !== 'darwin') { 48 | app.quit(); 49 | } 50 | }); 51 | 52 | app.on('activate', () => { 53 | if (win === null) { 54 | createWindow(); 55 | } 56 | }); 57 | 58 | let buildTrayIcon = () => { 59 | let trayIconPath = path.join(__dirname, 'icon.png'); 60 | var contextMenu = Menu.buildFromTemplate([ 61 | { 62 | label: 'Pokemons...', 63 | type: 'normal', 64 | click: function () { 65 | win.webContents.send('navigateTo', 'pokemon/list/pokemon/1'); 66 | } 67 | }, 68 | { 69 | label: 'Quit', 70 | accelerator: 'Command+Q', 71 | selector: 'terminate:' 72 | } 73 | ]); 74 | 75 | trayApp = new Tray(trayIconPath); 76 | trayApp.setToolTip('ng Demo'); 77 | trayApp.setContextMenu(contextMenu); 78 | }; 79 | -------------------------------------------------------------------------------- /ShoppingClient/buildAssets/desktop/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "CrossPlatformDemo", 3 | "productName": "Cross-Platform Demo", 4 | "version": "1.0.0", 5 | "main": "index.js" 6 | } 7 | -------------------------------------------------------------------------------- /ShoppingClient/buildAssets/mobile/config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Cross-Platform Demo 4 | 5 | A simple Angular cross-platform sample app 6 | 7 | 8 | Thinktecture AG 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /ShoppingClient/buildAssets/resources/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thinktecture/pragmatic-microservices-dotnetcore/881e43b4afd9ab17822238d1853ecf4d11567276/ShoppingClient/buildAssets/resources/icon.icns -------------------------------------------------------------------------------- /ShoppingClient/buildAssets/resources/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thinktecture/pragmatic-microservices-dotnetcore/881e43b4afd9ab17822238d1853ecf4d11567276/ShoppingClient/buildAssets/resources/icon.ico -------------------------------------------------------------------------------- /ShoppingClient/buildAssets/resources/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thinktecture/pragmatic-microservices-dotnetcore/881e43b4afd9ab17822238d1853ecf4d11567276/ShoppingClient/buildAssets/resources/icon.png -------------------------------------------------------------------------------- /ShoppingClient/buildAssets/resources/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thinktecture/pragmatic-microservices-dotnetcore/881e43b4afd9ab17822238d1853ecf4d11567276/ShoppingClient/buildAssets/resources/splash.png -------------------------------------------------------------------------------- /ShoppingClient/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "shopping-client-app", 3 | "version": "1.0.0", 4 | "license": "MIT", 5 | "scripts": { 6 | "ng": "ng", 7 | "start": "ng serve", 8 | "hmr": "ng serve --hmr --e=hmr", 9 | "lint": "ng lint", 10 | "clean": "rimraf node_modules dist tmp && npm cache clean", 11 | "clean-install": "npm run clean && npm install", 12 | "clean-start": "npm run clean-install && npm start", 13 | "build": "rimraf dist && ng build --output-path=\"dist/web\" --base-href=\".\"", 14 | "prod": "rimraf dist && ng build --prod --aot=true --stats-json=true --output-path=\"dist/web\" --base-href=\".\"", 15 | "prepare-electron": "mkdirp ./tmp/web && ncp buildAssets/desktop tmp && ncp dist/web tmp/web", 16 | "package-desktop": "npm run prepare-electron && electron-packager ./tmp/ --all --asar --electron-version=1.6.2 --icon=\"buildAssets/resources/icon\" --out=./dist/desktop/ && rimraf tmp", 17 | "package-desktop-macos": "npm run prepare-electron && electron-packager ./tmp/ --platform=darwin --asar --electron-version=1.6.2 --icon=\"buildAssets/resources/icon\" --out=./dist/desktop/ && rimraf tmp", 18 | "build-desktop": "npm run build && npm run package-desktop", 19 | "build-desktop-macos": "npm run build && npm run package-desktop-macos", 20 | "prod-desktop": "npm run prod && npm run package-desktop", 21 | "generate-mobile-assets": "cd dist/mobile && cordova-icon --icon=\"../../buildAssets/resources/icon.png\" && cordova-splash --splash=\"../../buildAssets/resources/splash.png\"", 22 | "prepare-mobile": "mkdirp dist/mobile/www && ncp dist/web dist/mobile/www && ncp buildAssets/mobile dist/mobile", 23 | "package-mobile": "npm run prepare-mobile && cd dist/mobile && cordova prepare && npm run generate-mobile-assets && cordova build && cd ../..", 24 | "package-mobile-ios": "npm run prepare-mobile && cd dist/mobile && cordova prepare ios && npm run generate-mobile-assets && cordova build && cd ../..", 25 | "package-mobile-android": "npm run prepare-mobile && cd dist/mobile && cordova prepare android && npm run generate-mobile-assets && cordova build && cd ../..", 26 | "package-mobile-windows": "npm run prepare-mobile && cd dist/mobile && cordova prepare windows && npm run generate-mobile-assets && cordova build && cd ../..", 27 | "build-mobile": "npm run build && npm run package-mobile", 28 | "build-mobile-ios": "npm run build && npm run package-mobile-ios", 29 | "build-mobile-android": "npm run build && npm run package-mobile-android", 30 | "build-mobile-windows": "npm run build && npm run package-mobile-windows", 31 | "prod-mobile": "npm run prod && npm run package-mobile", 32 | "prod-mobile-ios": "npm run prod && npm run package-mobile-ios", 33 | "prod-mobile-android": "npm run prod && npm run package-mobile-android", 34 | "prod-mobile-windows": "npm run prod && npm run package-mobile-windows", 35 | "bundle-report": "webpack-bundle-analyzer dist/web/stats.json" 36 | }, 37 | "private": true, 38 | "dependencies": { 39 | "@angular/animations": "7.0.0", 40 | "@angular/common": "7.0.0", 41 | "@angular/compiler": "7.0.0", 42 | "@angular/core": "7.0.0", 43 | "@angular/forms": "7.0.0", 44 | "@angular/http": "7.0.0", 45 | "@angular/platform-browser": "7.0.0", 46 | "@angular/platform-browser-dynamic": "7.0.0", 47 | "@angular/platform-server": "7.0.0", 48 | "@angular/router": "7.0.0", 49 | "@angularclass/hmr": "1.2.2", 50 | "@aspnet/signalr": "1.0.4", 51 | "@ngx-progressbar/core": "^5.2.0", 52 | "@ngx-progressbar/http": "^5.2.0", 53 | "angular-oauth2-oidc": "^4.0.3", 54 | "core-js": "2.4.1", 55 | "font-awesome": "4.7.0", 56 | "include-media": "1.4.9", 57 | "ngx-electron": "0.0.11", 58 | "rxjs": "^6.3.3", 59 | "zone.js": "^0.8.26" 60 | }, 61 | "devDependencies": { 62 | "@angular-devkit/build-angular": "~0.10.0", 63 | "@angular/cli": "7.0.1", 64 | "@angular/compiler-cli": "7.0.0", 65 | "@types/cordova": "0.0.34", 66 | "@types/cordova-plugin-device": "0.0.3", 67 | "@types/electron": "^1.4.34", 68 | "@types/jasmine": "2.5.46", 69 | "@types/node": "7.0.10", 70 | "codelyzer": "4.5.0", 71 | "cordova": "6.5.0", 72 | "cordova-icon": "0.9.1", 73 | "cordova-splash": "0.9.0", 74 | "electron-packager": "8.6.0", 75 | "jasmine-core": "2.5.2", 76 | "jasmine-spec-reporter": "3.2.0", 77 | "karma": "1.5.0", 78 | "karma-chrome-launcher": "2.0.0", 79 | "karma-cli": "1.0.1", 80 | "karma-coverage-istanbul-reporter": "1.0.0", 81 | "karma-jasmine": "1.1.0", 82 | "karma-jasmine-html-reporter": "0.2.2", 83 | "mkdirp": "0.5.1", 84 | "ncp": "2.0.0", 85 | "protractor": "^5.4.1", 86 | "rimraf": "2.6.1", 87 | "ts-node": "3.0.2", 88 | "tslint": "4.5.1", 89 | "typescript": "3.1.3", 90 | "webpack-bundle-analyzer": "^3.3.2" 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /ShoppingClient/src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thinktecture/pragmatic-microservices-dotnetcore/881e43b4afd9ab17822238d1853ecf4d11567276/ShoppingClient/src/assets/.gitkeep -------------------------------------------------------------------------------- /ShoppingClient/src/assets/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 16 | 17 | -------------------------------------------------------------------------------- /ShoppingClient/src/cordova.js: -------------------------------------------------------------------------------- 1 | /* This file is intentionally here as a placeholder */ -------------------------------------------------------------------------------- /ShoppingClient/src/environments/environment.hmr.ts: -------------------------------------------------------------------------------- 1 | import { AuthConfig } from "angular-oauth2-oidc"; 2 | 3 | export const environment = { 4 | production: false, 5 | hmr: true, 6 | 7 | loginRoute: "/login", 8 | webApiBaseUrl: "http://localhost:53278/api/", 9 | signalRBaseUrl: "http://localhost:53278/" 10 | }; 11 | 12 | export const resourceOwnerConfig: AuthConfig = { 13 | issuer: "https://tt-identityserver4-demo.azurewebsites.net", 14 | clientId: "resourceowner", 15 | dummyClientSecret: "no-really-a-secret", 16 | scope: "openid profile email api", 17 | oidc: false 18 | }; 19 | -------------------------------------------------------------------------------- /ShoppingClient/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | import { AuthConfig } from "angular-oauth2-oidc"; 2 | 3 | export const environment = { 4 | production: true, 5 | hmr: false, 6 | 7 | loginRoute: "/login", 8 | webApiBaseUrl: "http://localhost:53278/api/", 9 | signalRBaseUrl: "http://localhost:53278/" 10 | }; 11 | 12 | export const resourceOwnerConfig: AuthConfig = { 13 | issuer: "https://tt-identityserver4-demo.azurewebsites.net", 14 | clientId: "resourceowner", 15 | dummyClientSecret: "no-really-a-secret", 16 | scope: "openid profile email api", 17 | oidc: false 18 | }; 19 | -------------------------------------------------------------------------------- /ShoppingClient/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | import { AuthConfig } from "angular-oauth2-oidc"; 2 | 3 | export const environment = { 4 | production: false, 5 | hmr: false, 6 | 7 | loginRoute: "/login", 8 | webApiBaseUrl: "http://localhost:53278/api/", 9 | signalRBaseUrl: "http://localhost:53278/" 10 | }; 11 | 12 | export const resourceOwnerConfig: AuthConfig = { 13 | issuer: "https://tt-identityserver4-demo.azurewebsites.net", 14 | clientId: "resourceowner", 15 | dummyClientSecret: "no-really-a-secret", 16 | scope: "openid profile email api", 17 | oidc: false 18 | }; 19 | -------------------------------------------------------------------------------- /ShoppingClient/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thinktecture/pragmatic-microservices-dotnetcore/881e43b4afd9ab17822238d1853ecf4d11567276/ShoppingClient/src/favicon.ico -------------------------------------------------------------------------------- /ShoppingClient/src/hmr.ts: -------------------------------------------------------------------------------- 1 | import {ApplicationRef, NgModuleRef} from '@angular/core'; 2 | import {createNewHosts} from '@angularclass/hmr'; 3 | 4 | export const hmrBootstrap = (module: any, bootstrap: () => Promise>) => { 5 | let ngModule: NgModuleRef; 6 | module.hot.accept(); 7 | bootstrap().then(mod => ngModule = mod); 8 | module.hot.dispose(() => { 9 | const appRef: ApplicationRef = ngModule.injector.get(ApplicationRef); 10 | const elements = appRef.components.map(c => c.location.nativeElement); 11 | const makeVisible = createNewHosts(elements); 12 | ngModule.destroy(); 13 | makeVisible(); 14 | }); 15 | }; 16 | -------------------------------------------------------------------------------- /ShoppingClient/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | MyShopping 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
    14 |
    15 |
    16 |
    17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /ShoppingClient/src/main.ts: -------------------------------------------------------------------------------- 1 | import {enableProdMode} from '@angular/core'; 2 | import {platformBrowserDynamic} from '@angular/platform-browser-dynamic'; 3 | 4 | import {environment} from './environments/environment'; 5 | import {hmrBootstrap} from './hmr'; 6 | import {AppModule} from './modules/app/module'; 7 | 8 | if (environment.production) { 9 | enableProdMode(); 10 | } 11 | 12 | const bootstrap = () => platformBrowserDynamic().bootstrapModule(AppModule); 13 | 14 | function start() { 15 | if (window.cordova) { 16 | return document.addEventListener('deviceready', bootstrap); 17 | } 18 | 19 | window.addEventListener('load', bootstrap); 20 | } 21 | 22 | if (environment.hmr) { 23 | if (module['hot']) { 24 | hmrBootstrap(module, bootstrap); 25 | } else { 26 | console.error('HMR is not enabled for webpack-dev-server!'); 27 | console.log('Are you using the --hmr flag for ng serve?'); 28 | } 29 | } else { 30 | start(); 31 | } 32 | -------------------------------------------------------------------------------- /ShoppingClient/src/modules/app/components/header/header.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | MyShopping 4 | 5 | 6 | 7 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /ShoppingClient/src/modules/app/components/header/header.scss: -------------------------------------------------------------------------------- 1 | @import '../../../../styles/_utilities'; 2 | 3 | :host { 4 | top: 0; 5 | left: 0; 6 | right: 0; 7 | position: fixed; 8 | display: flex; 9 | height: $header-height; 10 | 11 | background-color: $primary-color; 12 | 13 | align-items: center; 14 | 15 | padding: $gap/2; 16 | color: white; 17 | 18 | @include media('<=desktop') { 19 | justify-content: center; 20 | } 21 | } 22 | 23 | .brand { 24 | padding-right: $gap/2; 25 | } 26 | 27 | .brand-logo { 28 | height: $header-height - $gap; 29 | margin-left: auto; 30 | 31 | @include media('<=desktop') { 32 | margin-left: inherit; 33 | position: absolute; 34 | right: $gap/2; 35 | } 36 | } 37 | 38 | .back-chevron { 39 | background-color: transparent; 40 | color: white; 41 | position: absolute; 42 | left: $gap/2; 43 | border: none; 44 | outline: none; 45 | 46 | i { 47 | font-size: 20px; 48 | } 49 | 50 | @include media('>desktop') { 51 | display: none; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /ShoppingClient/src/modules/app/components/header/header.ts: -------------------------------------------------------------------------------- 1 | import { Component } from "@angular/core"; 2 | import { Location } from "@angular/common"; 3 | import { PlatformService } from "../../services/platformService"; 4 | import { Router } from "@angular/router"; 5 | import { OAuthService } from "angular-oauth2-oidc"; 6 | 7 | @Component({ 8 | selector: "app-header", 9 | templateUrl: "header.html", 10 | styleUrls: ["header.scss"] 11 | }) 12 | export class HeaderComponent { 13 | public isLoggedIn = false; 14 | 15 | public get isBackChevronVisible(): boolean { 16 | return this._location.path() !== "/home" && this._platform.isIOS; 17 | } 18 | 19 | constructor( 20 | private _location: Location, 21 | private _router: Router, 22 | private _platform: PlatformService, 23 | private _security: OAuthService 24 | ) { 25 | this.isLoggedIn = _security.hasValidAccessToken(); 26 | this._security.events.subscribe(e => { 27 | if (e.type === 'token_received') { 28 | this.isLoggedIn = true; 29 | } 30 | }); 31 | } 32 | 33 | public logout(): void { 34 | this.isLoggedIn = false; 35 | this._security.logOut(); 36 | this._router.navigate(["/home"]); 37 | } 38 | 39 | public goBack() { 40 | this._location.back(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /ShoppingClient/src/modules/app/components/home/home.html: -------------------------------------------------------------------------------- 1 |

    Home

    2 | 3 |

    MyShopping - welcome!
    -------------------------------------------------------------------------------- /ShoppingClient/src/modules/app/components/home/home.scss: -------------------------------------------------------------------------------- 1 | @import '../../../../styles/_utilities'; 2 | 3 | :host { 4 | padding-bottom: $menu-height + 5px; 5 | } 6 | -------------------------------------------------------------------------------- /ShoppingClient/src/modules/app/components/home/home.ts: -------------------------------------------------------------------------------- 1 | import {Component} from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-home', 5 | templateUrl: 'home.html', 6 | styleUrls: ['home.scss'] 7 | }) 8 | export class HomeComponent { 9 | 10 | } 11 | -------------------------------------------------------------------------------- /ShoppingClient/src/modules/app/components/list/orderList.html: -------------------------------------------------------------------------------- 1 |

    Orders

    2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
    DescriptionCreatedShipping
    {{ order.description }}{{ order.created | date: 'dd.MM.yyyy HH:mm' }}
    -------------------------------------------------------------------------------- /ShoppingClient/src/modules/app/components/list/orderList.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from "@angular/core"; 2 | import { OrdersService } from "../../services/ordersService"; 3 | import { PushService } from "../../services/pushService"; 4 | 5 | @Component({ 6 | selector: "app-order-list", 7 | templateUrl: "orderList.html" 8 | }) 9 | export class OrderListComponent implements OnInit { 10 | public orders = []; 11 | 12 | constructor( 13 | private _orderService: OrdersService, 14 | private _pushService: PushService 15 | ) { 16 | this._pushService.orderCreated.subscribe(_ => { 17 | this.loadOrders(); 18 | }); 19 | 20 | this._pushService.orderShipping.subscribe(orderId => { 21 | const index = this.orders.findIndex(order => order.id === orderId); 22 | 23 | if (index !== -1) { 24 | const updatedOrder = this.orders[index]; 25 | updatedOrder.shippingCreated = true; 26 | this.orders[index] = updatedOrder; 27 | } 28 | }); 29 | } 30 | 31 | private loadOrders() { 32 | this._orderService.getOrders().subscribe(data => { 33 | this.orders = data; 34 | }); 35 | } 36 | 37 | public ngOnInit(): void { 38 | this.loadOrders(); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /ShoppingClient/src/modules/app/components/login/login.html: -------------------------------------------------------------------------------- 1 | 15 | -------------------------------------------------------------------------------- /ShoppingClient/src/modules/app/components/login/login.scss: -------------------------------------------------------------------------------- 1 | .login-form { 2 | background-color: #fff; 3 | 4 | min-height: 250px; 5 | width: 90%; 6 | max-width: 400px; 7 | 8 | display: flex; 9 | box-shadow: 0 10px 20px 0 rgba(0,0,0, .1); 10 | margin: 0 auto; 11 | } 12 | 13 | form { 14 | width: 100%; 15 | 16 | display: flex; 17 | flex-flow: row wrap; 18 | padding: 10px 20px; 19 | 20 | legend { 21 | font-size: 24px; 22 | flex: 0 0 100%; 23 | } 24 | 25 | fieldset { 26 | border: none; 27 | flex: 0 0 100%; 28 | padding: 20px 0; 29 | } 30 | 31 | input { 32 | min-width: 100%; 33 | 34 | font-size: 15px; 35 | 36 | border: 1px solid rgba(0,0,0, .1); 37 | border-radius: 5px; 38 | margin: 5px 0; 39 | padding: 7px 10px; 40 | transition: .3s all; 41 | will-change: border-color, box-shadow; 42 | 43 | &:focus { 44 | border-color: #ff5850; 45 | box-shadow: 0 3px 7px 0 rgba(0,0,0, .12); 46 | outline: none; 47 | } 48 | } 49 | 50 | button { 51 | background-color: #ff5850; 52 | 53 | font-size: 15px; 54 | line-height: 20px; 55 | color: white; 56 | 57 | align-self: flex-end; 58 | border: 1px solid transparent; 59 | border-radius: 30px; 60 | cursor: pointer; 61 | margin: 0 0 15px auto; 62 | padding: 7px 25px; 63 | transition: .3s all; 64 | will-change: box-shadow, border-color, background-color; 65 | 66 | &:focus, &:hover { 67 | background-color: darken(#ff5850, 5%); 68 | border-color: lighten(#ff5850, 5%); 69 | box-shadow: 0 3px 7px 0 rgba(0,0,0, .12); 70 | outline: none; 71 | } 72 | } 73 | } 74 | 75 | 76 | -------------------------------------------------------------------------------- /ShoppingClient/src/modules/app/components/login/login.ts: -------------------------------------------------------------------------------- 1 | import { Component, HostBinding } from "@angular/core"; 2 | import { ActivatedRoute, Router } from "@angular/router"; 3 | import { PushService } from "../../services/pushService"; 4 | import { OAuthService } from "angular-oauth2-oidc"; 5 | 6 | @Component({ 7 | selector: "app-security-login", 8 | styleUrls: ["login.scss"], 9 | templateUrl: "login.html" 10 | }) 11 | export class LoginComponent { 12 | @HostBinding("class.box") 13 | public loginCssClass = true; 14 | 15 | public username: string; 16 | public password: string; 17 | public error: string; 18 | 19 | constructor( 20 | private _securityService: OAuthService, 21 | private _activatedRoute: ActivatedRoute, 22 | private _router: Router, 23 | private _pushService: PushService 24 | ) {} 25 | 26 | public submit(): void { 27 | this._securityService 28 | .fetchTokenUsingPasswordFlowAndLoadUserProfile( 29 | this.username, 30 | this.password 31 | ) 32 | .then( 33 | () => { 34 | this._pushService.start(); 35 | this._router.navigate([ 36 | this._activatedRoute.snapshot.queryParams["redirectTo"] 37 | ]); 38 | }, 39 | error => (this.error = error) 40 | ); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /ShoppingClient/src/modules/app/components/menu/menu.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ShoppingClient/src/modules/app/components/menu/menu.scss: -------------------------------------------------------------------------------- 1 | @import '../../../../styles/_utilities'; 2 | 3 | :host { 4 | display: flex; 5 | flex-direction: column; 6 | z-index: 100; 7 | 8 | @include media('<=desktop') { 9 | position: fixed; 10 | left: 0; 11 | bottom: 0; 12 | width: $menu-width + $submenu-width; 13 | justify-content: center; 14 | 15 | background-color: $background-color; 16 | 17 | transition: transform $transition-duration; 18 | will-change: transform; 19 | 20 | @include media('>tablet') { 21 | top: $header-height; 22 | transform: translateX(-$submenu-width); 23 | border-right: 1px solid darken($background-color, 10%); 24 | } 25 | } 26 | 27 | @include media('<=tablet') { 28 | width: 100vw; 29 | height: $menu-height + $submenu-height; 30 | align-items: center; 31 | justify-content: flex-start; 32 | transform: translateY($submenu-height); 33 | border-top: 1px solid darken($background-color, 10%); 34 | } 35 | } 36 | 37 | .fa { 38 | padding-right: 6px; 39 | } 40 | 41 | .menu { 42 | list-style-type: none; 43 | margin: 0; 44 | padding: 0; 45 | 46 | display: flex; 47 | 48 | @include media('<=desktop', '>tablet') { 49 | flex-direction: column; 50 | width: $menu-width; 51 | align-self: flex-end; 52 | } 53 | 54 | @include media('<=tablet') { 55 | flex-direction: row; 56 | height: $menu-height; 57 | width: 100%; 58 | } 59 | } 60 | 61 | .dropdown-menu { 62 | display: none; 63 | position: absolute; 64 | flex-direction: column; 65 | top: 100%; 66 | 67 | background-color: $background-color; 68 | border-left: 1px solid $primary-color; 69 | border-right: 1px solid $primary-color; 70 | border-bottom: 1px solid $primary-color; 71 | 72 | border-bottom-left-radius: $border-radius; 73 | border-bottom-right-radius: $border-radius; 74 | 75 | width: 100%; 76 | 77 | .menu-link { 78 | color: black; 79 | } 80 | 81 | @include media('>desktop') { 82 | width: inherit; 83 | min-width: 100%; 84 | } 85 | 86 | @include media('<=desktop') { 87 | top: 0; 88 | left: 0; 89 | width: $submenu-width; 90 | bottom: 0; 91 | border: none; 92 | background-color: $primary-color; 93 | justify-content: center; 94 | 95 | border-radius: 0; 96 | 97 | .menu-link { 98 | color: white; 99 | justify-content: center; 100 | height: inherit; 101 | 102 | .fa { 103 | font-size: inherit; 104 | } 105 | } 106 | 107 | .menu-item { 108 | &.active { 109 | background-color: #E54F47; 110 | } 111 | } 112 | } 113 | 114 | @include media('<=tablet') { 115 | top: $menu-height; 116 | width: 100%; 117 | height: $submenu-height; 118 | justify-content: flex-start; 119 | } 120 | } 121 | 122 | .menu-item { 123 | cursor: pointer; 124 | position: relative; 125 | display: flex; 126 | 127 | &.show { 128 | .dropdown-menu { 129 | display: inherit; 130 | } 131 | 132 | @include media('<=desktop') { 133 | background-color: $primary-color; 134 | 135 | .menu-link { 136 | color: white; 137 | } 138 | } 139 | } 140 | 141 | @include media('<=desktop') { 142 | position: static; 143 | } 144 | 145 | &.active { 146 | @include media('<=tablet') { 147 | box-shadow: 0 -5px 0 0 $primary-color inset; 148 | } 149 | 150 | @include media('<=desktop', '>tablet') { 151 | box-shadow: 5px 0 0 0 $primary-color inset; 152 | } 153 | 154 | @include media('>desktop') { 155 | background-color: $background-color; 156 | 157 | > .menu-link { 158 | color: $primary-color; 159 | } 160 | 161 | .dropdown-menu { 162 | .menu-item { 163 | background-color: transparent; 164 | } 165 | } 166 | } 167 | } 168 | } 169 | 170 | .menu-link { 171 | color: white; 172 | text-decoration: none; 173 | display: flex; 174 | align-items: center; 175 | width: 100%; 176 | 177 | padding: $gap/2; 178 | 179 | @include media('>desktop') { 180 | height: $header-height; 181 | } 182 | 183 | @include media('<=desktop') { 184 | color: black; 185 | flex-wrap: wrap; 186 | justify-content: center; 187 | 188 | .fa { 189 | font-size: 2em; 190 | } 191 | } 192 | } 193 | 194 | .menu:not(.dropdown-menu) { 195 | @include media('<=desktop') { 196 | > .menu-item { 197 | > .menu-link { 198 | .fa { 199 | padding: 0; 200 | } 201 | } 202 | } 203 | } 204 | 205 | @include media('<=tablet') { 206 | > .menu-item { 207 | flex: 1; 208 | 209 | > .menu-link { 210 | .fa { 211 | width: 100%; 212 | text-align: center; 213 | padding: 0; 214 | } 215 | 216 | font-size: 0.8em; 217 | } 218 | } 219 | } 220 | } 221 | -------------------------------------------------------------------------------- /ShoppingClient/src/modules/app/components/menu/menu.ts: -------------------------------------------------------------------------------- 1 | import { Component } from "@angular/core"; 2 | import { WindowRef } from "../../services/windowRef"; 3 | 4 | @Component({ 5 | selector: "app-menu", 6 | templateUrl: "menu.html", 7 | styleUrls: ["menu.scss"] 8 | }) 9 | export class MenuComponent { 10 | private readonly _bodyCssClass = "show-menu"; 11 | 12 | constructor(private _windowRef: WindowRef) {} 13 | } 14 | -------------------------------------------------------------------------------- /ShoppingClient/src/modules/app/components/root/root.html: -------------------------------------------------------------------------------- 1 |
    2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /ShoppingClient/src/modules/app/components/root/root.scss: -------------------------------------------------------------------------------- 1 | @import '../../../../styles/_variables'; 2 | 3 | :host { 4 | display: flex; 5 | flex: 1; 6 | flex-direction: column; 7 | 8 | padding: $gap; 9 | } 10 | -------------------------------------------------------------------------------- /ShoppingClient/src/modules/app/components/root/root.ts: -------------------------------------------------------------------------------- 1 | import { Component } from "@angular/core"; 2 | import { DesktopIntegrationService } from "../../services/desktopIntegrationService"; 3 | import { PushService } from "../../services/pushService"; 4 | import { OAuthService } from "angular-oauth2-oidc"; 5 | import { resourceOwnerConfig } from "../../../../environments/environment"; 6 | 7 | @Component({ 8 | selector: "app-root", 9 | templateUrl: "root.html", 10 | styleUrls: ["root.scss"] 11 | }) 12 | export class RootComponent { 13 | constructor( 14 | private _securityService: OAuthService, 15 | private _pushService: PushService, 16 | private _desktopIntegration: DesktopIntegrationService 17 | ) { 18 | this.initOAuth(); 19 | 20 | this._desktopIntegration.register(); 21 | } 22 | 23 | private initOAuth() { 24 | this._securityService.setStorage(localStorage); 25 | this._securityService.configure(resourceOwnerConfig); 26 | this._securityService.loadDiscoveryDocument(); 27 | this._securityService.tryLogin().then(() => { 28 | this._pushService.start(); 29 | }); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /ShoppingClient/src/modules/app/guards/isAuthenticated.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CanActivate, 3 | ActivatedRouteSnapshot, 4 | RouterStateSnapshot, 5 | Router 6 | } from "@angular/router"; 7 | import { Injectable } from "@angular/core"; 8 | import { OAuthService } from "angular-oauth2-oidc"; 9 | import { environment } from "../../../environments/environment"; 10 | import { Observable } from "rxjs"; 11 | 12 | @Injectable() 13 | export class IsAuthenticated implements CanActivate { 14 | constructor(private _oauthService: OAuthService, private _router: Router) { 15 | if (!environment.loginRoute) { 16 | throw new Error("Login route has not been configured."); 17 | } 18 | } 19 | 20 | public canActivate( 21 | route: ActivatedRouteSnapshot, 22 | state: RouterStateSnapshot 23 | ): Observable | Promise | boolean { 24 | const result = this._oauthService.hasValidAccessToken(); 25 | 26 | if (!result) { 27 | this._router.navigate([environment.loginRoute], { 28 | queryParams: { 29 | redirectTo: state.url 30 | } 31 | }); 32 | 33 | return false; 34 | } 35 | 36 | return true; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /ShoppingClient/src/modules/app/module.ts: -------------------------------------------------------------------------------- 1 | import { AuthInterceptor } from "./services/authInterceptor"; 2 | import { OAuthService, OAuthModule } from "angular-oauth2-oidc"; 3 | import { BrowserModule } from "@angular/platform-browser"; 4 | import { NgModule } from "@angular/core"; 5 | import { FormsModule } from "@angular/forms"; 6 | import { RouterModule } from "@angular/router"; 7 | 8 | import { RootComponent } from "./components/root/root"; 9 | import { ROUTES } from "./routes"; 10 | import { HomeComponent } from "./components/home/home"; 11 | import { HeaderComponent } from "./components/header/header"; 12 | import { MenuComponent } from "./components/menu/menu"; 13 | import { WindowRef } from "./services/windowRef"; 14 | import { PlatformService } from "./services/platformService"; 15 | import { NgxElectronModule } from "ngx-electron"; 16 | import { DesktopIntegrationService } from "./services/desktopIntegrationService"; 17 | import { LoginComponent } from "./components/login/login"; 18 | import { IsAuthenticated } from "./guards/isAuthenticated"; 19 | import { OrdersService } from "./services/ordersService"; 20 | import { OrderListComponent } from "./components/list/orderList"; 21 | import { PushService } from "./services/pushService"; 22 | import { HttpClientModule, HTTP_INTERCEPTORS } from "@angular/common/http"; 23 | import { NgProgressModule } from "@ngx-progressbar/core"; 24 | import { NgProgressHttpModule } from "@ngx-progressbar/http"; 25 | 26 | @NgModule({ 27 | declarations: [ 28 | RootComponent, 29 | LoginComponent, 30 | HomeComponent, 31 | HeaderComponent, 32 | MenuComponent, 33 | OrderListComponent 34 | ], 35 | imports: [ 36 | BrowserModule, 37 | FormsModule, 38 | HttpClientModule, 39 | RouterModule.forRoot(ROUTES, { useHash: true }), 40 | NgProgressModule.forRoot(), 41 | NgProgressHttpModule.forRoot(), 42 | NgxElectronModule, 43 | OAuthModule.forRoot() 44 | ], 45 | bootstrap: [RootComponent], 46 | providers: [ 47 | WindowRef, 48 | OAuthService, 49 | OrdersService, 50 | PlatformService, 51 | PushService, 52 | DesktopIntegrationService, 53 | { 54 | provide: HTTP_INTERCEPTORS, 55 | useClass: AuthInterceptor, 56 | multi: true 57 | }, 58 | IsAuthenticated 59 | ] 60 | }) 61 | export class AppModule {} 62 | -------------------------------------------------------------------------------- /ShoppingClient/src/modules/app/routes.ts: -------------------------------------------------------------------------------- 1 | import {Routes} from '@angular/router'; 2 | import {HomeComponent} from './components/home/home'; 3 | import {LoginComponent} from './components/login/login'; 4 | import {IsAuthenticated} from './guards/isAuthenticated'; 5 | import {OrderListComponent} from './components/list/orderList'; 6 | 7 | export const ROUTES: Routes = [ 8 | { 9 | path: 'login', 10 | component: LoginComponent 11 | }, 12 | { 13 | path: '', 14 | pathMatch: 'full', 15 | redirectTo: '/home' 16 | }, 17 | { 18 | path: 'home', 19 | component: HomeComponent 20 | }, 21 | { 22 | path: 'orders', 23 | canActivate: [IsAuthenticated], 24 | children: [ 25 | { 26 | path: 'list', 27 | component: OrderListComponent 28 | } 29 | ] 30 | } 31 | ]; 32 | -------------------------------------------------------------------------------- /ShoppingClient/src/modules/app/services/authInterceptor.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Injector } from "@angular/core"; 2 | import { 3 | HttpEvent, 4 | HttpHandler, 5 | HttpInterceptor, 6 | HttpRequest 7 | } from "@angular/common/http"; 8 | import { OAuthService } from "angular-oauth2-oidc"; 9 | import { Observable } from "rxjs"; 10 | 11 | @Injectable() 12 | export class AuthInterceptor implements HttpInterceptor { 13 | private _securityService: OAuthService; 14 | 15 | constructor(private injector: Injector) {} 16 | 17 | intercept( 18 | req: HttpRequest, 19 | next: HttpHandler 20 | ): Observable> { 21 | let requestToForward = req; 22 | 23 | if (this._securityService === undefined) { 24 | this._securityService = this.injector.get(OAuthService); 25 | } 26 | 27 | if (this._securityService !== undefined) { 28 | const token = this._securityService.getAccessToken(); 29 | 30 | if (token !== "") { 31 | const tokenValue = "Bearer " + token; 32 | requestToForward = req.clone({ 33 | setHeaders: { Authorization: tokenValue } 34 | }); 35 | } 36 | } else { 37 | // tslint:disable-next-line:no-console 38 | console.debug("OidcSecurityService undefined: NO auth header!"); 39 | } 40 | 41 | return next.handle(requestToForward); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /ShoppingClient/src/modules/app/services/desktopIntegrationService.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, NgZone } from "@angular/core"; 2 | import { ElectronService } from "ngx-electron"; 3 | import { Router } from "@angular/router"; 4 | import { PlatformService } from "./platformService"; 5 | 6 | @Injectable() 7 | export class DesktopIntegrationService { 8 | constructor( 9 | private _platform: PlatformService, 10 | private _electronService: ElectronService, 11 | private _zone: NgZone, 12 | private _router: Router 13 | ) {} 14 | 15 | public register() { 16 | if (this._platform.isElectron) { 17 | this._electronService.ipcRenderer.on("navigateTo", (event, data) => { 18 | this._zone.run(() => { 19 | this._router.navigate([data]); 20 | }); 21 | }); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /ShoppingClient/src/modules/app/services/ordersService.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from "@angular/core"; 2 | import { environment } from "../../../environments/environment"; 3 | import { HttpClient } from "@angular/common/http"; 4 | 5 | @Injectable() 6 | export class OrdersService { 7 | constructor(private _http: HttpClient) {} 8 | 9 | public getOrders() { 10 | return this._http.get(environment.webApiBaseUrl + "orders"); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /ShoppingClient/src/modules/app/services/platformService.ts: -------------------------------------------------------------------------------- 1 | export class PlatformService { 2 | private _iOS: boolean; 3 | private _isAndroid: boolean; 4 | private _isElectron: boolean; 5 | 6 | public get isMobileDevice(): boolean { 7 | return this._iOS || this._isAndroid; 8 | } 9 | 10 | public get isMobileWeb(): boolean { 11 | return window.innerWidth <= 768; 12 | } 13 | 14 | public get isWeb(): boolean { 15 | return !this.isMobileDevice; 16 | } 17 | 18 | public get isIOS(): boolean { 19 | return this._iOS; 20 | } 21 | 22 | public get isAndroid(): boolean { 23 | return this._isAndroid; 24 | } 25 | 26 | public get isElectron(): boolean { 27 | return this._isElectron; 28 | } 29 | 30 | constructor() { 31 | this.guessPlatform(); 32 | } 33 | 34 | private guessPlatform(): void { 35 | this._iOS = window.cordova && window.cordova.platformId === 'ios'; 36 | this._isAndroid = window.cordova && window.cordova.platformId === 'android'; 37 | this._isElectron = window.navigator.userAgent.match(/Electron/) !== null; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /ShoppingClient/src/modules/app/services/pushService.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from "@angular/core"; 2 | import { environment } from "../../../environments/environment"; 3 | import { BehaviorSubject } from "rxjs"; 4 | import { HubConnection, HubConnectionBuilder, LogLevel } from "@aspnet/signalr"; 5 | import { OAuthService } from "angular-oauth2-oidc"; 6 | 7 | @Injectable() 8 | export class PushService { 9 | private _hubConnection: HubConnection; 10 | 11 | public orderShipping: BehaviorSubject = new BehaviorSubject(null); 12 | public orderCreated: BehaviorSubject = new BehaviorSubject(null); 13 | 14 | constructor(private _securityService: OAuthService) {} 15 | 16 | public start(): void { 17 | this._hubConnection = new HubConnectionBuilder() 18 | .withUrl( 19 | environment.signalRBaseUrl + 20 | "ordersHub" + 21 | "?authorization=" + 22 | this._securityService.getAccessToken() 23 | ) 24 | .configureLogging(LogLevel.Information) 25 | .build(); 26 | 27 | this._hubConnection.on("orderCreated", () => { 28 | this.orderCreated.next(null); 29 | }); 30 | 31 | this._hubConnection.on("shippingCreated", orderId => { 32 | this.orderShipping.next(orderId); 33 | }); 34 | 35 | this._hubConnection 36 | .start() 37 | .then(() => console.log("SignalR connection established.")) 38 | .catch(err => 39 | console.error("SignalR connection not established. " + err) 40 | ); 41 | } 42 | 43 | public stop(): void { 44 | if (this._hubConnection) { 45 | this._hubConnection.stop(); 46 | } 47 | 48 | this._hubConnection = undefined; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /ShoppingClient/src/modules/app/services/windowRef.ts: -------------------------------------------------------------------------------- 1 | export class WindowRef { 2 | public get nativeWindow(): Window { 3 | return window; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /ShoppingClient/src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html 15 | */ 16 | /*************************************************************************************************** 17 | * BROWSER POLYFILLS 18 | */ 19 | /** IE9, IE10 and IE11 requires all of the following polyfills. **/ 20 | // import 'core-js/es6/symbol'; 21 | // import 'core-js/es6/object'; 22 | // import 'core-js/es6/function'; 23 | // import 'core-js/es6/parse-int'; 24 | // import 'core-js/es6/parse-float'; 25 | // import 'core-js/es6/number'; 26 | // import 'core-js/es6/math'; 27 | // import 'core-js/es6/string'; 28 | // import 'core-js/es6/date'; 29 | // import 'core-js/es6/array'; 30 | // import 'core-js/es6/regexp'; 31 | // import 'core-js/es6/map'; 32 | // import 'core-js/es6/set'; 33 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 34 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 35 | /** IE10 and IE11 requires the following to support `@angular/animation`. */ 36 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 37 | /** Evergreen browsers require these. **/ 38 | import 'core-js/es6/reflect'; 39 | 40 | /** ALL Firefox browsers require the following to support `@angular/animation`. **/ 41 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 42 | /*************************************************************************************************** 43 | * Zone JS is required by Angular itself. 44 | */ 45 | import 'zone.js/dist/zone'; // Included with Angular CLI. 46 | 47 | 48 | /*************************************************************************************************** 49 | * APPLICATION IMPORTS 50 | */ 51 | 52 | /** 53 | * Date, currency, decimal and percent pipes. 54 | * Needed for: All but Chrome, Firefox, Edge, IE11 and Safari 10 55 | */ 56 | // import 'intl'; // Run `npm install --save intl`. 57 | -------------------------------------------------------------------------------- /ShoppingClient/src/styles/_backdrop.scss: -------------------------------------------------------------------------------- 1 | @import '_utilities'; 2 | 3 | .backdrop { 4 | position: fixed; 5 | top: $header-height; 6 | left: 0; 7 | right: 0; 8 | bottom: 0; 9 | background-color: black; 10 | pointer-events: none; 11 | 12 | opacity: 0; 13 | 14 | will-change: opacity; 15 | transition: opacity $transition-duration; 16 | } 17 | -------------------------------------------------------------------------------- /ShoppingClient/src/styles/_links.scss: -------------------------------------------------------------------------------- 1 | @import '_variables.scss'; 2 | 3 | router-outlet + * { 4 | a { 5 | color: $primary-color; 6 | text-decoration: none; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /ShoppingClient/src/styles/_loader.scss: -------------------------------------------------------------------------------- 1 | @import '_utilities'; 2 | 3 | .loader-container { 4 | position: fixed; 5 | display: flex; 6 | align-items: center; 7 | top: 0; 8 | bottom: 0; 9 | left: 0; 10 | right: 0; 11 | } 12 | 13 | .loader, 14 | .loader:after { 15 | border-radius: 50%; 16 | width: 10em; 17 | height: 10em; 18 | } 19 | 20 | .loader { 21 | margin: 60px auto; 22 | font-size: 10px; 23 | position: relative; 24 | text-indent: -9999em; 25 | border-top: 1.1em solid $primary-color; 26 | border-right: 1.1em solid $primary-color; 27 | border-bottom: 1.1em solid $primary-color; 28 | border-left: 1.1em solid #dddddd; 29 | transform: translateZ(0); 30 | animation: load8 1.1s infinite linear; 31 | } 32 | 33 | @keyframes load8 { 34 | 0% { 35 | -webkit-transform: rotate(0deg); 36 | transform: rotate(0deg); 37 | } 38 | 100% { 39 | -webkit-transform: rotate(360deg); 40 | transform: rotate(360deg); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /ShoppingClient/src/styles/_menu.scss: -------------------------------------------------------------------------------- 1 | @import '_utilities'; 2 | 3 | html { 4 | body { 5 | @include media('<=desktop') { 6 | @include media('>tablet') { 7 | padding-left: $menu-width; 8 | } 9 | 10 | &.show-menu { 11 | app-menu { 12 | transform: none; 13 | 14 | > .menu { 15 | > .menu-item { 16 | &.active:not(.show) { 17 | box-shadow: none; 18 | background-color: lighten($alternative-background-color, 10%); 19 | 20 | .menu-link { 21 | color: white; 22 | } 23 | } 24 | } 25 | } 26 | } 27 | 28 | .backdrop { 29 | opacity: 0.75; 30 | } 31 | } 32 | } 33 | 34 | @include media('<=tablet') { 35 | padding-bottom: $menu-height; 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /ShoppingClient/src/styles/_table.scss: -------------------------------------------------------------------------------- 1 | @import '_variables'; 2 | 3 | .table { 4 | width: 100%; 5 | 6 | border: 0; 7 | border-spacing: 0; 8 | 9 | td, th { 10 | padding: $gap/4; 11 | } 12 | 13 | thead { 14 | tr { 15 | background-color: $primary-color; 16 | 17 | th { 18 | text-align: left; 19 | color: white; 20 | font-weight: bold; 21 | 22 | &:first-child { 23 | width: 20%; 24 | } 25 | } 26 | } 27 | } 28 | 29 | tbody { 30 | tr { 31 | &:nth-child(even) { 32 | background-color: darken($background-color, 3%); 33 | } 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /ShoppingClient/src/styles/_utilities.scss: -------------------------------------------------------------------------------- 1 | @import '_variables'; 2 | // For Breakpoints 3 | @import '~include-media/dist/_include-media'; 4 | -------------------------------------------------------------------------------- /ShoppingClient/src/styles/_variables.scss: -------------------------------------------------------------------------------- 1 | $primary-color: #FF584F; 2 | $background-color: #F9F8F6; 3 | $alternative-background-color: #868686; 4 | 5 | $header-height: 50px; 6 | 7 | $gap: 25px; 8 | 9 | $border-radius: 6px; 10 | 11 | $menu-width: 80px; 12 | $submenu-width: 200px; 13 | 14 | $menu-height: 65px; 15 | $submenu-height: 180px; 16 | 17 | $transition-duration: 0.3s; 18 | -------------------------------------------------------------------------------- /ShoppingClient/src/styles/global.scss: -------------------------------------------------------------------------------- 1 | @import '_variables.scss'; 2 | @import '_menu.scss'; 3 | @import '_backdrop.scss'; 4 | @import '_table.scss'; 5 | @import '_links.scss'; 6 | @import '_loader.scss'; 7 | 8 | * { 9 | box-sizing: border-box; 10 | } 11 | 12 | body { 13 | overflow-x: hidden; 14 | -webkit-overflow-scrolling: touch; 15 | max-width: 100vw; 16 | max-height: 100vh; 17 | display: flex; 18 | min-width: 100vw; 19 | min-height: 100vh; 20 | margin: 0; 21 | padding: $header-height 0 0; 22 | 23 | font-family: -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI", "Roboto", "Helvetica Neue", Arial, sans-serif; 24 | background-color: $background-color; 25 | user-select: none; 26 | line-height: 1.5; 27 | } 28 | -------------------------------------------------------------------------------- /ShoppingClient/src/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "lib": [ 5 | "es2016", 6 | "dom" 7 | ], 8 | "outDir": "../out-tsc/app", 9 | "target": "es5", 10 | "module": "es2015", 11 | "baseUrl": "" 12 | }, 13 | "types":[ 14 | "signalr" 15 | ], 16 | "exclude": [ 17 | "test.ts", 18 | "**/*.spec.ts" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /ShoppingClient/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "exclude": [ 4 | "node_modules" 5 | ], 6 | "compilerOptions": { 7 | "typeRoots": [ 8 | "node_modules/@types" 9 | ], 10 | "outDir": "./dist/out-tsc", 11 | "sourceMap": true, 12 | "declaration": false, 13 | "moduleResolution": "node", 14 | "emitDecoratorMetadata": true, 15 | "experimentalDecorators": true, 16 | "target": "es5", 17 | "lib": [ 18 | "es2016", 19 | "dom" 20 | ], 21 | "module": "es2015", 22 | "baseUrl": "./" 23 | } 24 | } -------------------------------------------------------------------------------- /ShoppingClient/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": ["node_modules/codelyzer"], 3 | "rules": { 4 | "callable-types": true, 5 | "class-name": true, 6 | "comment-format": [true, "check-space"], 7 | "curly": true, 8 | "eofline": true, 9 | "forin": true, 10 | "import-blacklist": [true], 11 | "import-spacing": true, 12 | "indent": [true, "spaces"], 13 | "interface-over-type-literal": true, 14 | "label-position": true, 15 | "max-line-length": [true, 140], 16 | "member-access": false, 17 | "member-ordering": [ 18 | true, 19 | "static-before-instance", 20 | "variables-before-functions" 21 | ], 22 | "no-arg": true, 23 | "no-bitwise": true, 24 | "no-console": [true, "debug", "info", "time", "timeEnd", "trace"], 25 | "no-construct": true, 26 | "no-debugger": true, 27 | "no-duplicate-variable": true, 28 | "no-empty": false, 29 | "no-empty-interface": true, 30 | "no-eval": true, 31 | "no-inferrable-types": [true, "ignore-params"], 32 | "no-shadowed-variable": true, 33 | "no-string-literal": false, 34 | "no-string-throw": true, 35 | "no-switch-case-fall-through": true, 36 | "no-trailing-whitespace": true, 37 | "no-unused-expression": true, 38 | "no-use-before-declare": true, 39 | "no-var-keyword": true, 40 | "object-literal-sort-keys": false, 41 | "one-line": [ 42 | true, 43 | "check-open-brace", 44 | "check-catch", 45 | "check-else", 46 | "check-whitespace" 47 | ], 48 | "prefer-const": true, 49 | "quotemark": [false, "double"], 50 | "radix": true, 51 | "semicolon": ["always"], 52 | "triple-equals": [true, "allow-null-check"], 53 | "typedef-whitespace": [ 54 | true, 55 | { 56 | "call-signature": "nospace", 57 | "index-signature": "nospace", 58 | "parameter": "nospace", 59 | "property-declaration": "nospace", 60 | "variable-declaration": "nospace" 61 | } 62 | ], 63 | "typeof-compare": true, 64 | "unified-signatures": true, 65 | "variable-name": false, 66 | "whitespace": [ 67 | true, 68 | "check-branch", 69 | "check-decl", 70 | "check-operator", 71 | "check-separator", 72 | "check-type" 73 | ], 74 | "directive-selector": [true, "attribute", "app", "camelCase"], 75 | "component-selector": [true, "element", "app", "kebab-case"], 76 | "use-input-property-decorator": true, 77 | "use-output-property-decorator": true, 78 | "use-host-property-decorator": true, 79 | "no-input-rename": true, 80 | "no-output-rename": true, 81 | "use-life-cycle-interface": true, 82 | "use-pipe-transform-interface": true, 83 | "component-class-suffix": true, 84 | "directive-class-suffix": true, 85 | "no-access-missing-member": true, 86 | "templates-use-public": true, 87 | "invoke-injectable": true 88 | } 89 | } 90 | --------------------------------------------------------------------------------