├── Views ├── _ViewStart.cshtml ├── _ViewImports.cshtml ├── Home │ └── Index.cshtml └── Shared │ ├── _Layout.cshtml │ └── Error.cshtml ├── wwwroot └── favicon.ico ├── ClientApp ├── app │ ├── components │ │ ├── home │ │ │ ├── home.component.ts │ │ │ └── home.component.html │ │ ├── app │ │ │ ├── app.component.css │ │ │ ├── app.component.ts │ │ │ └── app.component.html │ │ ├── counter │ │ │ ├── counter.component.html │ │ │ ├── counter.component.ts │ │ │ └── counter.component.spec.ts │ │ ├── fetchdata │ │ │ ├── fetchdata.component.html │ │ │ └── fetchdata.component.ts │ │ └── navmenu │ │ │ ├── navmenu.component.ts │ │ │ ├── navmenu.component.css │ │ │ └── navmenu.component.html │ ├── app.module.server.ts │ ├── app.module.browser.ts │ ├── environments │ │ └── environment.ts │ ├── interceptors │ │ └── authentication.httpInterceptor.ts │ ├── app.module.shared.ts │ └── services │ │ └── authentication.service.ts ├── test │ ├── karma.conf.js │ └── boot-tests.ts ├── boot.browser.ts └── boot.server.ts ├── appsettings.json ├── appsettings.Development.json ├── tsconfig.json ├── Controllers ├── HomeController.cs └── SampleDataController.cs ├── Program.cs ├── LICENSE ├── msal-netcore-angular.sln ├── StartupAuth.cs ├── package.json ├── Startup.cs ├── README.md ├── msal-netcore-angular.csproj ├── webpack.config.js ├── webpack.config.vendor.js └── .gitignore /Views/_ViewStart.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | Layout = "_Layout"; 3 | } 4 | -------------------------------------------------------------------------------- /wwwroot/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Gimly/NetCoreAngularAzureB2CMsal/HEAD/wwwroot/favicon.ico -------------------------------------------------------------------------------- /Views/_ViewImports.cshtml: -------------------------------------------------------------------------------- 1 | @using msal_netcore_angular 2 | @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers 3 | @addTagHelper *, Microsoft.AspNetCore.SpaServices 4 | -------------------------------------------------------------------------------- /ClientApp/app/components/home/home.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'home', 5 | templateUrl: './home.component.html' 6 | }) 7 | export class HomeComponent { 8 | } 9 | -------------------------------------------------------------------------------- /ClientApp/app/components/app/app.component.css: -------------------------------------------------------------------------------- 1 | @media (max-width: 767px) { 2 | /* On small screens, the nav menu spans the full width of the screen. Leave a space for it. */ 3 | .body-content { 4 | padding-top: 50px; 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /ClientApp/app/components/counter/counter.component.html: -------------------------------------------------------------------------------- 1 |

Counter

2 | 3 |

This is a simple example of an Angular component.

4 | 5 |

Current count: {{ currentCount }}

6 | 7 | 8 | -------------------------------------------------------------------------------- /ClientApp/app/components/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app', 5 | templateUrl: './app.component.html', 6 | styleUrls: ['./app.component.css'] 7 | }) 8 | export class AppComponent { 9 | } 10 | -------------------------------------------------------------------------------- /ClientApp/app/components/app/app.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 5 |
6 |
7 | 8 |
9 |
10 |
11 | -------------------------------------------------------------------------------- /Views/Home/Index.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | ViewData["Title"] = "Home Page"; 3 | } 4 | 5 | Loading... 6 | 7 | 8 | @section scripts { 9 | 10 | } 11 | -------------------------------------------------------------------------------- /ClientApp/app/components/counter/counter.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'counter', 5 | templateUrl: './counter.component.html' 6 | }) 7 | export class CounterComponent { 8 | public currentCount = 0; 9 | 10 | public incrementCounter() { 11 | this.currentCount++; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "IncludeScopes": false, 4 | "Debug": { 5 | "LogLevel": { 6 | "Default": "Warning" 7 | } 8 | }, 9 | "Console": { 10 | "LogLevel": { 11 | "Default": "Warning" 12 | } 13 | } 14 | }, 15 | "Jwt": { 16 | "Audience": "25eef6e4-c905-4a07-8eb4-0d08d5df8b3f", 17 | "Policy": "b2c_1_susi", 18 | "Tenant": "fabrikamb2c" 19 | } 20 | } -------------------------------------------------------------------------------- /ClientApp/app/app.module.server.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { ServerModule } from '@angular/platform-server'; 3 | import { AppModuleShared } from './app.module.shared'; 4 | import { AppComponent } from './components/app/app.component'; 5 | 6 | @NgModule({ 7 | bootstrap: [ AppComponent ], 8 | imports: [ 9 | ServerModule, 10 | AppModuleShared 11 | ] 12 | }) 13 | export class AppModule { 14 | } 15 | -------------------------------------------------------------------------------- /appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "IncludeScopes": false, 4 | "Debug": { 5 | "LogLevel": { 6 | "Default": "Debug", 7 | "System": "Information", 8 | "Microsoft": "Information" 9 | } 10 | }, 11 | "Console": { 12 | "LogLevel": { 13 | "Default": "Debug", 14 | "System": "Information", 15 | "Microsoft": "Information" 16 | } 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Views/Shared/_Layout.cshtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | @ViewData["Title"] - msal_netcore_angular 7 | 8 | 9 | 10 | 11 | 12 | @RenderBody() 13 | 14 | @RenderSection("scripts", required: false) 15 | 16 | 17 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "es2015", 4 | "moduleResolution": "node", 5 | "target": "es5", 6 | "sourceMap": true, 7 | "experimentalDecorators": true, 8 | "emitDecoratorMetadata": true, 9 | "skipDefaultLibCheck": true, 10 | "skipLibCheck": true, // Workaround for https://github.com/angular/angular/issues/17863. Remove this if you upgrade to a fixed version of Angular. 11 | "strict": true, 12 | "lib": [ "es6", "dom" ], 13 | "types": [ "webpack-env" ] 14 | }, 15 | "exclude": [ "bin", "node_modules" ], 16 | "atom": { "rewriteTsconfig": false } 17 | } 18 | -------------------------------------------------------------------------------- /Controllers/HomeController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | using Microsoft.AspNetCore.Mvc; 7 | 8 | namespace msal_netcore_angular.Controllers 9 | { 10 | public class HomeController : Controller 11 | { 12 | public IActionResult Index() 13 | { 14 | return View(); 15 | } 16 | 17 | public IActionResult Error() 18 | { 19 | ViewData["RequestId"] = Activity.Current?.Id ?? HttpContext.TraceIdentifier; 20 | return View(); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /ClientApp/app/app.module.browser.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { BrowserModule } from '@angular/platform-browser'; 3 | import { AppModuleShared } from './app.module.shared'; 4 | import { AppComponent } from './components/app/app.component'; 5 | 6 | @NgModule({ 7 | bootstrap: [ AppComponent ], 8 | imports: [ 9 | BrowserModule, 10 | AppModuleShared 11 | ], 12 | providers: [ 13 | { provide: 'BASE_URL', useFactory: getBaseUrl } 14 | ] 15 | }) 16 | export class AppModule { 17 | } 18 | 19 | export function getBaseUrl() { 20 | return document.getElementsByTagName('base')[0].href; 21 | } 22 | -------------------------------------------------------------------------------- /ClientApp/app/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // The file contents for the current environment will overwrite these during build. 2 | // The build system defaults to the dev environment which uses `environment.ts`, but if you do 3 | // `ng build --env=prod` then `environment.prod.ts` will be used instead. 4 | 5 | export const environment = { 6 | production: false, 7 | tenant: 'fabrikamb2c.onmicrosoft.com', 8 | clientID: 'e760cab2-b9a1-4c0d-86fb-ff7084abd902', 9 | signUpSignInPolicy: 'b2c_1_susi', 10 | b2cScopes: ['https://fabrikamb2c.onmicrosoft.com/demoapi/demo.read'], 11 | webApi: 'https://fabrikamb2chello.azurewebsites.net/hello' 12 | }; 13 | -------------------------------------------------------------------------------- /Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | using Microsoft.AspNetCore; 7 | using Microsoft.AspNetCore.Hosting; 8 | using Microsoft.Extensions.Configuration; 9 | using Microsoft.Extensions.Logging; 10 | 11 | namespace msal_netcore_angular 12 | { 13 | public class Program 14 | { 15 | public static void Main(string[] args) 16 | { 17 | BuildWebHost(args).Run(); 18 | } 19 | 20 | public static IWebHost BuildWebHost(string[] args) => 21 | WebHost.CreateDefaultBuilder(args) 22 | .UseStartup() 23 | .Build(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /ClientApp/app/components/fetchdata/fetchdata.component.html: -------------------------------------------------------------------------------- 1 |

Weather forecast

2 | 3 |

This component demonstrates fetching data from the server.

4 | 5 |

Loading...

6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 |
DateTemp. (C)Temp. (F)Summary
{{ forecast.dateFormatted }}{{ forecast.temperatureC }}{{ forecast.temperatureF }}{{ forecast.summary }}
25 | -------------------------------------------------------------------------------- /ClientApp/app/components/navmenu/navmenu.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { AuthenticationService } from '../../services/authentication.service'; 3 | 4 | @Component({ 5 | selector: 'nav-menu', 6 | templateUrl: './navmenu.component.html', 7 | styleUrls: ['./navmenu.component.css'] 8 | }) 9 | export class NavMenuComponent { 10 | constructor(private authService: AuthenticationService) { } 11 | 12 | login(): void { 13 | this.authService.login(); 14 | } 15 | 16 | logout(): void { 17 | this.authService.logout(); 18 | } 19 | 20 | get isOnline(): boolean { 21 | return this.authService.isOnline(); 22 | } 23 | 24 | get user(): string { 25 | return this.authService.getUser().name; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /ClientApp/app/components/fetchdata/fetchdata.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Inject, OnInit } from '@angular/core'; 2 | import { HttpClient } from '@angular/common/http'; 3 | 4 | @Component({ 5 | selector: 'fetchdata', 6 | templateUrl: './fetchdata.component.html' 7 | }) 8 | export class FetchDataComponent implements OnInit { 9 | public forecasts: WeatherForecast[]; 10 | 11 | constructor(private http: HttpClient, @Inject('BASE_URL') private baseUrl: string) { } 12 | 13 | ngOnInit(): void { 14 | this.http.get(this.baseUrl + 'api/SampleData/WeatherForecasts') 15 | .subscribe(result => { 16 | this.forecasts = result; 17 | }, error => console.error(error)); 18 | } 19 | } 20 | 21 | interface WeatherForecast { 22 | dateFormatted: string; 23 | temperatureC: number; 24 | temperatureF: number; 25 | summary: string; 26 | } 27 | -------------------------------------------------------------------------------- /Views/Shared/Error.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | ViewData["Title"] = "Error"; 3 | } 4 | 5 |

Error.

6 |

An error occurred while processing your request.

7 | 8 | @if (!string.IsNullOrEmpty((string)ViewData["RequestId"])) 9 | { 10 |

11 | Request ID: @ViewData["RequestId"] 12 |

13 | } 14 | 15 |

Development Mode

16 |

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

19 |

20 | Development environment should not be enabled in deployed applications, as it can result in sensitive information from exceptions being displayed to end users. For local debugging, development environment can be enabled by setting the ASPNETCORE_ENVIRONMENT environment variable to Development, and restarting the application. 21 |

22 | -------------------------------------------------------------------------------- /ClientApp/test/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/0.13/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '.', 7 | frameworks: ['jasmine'], 8 | files: [ 9 | '../../wwwroot/dist/vendor.js', 10 | './boot-tests.ts' 11 | ], 12 | preprocessors: { 13 | './boot-tests.ts': ['webpack'] 14 | }, 15 | reporters: ['progress'], 16 | port: 9876, 17 | colors: true, 18 | logLevel: config.LOG_INFO, 19 | autoWatch: true, 20 | browsers: ['Chrome'], 21 | mime: { 'application/javascript': ['ts','tsx'] }, 22 | singleRun: false, 23 | webpack: require('../../webpack.config.js')().filter(config => config.target !== 'node'), // Test against client bundle, because tests run in a browser 24 | webpackMiddleware: { stats: 'errors-only' } 25 | }); 26 | }; 27 | -------------------------------------------------------------------------------- /ClientApp/boot.browser.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | import 'zone.js'; 3 | import 'bootstrap'; 4 | import { enableProdMode } from '@angular/core'; 5 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 6 | import { AppModule } from './app/app.module.browser'; 7 | 8 | if (module.hot) { 9 | module.hot.accept(); 10 | module.hot.dispose(() => { 11 | // Before restarting the app, we create a new root element and dispose the old one 12 | const oldRootElem = document.querySelector('app'); 13 | const newRootElem = document.createElement('app'); 14 | oldRootElem!.parentNode!.insertBefore(newRootElem, oldRootElem); 15 | modulePromise.then(appModule => appModule.destroy()); 16 | }); 17 | } else { 18 | enableProdMode(); 19 | } 20 | 21 | // Note: @ng-tools/webpack looks for the following expression when performing production 22 | // builds. Don't change how this line looks, otherwise you may break tree-shaking. 23 | const modulePromise = platformBrowserDynamic().bootstrapModule(AppModule); 24 | -------------------------------------------------------------------------------- /ClientApp/app/interceptors/authentication.httpInterceptor.ts: -------------------------------------------------------------------------------- 1 | import { Observable } from 'rxjs/Observable'; 2 | import 'rxjs/add/observable/fromPromise'; 3 | import 'rxjs/add/operator/switchMap'; 4 | 5 | import { Injectable } from '@angular/core'; 6 | import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent } from '@angular/common/http'; 7 | import { AuthenticationService } from '../services/authentication.service'; 8 | 9 | @Injectable() 10 | export class AuthenticationHttpInterceptor implements HttpInterceptor { 11 | 12 | constructor(private authenticationService: AuthenticationService) { } 13 | 14 | intercept(req: HttpRequest, next: HttpHandler): Observable> { 15 | 16 | return Observable.fromPromise(this.authenticationService.getAuthenticationToken()) 17 | .switchMap(token => { 18 | req = req.clone({ 19 | setHeaders: { 20 | Authorization: `Bearer ${token}` 21 | } 22 | }); 23 | return next.handle(req); 24 | }); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Xavier Hahn 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 | -------------------------------------------------------------------------------- /ClientApp/test/boot-tests.ts: -------------------------------------------------------------------------------- 1 | // Load required polyfills and testing libraries 2 | import 'reflect-metadata'; 3 | import 'zone.js'; 4 | import 'zone.js/dist/long-stack-trace-zone'; 5 | import 'zone.js/dist/proxy.js'; 6 | import 'zone.js/dist/sync-test'; 7 | import 'zone.js/dist/jasmine-patch'; 8 | import 'zone.js/dist/async-test'; 9 | import 'zone.js/dist/fake-async-test'; 10 | import * as testing from '@angular/core/testing'; 11 | import * as testingBrowser from '@angular/platform-browser-dynamic/testing'; 12 | 13 | // There's no typing for the `__karma__` variable. Just declare it as any 14 | declare var __karma__: any; 15 | declare var require: any; 16 | 17 | // Prevent Karma from running prematurely 18 | __karma__.loaded = function () {}; 19 | 20 | // First, initialize the Angular testing environment 21 | testing.getTestBed().initTestEnvironment( 22 | testingBrowser.BrowserDynamicTestingModule, 23 | testingBrowser.platformBrowserDynamicTesting() 24 | ); 25 | 26 | // Then we find all the tests 27 | const context = require.context('../', true, /\.spec\.ts$/); 28 | 29 | // And load the modules 30 | context.keys().map(context); 31 | 32 | // Finally, start Karma to run the tests 33 | __karma__.start(); 34 | -------------------------------------------------------------------------------- /msal-netcore-angular.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.27004.2005 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "msal-netcore-angular", "msal-netcore-angular.csproj", "{679AC8AD-BDCD-4365-97D4-023E87A3C8DD}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {679AC8AD-BDCD-4365-97D4-023E87A3C8DD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {679AC8AD-BDCD-4365-97D4-023E87A3C8DD}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {679AC8AD-BDCD-4365-97D4-023E87A3C8DD}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {679AC8AD-BDCD-4365-97D4-023E87A3C8DD}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ExtensibilityGlobals) = postSolution 23 | SolutionGuid = {1F262BB8-4D88-4468-A314-9084FDE598F0} 24 | EndGlobalSection 25 | EndGlobal 26 | -------------------------------------------------------------------------------- /ClientApp/app/components/counter/counter.component.spec.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { assert } from 'chai'; 3 | import { CounterComponent } from './counter.component'; 4 | import { TestBed, async, ComponentFixture } from '@angular/core/testing'; 5 | 6 | let fixture: ComponentFixture; 7 | 8 | describe('Counter component', () => { 9 | beforeEach(() => { 10 | TestBed.configureTestingModule({ declarations: [CounterComponent] }); 11 | fixture = TestBed.createComponent(CounterComponent); 12 | fixture.detectChanges(); 13 | }); 14 | 15 | it('should display a title', async(() => { 16 | const titleText = fixture.nativeElement.querySelector('h1').textContent; 17 | expect(titleText).toEqual('Counter'); 18 | })); 19 | 20 | it('should start with count 0, then increments by 1 when clicked', async(() => { 21 | const countElement = fixture.nativeElement.querySelector('strong'); 22 | expect(countElement.textContent).toEqual('0'); 23 | 24 | const incrementButton = fixture.nativeElement.querySelector('button'); 25 | incrementButton.click(); 26 | fixture.detectChanges(); 27 | expect(countElement.textContent).toEqual('1'); 28 | })); 29 | }); 30 | -------------------------------------------------------------------------------- /ClientApp/app/components/navmenu/navmenu.component.css: -------------------------------------------------------------------------------- 1 | li .glyphicon { 2 | margin-right: 10px; 3 | } 4 | 5 | /* Highlighting rules for nav menu items */ 6 | li.link-active a, 7 | li.link-active a:hover, 8 | li.link-active a:focus { 9 | background-color: #4189C7; 10 | color: white; 11 | } 12 | 13 | /* Keep the nav menu independent of scrolling and on top of other items */ 14 | .main-nav { 15 | position: fixed; 16 | top: 0; 17 | left: 0; 18 | right: 0; 19 | z-index: 1; 20 | } 21 | 22 | @media (min-width: 768px) { 23 | /* On small screens, convert the nav menu to a vertical sidebar */ 24 | .main-nav { 25 | height: 100%; 26 | width: calc(25% - 20px); 27 | } 28 | .navbar { 29 | border-radius: 0px; 30 | border-width: 0px; 31 | height: 100%; 32 | } 33 | .navbar-header { 34 | float: none; 35 | } 36 | .navbar-collapse { 37 | border-top: 1px solid #444; 38 | padding: 0px; 39 | } 40 | .navbar p { 41 | color: white; 42 | } 43 | .navbar ul { 44 | float: none; 45 | } 46 | .navbar li { 47 | float: none; 48 | font-size: 15px; 49 | margin: 6px; 50 | } 51 | .navbar li a { 52 | padding: 10px 16px; 53 | border-radius: 4px; 54 | } 55 | .navbar a { 56 | /* If a menu item's text is too long, truncate it */ 57 | width: 100%; 58 | white-space: nowrap; 59 | overflow: hidden; 60 | text-overflow: ellipsis; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Controllers/SampleDataController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Authorization; 6 | using Microsoft.AspNetCore.Mvc; 7 | 8 | namespace msal_netcore_angular.Controllers 9 | { 10 | [Authorize] 11 | [Route("api/[controller]")] 12 | public class SampleDataController : Controller 13 | { 14 | private static string[] Summaries = new[] 15 | { 16 | "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" 17 | }; 18 | 19 | [HttpGet("[action]")] 20 | public IEnumerable WeatherForecasts() 21 | { 22 | var rng = new Random(); 23 | return Enumerable.Range(1, 5).Select(index => new WeatherForecast 24 | { 25 | DateFormatted = DateTime.Now.AddDays(index).ToString("d"), 26 | TemperatureC = rng.Next(-20, 55), 27 | Summary = Summaries[rng.Next(Summaries.Length)] 28 | }); 29 | } 30 | 31 | public class WeatherForecast 32 | { 33 | public string DateFormatted { get; set; } 34 | public int TemperatureC { get; set; } 35 | public string Summary { get; set; } 36 | 37 | public int TemperatureF 38 | { 39 | get 40 | { 41 | return 32 + (int)(TemperatureC / 0.5556); 42 | } 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /ClientApp/app/app.module.shared.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { HTTP_INTERCEPTORS, HttpClientModule } from '@angular/common/http'; 4 | import { FormsModule } from '@angular/forms'; 5 | import { RouterModule } from '@angular/router'; 6 | 7 | import { AppComponent } from './components/app/app.component'; 8 | import { NavMenuComponent } from './components/navmenu/navmenu.component'; 9 | import { HomeComponent } from './components/home/home.component'; 10 | import { FetchDataComponent } from './components/fetchdata/fetchdata.component'; 11 | import { CounterComponent } from './components/counter/counter.component'; 12 | import { AuthenticationService } from './services/authentication.service'; 13 | import { AuthenticationHttpInterceptor } from './interceptors/authentication.httpInterceptor'; 14 | 15 | @NgModule({ 16 | declarations: [ 17 | AppComponent, 18 | NavMenuComponent, 19 | CounterComponent, 20 | FetchDataComponent, 21 | HomeComponent 22 | ], 23 | imports: [ 24 | CommonModule, 25 | HttpClientModule, 26 | FormsModule, 27 | RouterModule.forRoot([ 28 | { path: '', component: HomeComponent }, 29 | { path: 'home', component: HomeComponent }, 30 | { path: 'counter', component: CounterComponent }, 31 | { path: 'fetch-data', component: FetchDataComponent }, 32 | { path: '**', redirectTo: 'home' } 33 | ]), 34 | ], 35 | providers: [ 36 | AuthenticationService, 37 | { 38 | provide: HTTP_INTERCEPTORS, 39 | useClass: AuthenticationHttpInterceptor, 40 | multi: true 41 | } 42 | ] 43 | }) 44 | export class AppModuleShared { 45 | } 46 | -------------------------------------------------------------------------------- /ClientApp/app/components/home/home.component.html: -------------------------------------------------------------------------------- 1 |

Hello, world!

2 |

Welcome to your new single-page application, built with:

3 | 9 |

To help you get started, we've also set up:

10 |
    11 |
  • Client-side navigation. For example, click Counter then Back to return here.
  • 12 |
  • Server-side prerendering. For faster initial loading and improved SEO, your Angular app is prerendered on the server. The resulting HTML is then transferred to the browser where a client-side copy of the app takes over.
  • 13 |
  • Webpack dev middleware. In development mode, there's no need to run the webpack build tool. Your client-side resources are dynamically built on demand. Updates are available as soon as you modify any file.
  • 14 |
  • Hot module replacement. In development mode, you don't even need to reload the page after making most changes. Within seconds of saving changes to files, your Angular app will be rebuilt and a new instance injected is into the page.
  • 15 |
  • Efficient production builds. In production mode, development-time features are disabled, and the webpack build tool produces minified static CSS and JavaScript files.
  • 16 |
17 | -------------------------------------------------------------------------------- /ClientApp/boot.server.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | import 'zone.js'; 3 | import 'rxjs/add/operator/first'; 4 | import { APP_BASE_HREF } from '@angular/common'; 5 | import { enableProdMode, ApplicationRef, NgZone, ValueProvider } from '@angular/core'; 6 | import { platformDynamicServer, PlatformState, INITIAL_CONFIG } from '@angular/platform-server'; 7 | import { createServerRenderer, RenderResult } from 'aspnet-prerendering'; 8 | import { AppModule } from './app/app.module.server'; 9 | 10 | enableProdMode(); 11 | 12 | export default createServerRenderer(params => { 13 | const providers = [ 14 | { provide: INITIAL_CONFIG, useValue: { document: '', url: params.url } }, 15 | { provide: APP_BASE_HREF, useValue: params.baseUrl }, 16 | { provide: 'BASE_URL', useValue: params.origin + params.baseUrl }, 17 | ]; 18 | 19 | return platformDynamicServer(providers).bootstrapModule(AppModule).then(moduleRef => { 20 | const appRef: ApplicationRef = moduleRef.injector.get(ApplicationRef); 21 | const state = moduleRef.injector.get(PlatformState); 22 | const zone = moduleRef.injector.get(NgZone); 23 | 24 | return new Promise((resolve, reject) => { 25 | zone.onError.subscribe((errorInfo: any) => reject(errorInfo)); 26 | appRef.isStable.first(isStable => isStable).subscribe(() => { 27 | // Because 'onStable' fires before 'onError', we have to delay slightly before 28 | // completing the request in case there's an error to report 29 | setImmediate(() => { 30 | resolve({ 31 | html: state.renderToString() 32 | }); 33 | moduleRef.destroy(); 34 | }); 35 | }); 36 | }); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /StartupAuth.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authentication.JwtBearer; 2 | using Microsoft.AspNetCore.Hosting; 3 | using Microsoft.AspNetCore.Http; 4 | using Microsoft.Extensions.Configuration; 5 | using Microsoft.Extensions.DependencyInjection; 6 | 7 | namespace msal_netcore_angular 8 | { 9 | public static class StartupAuth 10 | { 11 | const string metaDataAddressFormatter = "https://login.microsoftonline.com/{0}/v2.0/.well-known/openid-configuration?p={1}"; 12 | const string tenantFormatter = "{0}.onmicrosoft.com"; 13 | 14 | public static void AddAzureB2CAuthentication(this IServiceCollection services, string policy, string tenant, string audience, bool isDevelopment) 15 | { 16 | var myTenant = string.Format(tenantFormatter, tenant); 17 | 18 | services.AddAuthentication(options => 19 | { 20 | options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; 21 | options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; 22 | }).AddJwtBearer(o => 23 | { 24 | o.MetadataAddress = string.Format(metaDataAddressFormatter, myTenant, policy); 25 | o.Audience = audience; 26 | o.Events = new JwtBearerEvents() 27 | { 28 | OnAuthenticationFailed = c => 29 | { 30 | c.NoResult(); 31 | 32 | c.Response.StatusCode = 500; 33 | c.Response.ContentType = "text/plain"; 34 | 35 | if (isDevelopment) 36 | { 37 | return c.Response.WriteAsync(c.Exception.ToString()); 38 | } 39 | 40 | return c.Response.WriteAsync("An error occured processing your authentication.
\r\n" + c.Exception.ToString()); 41 | } 42 | }; 43 | }); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /ClientApp/app/components/navmenu/navmenu.component.html: -------------------------------------------------------------------------------- 1 | 38 | 39 | 40 |

Hello, {{user}}

41 | 42 |
-------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "msal_netcore_angular", 3 | "private": true, 4 | "version": "0.0.0", 5 | "scripts": { 6 | "test": "karma start ClientApp/test/karma.conf.js" 7 | }, 8 | "dependencies": { 9 | "@angular/animations": "^4.3.5", 10 | "@angular/common": "^4.3.5", 11 | "@angular/compiler": "^4.3.5", 12 | "@angular/compiler-cli": "^4.3.5", 13 | "@angular/core": "^4.3.5", 14 | "@angular/forms": "^4.3.5", 15 | "@angular/http": "^4.3.5", 16 | "@angular/platform-browser": "^4.3.5", 17 | "@angular/platform-browser-dynamic": "^4.3.5", 18 | "@angular/platform-server": "^4.3.5", 19 | "@angular/router": "^4.3.5", 20 | "@ngtools/webpack": "1.5.0", 21 | "@types/webpack-env": "1.13.0", 22 | "angular2-template-loader": "0.6.2", 23 | "aspnet-prerendering": "^3.0.1", 24 | "aspnet-webpack": "^2.0.1", 25 | "awesome-typescript-loader": "3.2.1", 26 | "bootstrap": "3.3.7", 27 | "css": "2.2.1", 28 | "css-loader": "0.28.4", 29 | "es6-shim": "0.35.3", 30 | "event-source-polyfill": "0.0.9", 31 | "expose-loader": "0.7.3", 32 | "extract-text-webpack-plugin": "2.1.2", 33 | "file-loader": "0.11.2", 34 | "html-loader": "0.4.5", 35 | "isomorphic-fetch": "2.2.1", 36 | "jquery": "3.2.1", 37 | "json-loader": "0.5.4", 38 | "msal": "0.1.3", 39 | "preboot": "4.5.2", 40 | "raw-loader": "0.5.1", 41 | "reflect-metadata": "0.1.10", 42 | "rxjs": "5.4.2", 43 | "style-loader": "0.18.2", 44 | "to-string-loader": "1.1.5", 45 | "typescript": "2.4.1", 46 | "url-loader": "0.5.9", 47 | "webpack": "2.5.1", 48 | "webpack-hot-middleware": "2.18.2", 49 | "webpack-merge": "4.1.0", 50 | "zone.js": "0.8.12" 51 | }, 52 | "devDependencies": { 53 | "@types/chai": "4.0.1", 54 | "@types/jasmine": "2.5.53", 55 | "chai": "4.0.2", 56 | "jasmine-core": "2.6.4", 57 | "karma": "1.7.0", 58 | "karma-chai": "0.1.0", 59 | "karma-chrome-launcher": "2.2.0", 60 | "karma-cli": "1.0.1", 61 | "karma-jasmine": "1.1.0", 62 | "karma-webpack": "2.0.3" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /ClientApp/app/services/authentication.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import * as Msal from 'msal'; 3 | 4 | import { environment } from '../environments/environment'; 5 | import { User } from "msal/lib-commonjs/User"; 6 | 7 | @Injectable() 8 | export class AuthenticationService { 9 | private authority = `https://login.microsoftonline.com/tfp/${environment.tenant}/${environment.signUpSignInPolicy}`; 10 | 11 | private clientApplication: Msal.UserAgentApplication; 12 | 13 | constructor() { 14 | this.clientApplication = 15 | new Msal.UserAgentApplication( 16 | environment.clientID, 17 | this.authority, 18 | this.authCallback, 19 | { 20 | redirectUri: window.location.origin 21 | }); 22 | } 23 | 24 | public login(): void { 25 | this.clientApplication.loginRedirect(environment.b2cScopes); 26 | } 27 | 28 | public logout(): void { 29 | this.clientApplication.logout(); 30 | } 31 | 32 | public isOnline(): boolean { 33 | return this.clientApplication.getUser() != null; 34 | } 35 | 36 | public getUser(): User { 37 | return this.clientApplication.getUser(); 38 | } 39 | 40 | public getAuthenticationToken(): Promise { 41 | return this.clientApplication.acquireTokenSilent(environment.b2cScopes) 42 | .then(token => { 43 | return token; 44 | }).catch(error => { 45 | return this.clientApplication.acquireTokenPopup(environment.b2cScopes) 46 | .then(token => { 47 | return Promise.resolve(token); 48 | }).catch(innererror => { 49 | console.error('Could not retrieve token from popup.', innererror); 50 | return Promise.resolve(''); 51 | }); 52 | }); 53 | } 54 | 55 | private authCallback(errorDesc: any, token: any, error: any, tokenType: any) { 56 | if (error) { 57 | console.error(`${error} ${errorDesc}`); 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Startup.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Builder; 2 | using Microsoft.AspNetCore.Hosting; 3 | using Microsoft.AspNetCore.SpaServices.Webpack; 4 | using Microsoft.Extensions.Configuration; 5 | using Microsoft.Extensions.DependencyInjection; 6 | 7 | namespace msal_netcore_angular 8 | { 9 | public class Startup 10 | { 11 | public Startup(IConfiguration configuration, IHostingEnvironment env) 12 | { 13 | Configuration = configuration; 14 | Environment = env; 15 | } 16 | 17 | public IConfiguration Configuration { get; } 18 | public IHostingEnvironment Environment { get; private set; } 19 | 20 | // This method gets called by the runtime. Use this method to add services to the container. 21 | public void ConfigureServices(IServiceCollection services) 22 | { 23 | services.AddMvc(); 24 | 25 | services.AddAzureB2CAuthentication(Configuration["Jwt:Policy"], 26 | Configuration["Jwt:Tenant"], 27 | Configuration["Jwt:Audience"], 28 | Environment.IsDevelopment()); 29 | } 30 | 31 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 32 | public void Configure(IApplicationBuilder app) 33 | { 34 | if (Environment.IsDevelopment()) 35 | { 36 | app.UseDeveloperExceptionPage(); 37 | app.UseWebpackDevMiddleware(new WebpackDevMiddlewareOptions 38 | { 39 | HotModuleReplacement = true 40 | }); 41 | } 42 | else 43 | { 44 | app.UseExceptionHandler("/Home/Error"); 45 | } 46 | 47 | app.UseAuthentication(); 48 | 49 | app.UseStaticFiles(); 50 | 51 | app.UseMvc(routes => 52 | { 53 | routes.MapRoute( 54 | name: "default", 55 | template: "{controller=Home}/{action=Index}/{id?}"); 56 | 57 | routes.MapSpaFallbackRoute( 58 | name: "spa-fallback", 59 | defaults: new { controller = "Home", action = "Index" }); 60 | }); 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Angular .Net Core startup template with Authentication through Azure AD B2C 2 | This is the dotnet core angular default template with an addition of authentication with the help of Azure AD B2C and msal.js 3 | 4 | ## Setting up 5 | First, you'll need to download the source code using git. 6 | ```Bash 7 | git clone https://github.com/Gimly/NetCoreAngularAzureB2CMsal.git 8 | ``` 9 | Then, go to the folder where the code has been loaded and restore both Nuget and NPM packages 10 | ```Bash 11 | cd NetCoreAngularAzureB2CMsal 12 | dotnet restore 13 | npm install 14 | ``` 15 | Then, open the source code and go to `~/ClientApp/app/environments` and update the `environment.ts` file to match your own Azure AD B2C subcription. 16 | ```TypeScript 17 | export const environment = { 18 | production: false, 19 | tenant: 'fabrikamb2c.onmicrosoft.com', // The name of your Azure AD B2C Tenant 20 | clientID: 'e760cab2-b9a1-4c0d-86fb-ff7084abd902', // The application ID 21 | signUpSignInPolicy: 'b2c_1_susi', // The name of the Sign-up or sign-in policy 22 | b2cScopes: ['https://fabrikamb2c.onmicrosoft.com/demoapi/demo.read'], // A scope that you will setup in the Application 23 | webApi: 'https://fabrikamb2chello.azurewebsites.net/hello' // URL of a web api 24 | }; 25 | ``` 26 | You'll also need to change the .Net Core API side's by changing the `JWT` property in the `appsettings.json` file. As an alternative, you can right click the project and select `Manage User Secrets`. This prevent you from commiting your tenant configuration to Git unless so desired. 27 | ```json 28 | "Jwt": { 29 | "Audience": "25eef6e4-c905-4a07-8eb4-0d08d5df8b3f", 30 | "Policy": "b2c_1_susi", 31 | "Tenant": "fabrikamb2c" 32 | } 33 | 34 | ``` 35 | 36 | Don't forget to add the URL and port on which you'll be running the sample in the application properties as a Reply URL, otherwise it won't work. 37 | 38 | Alternatively, you can also keep the settings and try to connect with the fabrikamb2c tenant, just make sure that you run the app on port 6420 if you want to do this. 39 | To do this, set the `ASPNETCORE_URLS` environment variable to `"http://localhost:6420"`. If you're using PowerShell, use this command: 40 | ```PowerShell 41 | $env:ASPNETCORE_URLS="http://localhost:6420" 42 | ``` 43 | 44 | ## Running the sample 45 | Before you run the sample locally, make sure you're running it in Development mode, otherwise it might not correctly bootstrap webpack. 46 | ```PowerShell 47 | $Env:ASPNETCORE_ENVIRONMENT = "Development" 48 | ``` 49 | 50 | To run the sample, simply call 51 | ```Bash 52 | dotnet run 53 | ``` 54 | -------------------------------------------------------------------------------- /msal-netcore-angular.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp2.0 5 | true 6 | Latest 7 | false 8 | 0152be93-5c53-4399-923a-6e9ab1fe8846 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | %(DistFiles.Identity) 50 | PreserveNewest 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | const merge = require('webpack-merge'); 4 | const AotPlugin = require('@ngtools/webpack').AotPlugin; 5 | const CheckerPlugin = require('awesome-typescript-loader').CheckerPlugin; 6 | 7 | module.exports = (env) => { 8 | // Configuration in common to both client-side and server-side bundles 9 | const isDevBuild = !(env && env.prod); 10 | const sharedConfig = { 11 | stats: { modules: false }, 12 | context: __dirname, 13 | resolve: { extensions: [ '.js', '.ts' ] }, 14 | output: { 15 | filename: '[name].js', 16 | publicPath: 'dist/' // Webpack dev middleware, if enabled, handles requests for this URL prefix 17 | }, 18 | module: { 19 | rules: [ 20 | { test: /\.ts$/, include: /ClientApp/, use: isDevBuild ? ['awesome-typescript-loader?silent=true', 'angular2-template-loader'] : '@ngtools/webpack' }, 21 | { test: /\.html$/, use: 'html-loader?minimize=false' }, 22 | { test: /\.css$/, use: [ 'to-string-loader', isDevBuild ? 'css-loader' : 'css-loader?minimize' ] }, 23 | { test: /\.(png|jpg|jpeg|gif|svg)$/, use: 'url-loader?limit=25000' } 24 | ] 25 | }, 26 | plugins: [new CheckerPlugin()] 27 | }; 28 | 29 | // Configuration for client-side bundle suitable for running in browsers 30 | const clientBundleOutputDir = './wwwroot/dist'; 31 | const clientBundleConfig = merge(sharedConfig, { 32 | entry: { 'main-client': './ClientApp/boot.browser.ts' }, 33 | output: { path: path.join(__dirname, clientBundleOutputDir) }, 34 | plugins: [ 35 | new webpack.DllReferencePlugin({ 36 | context: __dirname, 37 | manifest: require('./wwwroot/dist/vendor-manifest.json') 38 | }) 39 | ].concat(isDevBuild ? [ 40 | // Plugins that apply in development builds only 41 | new webpack.SourceMapDevToolPlugin({ 42 | filename: '[file].map', // Remove this line if you prefer inline source maps 43 | moduleFilenameTemplate: path.relative(clientBundleOutputDir, '[resourcePath]') // Point sourcemap entries to the original file locations on disk 44 | }) 45 | ] : [ 46 | // Plugins that apply in production builds only 47 | new webpack.optimize.UglifyJsPlugin(), 48 | new AotPlugin({ 49 | tsConfigPath: './tsconfig.json', 50 | entryModule: path.join(__dirname, 'ClientApp/app/app.module.browser#AppModule'), 51 | exclude: ['./**/*.server.ts'] 52 | }) 53 | ]) 54 | }); 55 | 56 | // Configuration for server-side (prerendering) bundle suitable for running in Node 57 | const serverBundleConfig = merge(sharedConfig, { 58 | resolve: { mainFields: ['main'] }, 59 | entry: { 'main-server': './ClientApp/boot.server.ts' }, 60 | plugins: [ 61 | new webpack.DllReferencePlugin({ 62 | context: __dirname, 63 | manifest: require('./ClientApp/dist/vendor-manifest.json'), 64 | sourceType: 'commonjs2', 65 | name: './vendor' 66 | }) 67 | ].concat(isDevBuild ? [] : [ 68 | // Plugins that apply in production builds only 69 | new AotPlugin({ 70 | tsConfigPath: './tsconfig.json', 71 | entryModule: path.join(__dirname, 'ClientApp/app/app.module.server#AppModule'), 72 | exclude: ['./**/*.browser.ts'] 73 | }) 74 | ]), 75 | output: { 76 | libraryTarget: 'commonjs', 77 | path: path.join(__dirname, './ClientApp/dist') 78 | }, 79 | target: 'node', 80 | devtool: 'inline-source-map' 81 | }); 82 | 83 | return [clientBundleConfig, serverBundleConfig]; 84 | }; 85 | -------------------------------------------------------------------------------- /webpack.config.vendor.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | const ExtractTextPlugin = require('extract-text-webpack-plugin'); 4 | const merge = require('webpack-merge'); 5 | const treeShakableModules = [ 6 | '@angular/animations', 7 | '@angular/common', 8 | '@angular/compiler', 9 | '@angular/core', 10 | '@angular/forms', 11 | '@angular/http', 12 | '@angular/platform-browser', 13 | '@angular/platform-browser-dynamic', 14 | '@angular/router', 15 | 'zone.js', 16 | ]; 17 | const nonTreeShakableModules = [ 18 | 'bootstrap', 19 | 'bootstrap/dist/css/bootstrap.css', 20 | 'es6-promise', 21 | 'es6-shim', 22 | 'event-source-polyfill', 23 | 'jquery', 24 | ]; 25 | const allModules = treeShakableModules.concat(nonTreeShakableModules); 26 | 27 | module.exports = (env) => { 28 | const extractCSS = new ExtractTextPlugin('vendor.css'); 29 | const isDevBuild = !(env && env.prod); 30 | const sharedConfig = { 31 | stats: { modules: false }, 32 | resolve: { extensions: [ '.js' ] }, 33 | module: { 34 | rules: [ 35 | { test: /\.(png|woff|woff2|eot|ttf|svg)(\?|$)/, use: 'url-loader?limit=100000' } 36 | ] 37 | }, 38 | output: { 39 | publicPath: 'dist/', 40 | filename: '[name].js', 41 | library: '[name]_[hash]' 42 | }, 43 | plugins: [ 44 | new webpack.ProvidePlugin({ $: 'jquery', jQuery: 'jquery' }), // Maps these identifiers to the jQuery package (because Bootstrap expects it to be a global variable) 45 | new webpack.ContextReplacementPlugin(/\@angular\b.*\b(bundles|linker)/, path.join(__dirname, './ClientApp')), // Workaround for https://github.com/angular/angular/issues/11580 46 | new webpack.ContextReplacementPlugin(/angular(\\|\/)core(\\|\/)@angular/, path.join(__dirname, './ClientApp')), // Workaround for https://github.com/angular/angular/issues/14898 47 | new webpack.IgnorePlugin(/^vertx$/) // Workaround for https://github.com/stefanpenner/es6-promise/issues/100 48 | ] 49 | }; 50 | 51 | const clientBundleConfig = merge(sharedConfig, { 52 | entry: { 53 | // To keep development builds fast, include all vendor dependencies in the vendor bundle. 54 | // But for production builds, leave the tree-shakable ones out so the AOT compiler can produce a smaller bundle. 55 | vendor: isDevBuild ? allModules : nonTreeShakableModules 56 | }, 57 | output: { path: path.join(__dirname, 'wwwroot', 'dist') }, 58 | module: { 59 | rules: [ 60 | { test: /\.css(\?|$)/, use: extractCSS.extract({ use: isDevBuild ? 'css-loader' : 'css-loader?minimize' }) } 61 | ] 62 | }, 63 | plugins: [ 64 | extractCSS, 65 | new webpack.DllPlugin({ 66 | path: path.join(__dirname, 'wwwroot', 'dist', '[name]-manifest.json'), 67 | name: '[name]_[hash]' 68 | }) 69 | ].concat(isDevBuild ? [] : [ 70 | new webpack.optimize.UglifyJsPlugin() 71 | ]) 72 | }); 73 | 74 | const serverBundleConfig = merge(sharedConfig, { 75 | target: 'node', 76 | resolve: { mainFields: ['main'] }, 77 | entry: { vendor: allModules.concat(['aspnet-prerendering']) }, 78 | output: { 79 | path: path.join(__dirname, 'ClientApp', 'dist'), 80 | libraryTarget: 'commonjs2', 81 | }, 82 | module: { 83 | rules: [ { test: /\.css(\?|$)/, use: ['to-string-loader', isDevBuild ? 'css-loader' : 'css-loader?minimize' ] } ] 84 | }, 85 | plugins: [ 86 | new webpack.DllPlugin({ 87 | path: path.join(__dirname, 'ClientApp', 'dist', '[name]-manifest.json'), 88 | name: '[name]_[hash]' 89 | }) 90 | ] 91 | }); 92 | 93 | return [clientBundleConfig, serverBundleConfig]; 94 | } 95 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /Properties/launchSettings.json 2 | 3 | ## Ignore Visual Studio temporary files, build results, and 4 | ## files generated by popular Visual Studio add-ons. 5 | 6 | # User-specific files 7 | *.suo 8 | *.user 9 | *.userosscache 10 | *.sln.docstates 11 | 12 | # User-specific files (MonoDevelop/Xamarin Studio) 13 | *.userprefs 14 | 15 | # Build results 16 | [Dd]ebug/ 17 | [Dd]ebugPublic/ 18 | [Rr]elease/ 19 | [Rr]eleases/ 20 | x64/ 21 | x86/ 22 | build/ 23 | bld/ 24 | bin/ 25 | Bin/ 26 | obj/ 27 | Obj/ 28 | 29 | # Visual Studio 2015 cache/options directory 30 | .vs/ 31 | /wwwroot/dist/ 32 | /ClientApp/dist/ 33 | 34 | # MSTest test Results 35 | [Tt]est[Rr]esult*/ 36 | [Bb]uild[Ll]og.* 37 | 38 | # NUNIT 39 | *.VisualState.xml 40 | TestResult.xml 41 | 42 | # Build Results of an ATL Project 43 | [Dd]ebugPS/ 44 | [Rr]eleasePS/ 45 | dlldata.c 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 | 84 | # Visual Studio profiler 85 | *.psess 86 | *.vsp 87 | *.vspx 88 | *.sap 89 | 90 | # TFS 2012 Local Workspace 91 | $tf/ 92 | 93 | # Guidance Automation Toolkit 94 | *.gpState 95 | 96 | # ReSharper is a .NET coding add-in 97 | _ReSharper*/ 98 | *.[Rr]e[Ss]harper 99 | *.DotSettings.user 100 | 101 | # JustCode is a .NET coding add-in 102 | .JustCode 103 | 104 | # TeamCity is a build add-in 105 | _TeamCity* 106 | 107 | # DotCover is a Code Coverage Tool 108 | *.dotCover 109 | 110 | # NCrunch 111 | _NCrunch_* 112 | .*crunch*.local.xml 113 | nCrunchTemp_* 114 | 115 | # MightyMoose 116 | *.mm.* 117 | AutoTest.Net/ 118 | 119 | # Web workbench (sass) 120 | .sass-cache/ 121 | 122 | # Installshield output folder 123 | [Ee]xpress/ 124 | 125 | # DocProject is a documentation generator add-in 126 | DocProject/buildhelp/ 127 | DocProject/Help/*.HxT 128 | DocProject/Help/*.HxC 129 | DocProject/Help/*.hhc 130 | DocProject/Help/*.hhk 131 | DocProject/Help/*.hhp 132 | DocProject/Help/Html2 133 | DocProject/Help/html 134 | 135 | # Click-Once directory 136 | publish/ 137 | 138 | # Publish Web Output 139 | *.[Pp]ublish.xml 140 | *.azurePubxml 141 | # TODO: Comment the next line if you want to checkin your web deploy settings 142 | # but database connection strings (with potential passwords) will be unencrypted 143 | *.pubxml 144 | *.publishproj 145 | 146 | # NuGet Packages 147 | *.nupkg 148 | # The packages folder can be ignored because of Package Restore 149 | **/packages/* 150 | # except build/, which is used as an MSBuild target. 151 | !**/packages/build/ 152 | # Uncomment if necessary however generally it will be regenerated when needed 153 | #!**/packages/repositories.config 154 | 155 | # Microsoft Azure Build Output 156 | csx/ 157 | *.build.csdef 158 | 159 | # Microsoft Azure Emulator 160 | ecf/ 161 | rcf/ 162 | 163 | # Microsoft Azure ApplicationInsights config file 164 | ApplicationInsights.config 165 | 166 | # Windows Store app package directory 167 | AppPackages/ 168 | BundleArtifacts/ 169 | 170 | # Visual Studio cache files 171 | # files ending in .cache can be ignored 172 | *.[Cc]ache 173 | # but keep track of directories ending in .cache 174 | !*.[Cc]ache/ 175 | 176 | # Others 177 | ClientBin/ 178 | ~$* 179 | *~ 180 | *.dbmdl 181 | *.dbproj.schemaview 182 | *.pfx 183 | *.publishsettings 184 | orleans.codegen.cs 185 | 186 | /node_modules 187 | 188 | /yarn.lock 189 | 190 | # RIA/Silverlight projects 191 | Generated_Code/ 192 | 193 | # Backup & report files from converting an old project file 194 | # to a newer Visual Studio version. Backup files are not needed, 195 | # because we have git ;-) 196 | _UpgradeReport_Files/ 197 | Backup*/ 198 | UpgradeLog*.XML 199 | UpgradeLog*.htm 200 | 201 | # SQL Server files 202 | *.mdf 203 | *.ldf 204 | 205 | # Business Intelligence projects 206 | *.rdl.data 207 | *.bim.layout 208 | *.bim_*.settings 209 | 210 | # Microsoft Fakes 211 | FakesAssemblies/ 212 | 213 | # GhostDoc plugin setting file 214 | *.GhostDoc.xml 215 | 216 | # Node.js Tools for Visual Studio 217 | .ntvs_analysis.dat 218 | 219 | # Visual Studio 6 build log 220 | *.plg 221 | 222 | # Visual Studio 6 workspace options file 223 | *.opt 224 | 225 | # Visual Studio LightSwitch build output 226 | **/*.HTMLClient/GeneratedArtifacts 227 | **/*.DesktopClient/GeneratedArtifacts 228 | **/*.DesktopClient/ModelManifest.xml 229 | **/*.Server/GeneratedArtifacts 230 | **/*.Server/ModelManifest.xml 231 | _Pvt_Extensions 232 | 233 | # Paket dependency manager 234 | .paket/paket.exe 235 | 236 | # FAKE - F# Make 237 | .fake/ 238 | --------------------------------------------------------------------------------