2 |
--------------------------------------------------------------------------------
/src/server/Extensions/AuthenticationBuilderExtensions.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Authentication;
2 | using Microsoft.AspNetCore.Authentication.JwtBearer;
3 | using Microsoft.Extensions.DependencyInjection;
4 | using Microsoft.Extensions.Logging;
5 | using Microsoft.Extensions.Options;
6 | using ServiceApp.Logging;
7 | using ServiceApp.Models;
8 | using System;
9 |
10 | namespace ServiceApp.Extensions
11 | {
12 | ///
13 | /// ADAL JwtBearer configuration
14 | ///
15 | public static class AuthenticationBuilderExtensions
16 | {
17 | public static AuthenticationBuilder AddAzureAdBearer(this AuthenticationBuilder builder, Action
configureOptions)
18 | {
19 | builder.Services.Configure(configureOptions);
20 | builder.Services.AddSingleton, ConfigureAzureOptions>();
21 | builder.AddJwtBearer();
22 | return builder;
23 | }
24 |
25 | private class ConfigureAzureOptions : IConfigureNamedOptions
26 | {
27 | private readonly AzureAdOptions _azureOptions;
28 | private readonly ILogger _logger;
29 |
30 | public ConfigureAzureOptions(IOptions azureOptions, ILoggerFactory loggerFactory)
31 | {
32 | _azureOptions = azureOptions.Value;
33 | _logger = loggerFactory.CreateLogger();
34 | }
35 |
36 | public void Configure(string name, JwtBearerOptions options)
37 | {
38 | options.Audience = _azureOptions.ClientId;
39 |
40 | options.Authority = $"{_azureOptions.Instance}{_azureOptions.TenantId}";
41 |
42 | options.Events = new MyJwtBearerEvents(_logger);
43 | }
44 |
45 | public void Configure(JwtBearerOptions options)
46 | {
47 | Configure(Options.DefaultName, options);
48 | }
49 | }
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/client/spa-app/src/app/shared/services/adal.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@angular/core';
2 | import { Observable, Subscriber } from 'rxjs';
3 | import { retry } from 'rxjs/operators';
4 | import { AdalConfigService } from './adal-config.service';
5 | import { adal } from 'adal-angular';
6 |
7 | declare var AuthenticationContext: adal.AuthenticationContextStatic;
8 | let createAuthContextFn: adal.AuthenticationContextStatic = AuthenticationContext;
9 |
10 | @Injectable()
11 | export class AdalService {
12 | private context: adal.AuthenticationContext;
13 | constructor(private configService: AdalConfigService) {
14 | this.context = new createAuthContextFn(configService.adalSettings);
15 | }
16 | login() {
17 | this.context.login();
18 | }
19 | logout() {
20 | this.context.logOut();
21 | }
22 | get authContext() {
23 | return this.context;
24 | }
25 | handleWindowCallback() {
26 | this.context.handleWindowCallback();
27 | }
28 | public get userInfo() {
29 |
30 | return this.context.getCachedUser();
31 | }
32 | public get accessToken() {
33 | return this.context.getCachedToken(this.configService.adalSettings.clientId);
34 | }
35 | public get isAuthenticated() {
36 | return this.userInfo && this.accessToken;
37 | }
38 |
39 | public isCallback(hash: string) {
40 | return this.context.isCallback(hash);
41 | }
42 |
43 | public getLoginError() {
44 | return this.context.getLoginError();
45 | }
46 |
47 | public getAccessToken(endpoint: string, callbacks: (message: string, token: string) => any) {
48 |
49 | return this.context.acquireToken(endpoint, callbacks);
50 | }
51 |
52 | public acquireTokenResilient(resource: string): Observable {
53 | return new Observable((subscriber: Subscriber) =>
54 | this.context.acquireToken(resource, (message: string, token: string) => {
55 | if (token) {
56 | subscriber.next(token);
57 | } else {
58 | console.error(message)
59 | subscriber.error(message);
60 | }
61 | })
62 | ).pipe(retry(3));
63 | }
64 | }
--------------------------------------------------------------------------------
/src/server/Startup.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Authentication.JwtBearer;
2 | using Microsoft.AspNetCore.Builder;
3 | using Microsoft.AspNetCore.Hosting;
4 | using Microsoft.AspNetCore.Mvc;
5 | using Microsoft.Extensions.Configuration;
6 | using Microsoft.Extensions.DependencyInjection;
7 | using ServiceApp.Extensions;
8 |
9 | namespace ServiceApp
10 | {
11 | public class Startup
12 | {
13 | public Startup(IConfiguration configuration)
14 | {
15 | Configuration = configuration;
16 | }
17 |
18 | public IConfiguration Configuration { get; }
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.AddAuthentication(sharedOptions =>
24 | {
25 | sharedOptions.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
26 | })
27 | // for MSAL (V2)
28 | //.AddAzureAdBearerV2(options => Configuration.Bind("AzureAd", options));
29 | // for ADAL (V1)
30 | .AddAzureAdBearer(options => Configuration.Bind("AzureAd", options));
31 |
32 | services.AddLogging();
33 | services.AddCors(setup =>
34 | {
35 | setup.DefaultPolicyName = "open";
36 | setup.AddDefaultPolicy(p => {
37 | p.AllowAnyHeader();
38 | p.AllowAnyMethod();
39 | p.AllowAnyOrigin();
40 | p.AllowCredentials();
41 | });
42 | });
43 |
44 | services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
45 | }
46 |
47 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
48 | public void Configure(IApplicationBuilder app, IHostingEnvironment env)
49 | {
50 | if (env.IsDevelopment())
51 | {
52 | app.UseDeveloperExceptionPage();
53 | }
54 | else
55 | {
56 | app.UseHsts();
57 | }
58 |
59 | app.UseCors("open");
60 | app.UseHttpsRedirection();
61 | app.UseAuthentication();
62 | app.UseMvc();
63 | }
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/src/server/Extensions/AuthenticationBuilderExtensionsV2.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Authentication;
2 | using Microsoft.AspNetCore.Authentication.JwtBearer;
3 | using Microsoft.Extensions.DependencyInjection;
4 | using Microsoft.Extensions.Options;
5 | using Microsoft.IdentityModel.Tokens;
6 | using ServiceApp.Models;
7 | using System;
8 |
9 | namespace ServiceApp.Extensions
10 | {
11 | ///
12 | /// MSAL JwtBearer Configuration (v2)
13 | ///
14 | public static class AzureAdServiceCollectionExtensionsV2
15 | {
16 |
17 | public static AuthenticationBuilder AddAzureAdBearerV2(this AuthenticationBuilder builder, Action configureOptions)
18 | {
19 | builder.Services.Configure(configureOptions);
20 | builder.Services.AddSingleton, ConfigureAzureOptions>();
21 | builder.AddJwtBearer();
22 | return builder;
23 | }
24 |
25 | private class ConfigureAzureOptions : IConfigureNamedOptions
26 | {
27 | private readonly AzureAdOptions _azureOptions;
28 |
29 | public ConfigureAzureOptions(IOptions azureOptions)
30 | {
31 | _azureOptions = azureOptions.Value;
32 | }
33 |
34 | public void Configure(string name, JwtBearerOptions options)
35 | {
36 | options.Audience = _azureOptions.ClientId;
37 | options.Authority = $"{_azureOptions.Instance}{_azureOptions.TenantId}/v2.0/";
38 | options.TokenValidationParameters.ValidateIssuer = true;
39 | options.TokenValidationParameters.IssuerValidator = ValidateIssuer;
40 | }
41 |
42 | private string ValidateIssuer(string issuer, SecurityToken securityToken, TokenValidationParameters validationParameters)
43 | {
44 | Uri issuerUri = new Uri(issuer);
45 | Uri knownIssuerUri = new Uri(_azureOptions.IssuerV2);
46 |
47 | if (knownIssuerUri.AbsolutePath.Equals(issuerUri.AbsolutePath, StringComparison.OrdinalIgnoreCase))
48 | {
49 | return issuer;
50 | }
51 | else
52 | {
53 | throw new SecurityTokenInvalidIssuerException("Unknown issuer");
54 | }
55 | }
56 |
57 | public void Configure(JwtBearerOptions options)
58 | {
59 | Configure(Options.DefaultName, options);
60 | }
61 | }
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/src/azure-ad-setup/notes.txt:
--------------------------------------------------------------------------------
1 | Step1: Create the server application
2 | az ad app create --display-name WebServer-OpenIDConnect-DotNet
3 | --key-type Password
4 | --native-app false
5 | --password LetMeIn101
6 | --oauth2-allow-implicit-flow true
7 | --identifier-uris https://localhost:5001
8 | --app-roles @manifest.json
9 |
10 | Step2: Create Service Principal for Application (Optional)
11 | az ad sp create --id [REPLACE WITH CLIENTID FROM STEP 1]
12 | [--subscription]
13 |
14 | Step3: Create Client application
15 | az ad app create --display-name WebApp-OpenIDConnect-DotNet
16 | --key-type Password
17 | --native-app false
18 | --password LetMeIn101
19 | --oauth2-allow-implicit-flow true
20 | --identifier-uris https://localhost:4200
21 | --homepage https://localhost:4200/home
22 | --reply-urls https://localhost:4200/frameredirect http://localhost:4200/frameredirect
23 | --required-resource-accesses @manifest2.json
24 |
25 | Step4: Convert Client App to SP (Optional)
26 | az ad sp create --id [REPLACE WITH CLIENTID FROM STEP 3]
27 |
28 | Step5: Update the Client Apps --required-resource-accesses
29 | az ad app update --id [REPLACE WITH CLIENTID FROM STEP 3] --required-resource-accesses @manifest2.json
30 |
31 | Step6: Create a Client Secret for Client App (Optional, needed for Client Credentials Flow)
32 | az ad app credential reset --id [REPLACE WITH CLIENTID FROM STEP 3] --append --credential-description secretkey --password LetMeIn101 --years 2
33 |
34 | Step7: Add Secret
35 | az ad app credential reset
36 | --id [REPLACE WITH CLIENTID FROM STEP 3]
37 | --append
38 | --credential-description secretkey
39 | --password LetMeIn101
40 | --years 2
41 |
42 | Step 8: Grant accesses
43 | az ad app permission grant --id [REPLACE WITH CLIENTID FROM STEP 3] --api [REPLACE WITH CLIENTID FROM STEP 1]
44 |
45 | Manual Edits:
46 | For some reason, the client app settings didn't stick. I had to manually add the redirect url and oauth2AllowImplicitFlow to manifest
47 | "replyUrls": [
48 | "http://localhost:4200/frameredirect",
49 | "https://localhost:4200/frameredirect"
50 | ],
51 |
52 | and
53 |
54 | "oauth2AllowImplicitFlow": true,
55 |
56 |
57 | Additional Info:
58 | // See list of permissions on a client app
59 | az ad app permission list --id [REPLACE WITH CLIENTID]
60 |
61 | // Adding Delegated permissions creates the following in manifest
62 | "requiredResourceAccess": [
63 | {
64 | "resourceAppId": "f8f1e8d3-dfd1-400f-93b6-30b7404d2c9a", <- This is the AppID
65 | "resourceAccess": [
66 | {
67 | "id": "4da72b0f-1f95-4d53-8b8d-ed070fd881cf", <- This is the OAuth2 Permissions
68 | "type": "Scope"
69 | }
70 | ]
71 | }
72 | ],
--------------------------------------------------------------------------------
/src/client/spa-app/src/app/common/basehttp.service.ts:
--------------------------------------------------------------------------------
1 | import { Observable, throwError } from "rxjs";
2 | import { tap } from 'rxjs/operators';
3 | import { HttpClient, HttpHeaders, HttpErrorResponse } from '@angular/common/http';
4 |
5 | export class BaseHttpService{
6 |
7 | endpoint = '';
8 | headers = new HttpHeaders({ 'Content-Type': 'application/json' });
9 |
10 | constructor(private httpClient: HttpClient, private baseUrl: string, private controller: string) {
11 | this.endpoint = baseUrl + '/api/' + controller + '/';
12 | }
13 | get(id: number): Observable | T {
14 | return this.httpClient.get(this.endpoint + '/' + id, { headers: this.headers })
15 | .pipe(
16 | tap( // Log the result or error
17 | data => {
18 | // choose to log
19 | },
20 | error => this.handleError(error)
21 | )
22 | );
23 | }
24 |
25 | getAll(): Observable | T[] {
26 |
27 | return this.httpClient.get(this.endpoint, { headers: this.headers })
28 | .pipe(
29 | tap( // Log the result or error
30 | data => {
31 | // choose to log
32 | },
33 | error => this.handleError(error)
34 | )
35 | );
36 | }
37 |
38 | put(item: T): Observable | T {
39 | return this.httpClient.put(this.endpoint + 'all', JSON.stringify(item), { headers: this.headers })
40 | .pipe(
41 | tap( // Log the result or error
42 | data => {
43 | // choose to log
44 | },
45 | error => this.handleError(error)
46 | )
47 | );
48 | }
49 |
50 | post(item: T): Observable | any {
51 | return this.httpClient.post(this.endpoint, JSON.stringify(item), { headers: this.headers })
52 | .pipe(
53 | tap( // Log the result or error
54 | data => {
55 | // choose to log
56 | },
57 | error => this.handleError(error)
58 | )
59 | );
60 | }
61 |
62 | delete(id: number): Observable | any {
63 | return this.httpClient.delete(this.endpoint + '/' + id, { headers: this.headers })
64 | .pipe(
65 | tap( // Log the result or error
66 | data => {
67 | // choose to log
68 | },
69 | error => this.handleError(error)
70 | )
71 | );
72 | }
73 |
74 | private handleError(error: HttpErrorResponse) {
75 | if (error.error instanceof ErrorEvent) {
76 | // A client-side or network error occurred. Handle it accordingly.
77 | console.error('An error occurred:', error.error.message);
78 | } else {
79 | // The backend returned an unsuccessful response code.
80 | // The response body may contain clues as to what went wrong,
81 | console.error(
82 | `Backend returned code ${error.status}, ` +
83 | `body was: ${error.error}`);
84 | }
85 | // return an observable with a user-facing error message
86 | return throwError(
87 | 'Something bad happened; please try again later.');
88 | };
89 | }
--------------------------------------------------------------------------------
/src/client/spa-app/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "rulesDirectory": [
3 | "node_modules/codelyzer"
4 | ],
5 | "rules": {
6 | "arrow-return-shorthand": true,
7 | "callable-types": true,
8 | "class-name": true,
9 | "comment-format": [
10 | true,
11 | "check-space"
12 | ],
13 | "curly": true,
14 | "deprecation": {
15 | "severity": "warn"
16 | },
17 | "eofline": true,
18 | "forin": true,
19 | "import-blacklist": [
20 | true,
21 | "rxjs/Rx"
22 | ],
23 | "import-spacing": true,
24 | "indent": [
25 | true,
26 | "spaces"
27 | ],
28 | "interface-over-type-literal": true,
29 | "label-position": true,
30 | "max-line-length": [
31 | true,
32 | 140
33 | ],
34 | "member-access": false,
35 | "member-ordering": [
36 | true,
37 | {
38 | "order": [
39 | "static-field",
40 | "instance-field",
41 | "static-method",
42 | "instance-method"
43 | ]
44 | }
45 | ],
46 | "no-arg": true,
47 | "no-bitwise": true,
48 | "no-console": [
49 | true,
50 | "debug",
51 | "info",
52 | "time",
53 | "timeEnd",
54 | "trace"
55 | ],
56 | "no-construct": true,
57 | "no-debugger": true,
58 | "no-duplicate-super": true,
59 | "no-empty": false,
60 | "no-empty-interface": true,
61 | "no-eval": true,
62 | "no-inferrable-types": [
63 | true,
64 | "ignore-params"
65 | ],
66 | "no-misused-new": true,
67 | "no-non-null-assertion": true,
68 | "no-redundant-jsdoc": true,
69 | "no-shadowed-variable": true,
70 | "no-string-literal": false,
71 | "no-string-throw": true,
72 | "no-switch-case-fall-through": true,
73 | "no-trailing-whitespace": true,
74 | "no-unnecessary-initializer": true,
75 | "no-unused-expression": true,
76 | "no-use-before-declare": true,
77 | "no-var-keyword": true,
78 | "object-literal-sort-keys": false,
79 | "one-line": [
80 | true,
81 | "check-open-brace",
82 | "check-catch",
83 | "check-else",
84 | "check-whitespace"
85 | ],
86 | "prefer-const": true,
87 | "quotemark": [
88 | true,
89 | "single"
90 | ],
91 | "radix": true,
92 | "semicolon": [
93 | true,
94 | "always"
95 | ],
96 | "triple-equals": [
97 | true,
98 | "allow-null-check"
99 | ],
100 | "typedef-whitespace": [
101 | true,
102 | {
103 | "call-signature": "nospace",
104 | "index-signature": "nospace",
105 | "parameter": "nospace",
106 | "property-declaration": "nospace",
107 | "variable-declaration": "nospace"
108 | }
109 | ],
110 | "unified-signatures": true,
111 | "variable-name": false,
112 | "whitespace": [
113 | true,
114 | "check-branch",
115 | "check-decl",
116 | "check-operator",
117 | "check-separator",
118 | "check-type"
119 | ],
120 | "no-output-on-prefix": true,
121 | "use-input-property-decorator": true,
122 | "use-output-property-decorator": true,
123 | "use-host-property-decorator": true,
124 | "no-input-rename": true,
125 | "no-output-rename": true,
126 | "use-life-cycle-interface": true,
127 | "use-pipe-transform-interface": true,
128 | "component-class-suffix": true,
129 | "directive-class-suffix": true
130 | }
131 | }
132 |
--------------------------------------------------------------------------------
/src/client/spa-app/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/guide/browser-support
15 | */
16 |
17 | /***************************************************************************************************
18 | * BROWSER POLYFILLS
19 | */
20 |
21 | /** IE9, IE10 and IE11 requires all of the following polyfills. **/
22 | // import 'core-js/es6/symbol';
23 | // import 'core-js/es6/object';
24 | // import 'core-js/es6/function';
25 | // import 'core-js/es6/parse-int';
26 | // import 'core-js/es6/parse-float';
27 | // import 'core-js/es6/number';
28 | // import 'core-js/es6/math';
29 | // import 'core-js/es6/string';
30 | // import 'core-js/es6/date';
31 | // import 'core-js/es6/regexp';
32 | // import 'core-js/es6/map';
33 | // import 'core-js/es6/weak-map';
34 | // import 'core-js/es6/set';
35 |
36 | /**
37 | * If your app need to indexed by Google Search, your app require polyfills 'core-js/es6/array'
38 | * Google bot use ES5.
39 | * FYI: Googlebot uses a renderer following the similar spec to Chrome 41.
40 | * https://developers.google.com/search/docs/guides/rendering
41 | **/
42 | // import 'core-js/es6/array';
43 |
44 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */
45 | // import 'classlist.js'; // Run `npm install --save classlist.js`.
46 |
47 | /** IE10 and IE11 requires the following for the Reflect API. */
48 | // import 'core-js/es6/reflect';
49 |
50 | /**
51 | * Web Animations `@angular/platform-browser/animations`
52 | * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari.
53 | * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0).
54 | **/
55 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`.
56 |
57 | /**
58 | * By default, zone.js will patch all possible macroTask and DomEvents
59 | * user can disable parts of macroTask/DomEvents patch by setting following flags
60 | */
61 |
62 | // (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame
63 | // (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick
64 | // (window as any).__zone_symbol__BLACK_LISTED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames
65 |
66 | /*
67 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js
68 | * with the following flag, it will bypass `zone.js` patch for IE/Edge
69 | */
70 | // (window as any).__Zone_enable_cross_context_check = true;
71 |
72 | /***************************************************************************************************
73 | * Zone JS is required by default for Angular itself.
74 | */
75 | import 'zone.js/dist/zone'; // Included with Angular CLI.
76 |
77 |
78 |
79 | /***************************************************************************************************
80 | * APPLICATION IMPORTS
81 | */
82 |
--------------------------------------------------------------------------------
/src/client/spa-app/angular.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
3 | "version": 1,
4 | "newProjectRoot": "projects",
5 | "projects": {
6 | "help": {
7 | "root": "",
8 | "sourceRoot": "src",
9 | "projectType": "application",
10 | "prefix": "app",
11 | "schematics": {},
12 | "architect": {
13 | "build": {
14 | "builder": "@angular-devkit/build-angular:browser",
15 | "options": {
16 | "outputPath": "dist/help",
17 | "index": "src/index.html",
18 | "main": "src/main.ts",
19 | "polyfills": "src/polyfills.ts",
20 | "tsConfig": "src/tsconfig.app.json",
21 | "assets": [
22 | "src/favicon.ico",
23 | "src/assets"
24 | ],
25 | "styles": [
26 | "src/public/bootstrap/dist/css/bootstrap.min.css",
27 | "src/styles.css"
28 | ],
29 | "scripts": [
30 | "src/public/jquery/dist/jquery.min.js",
31 | "src/public/bootstrap/dist/js/bootstrap.min.js",
32 | "./node_modules/adal-angular/dist/adal.min.js"
33 | ]
34 | },
35 | "configurations": {
36 | "production": {
37 | "fileReplacements": [
38 | {
39 | "replace": "src/environments/environment.ts",
40 | "with": "src/environments/environment.prod.ts"
41 | }
42 | ],
43 | "optimization": true,
44 | "outputHashing": "all",
45 | "sourceMap": false,
46 | "extractCss": true,
47 | "namedChunks": false,
48 | "aot": true,
49 | "extractLicenses": true,
50 | "vendorChunk": false,
51 | "buildOptimizer": true,
52 | "budgets": [
53 | {
54 | "type": "initial",
55 | "maximumWarning": "2mb",
56 | "maximumError": "5mb"
57 | }
58 | ]
59 | }
60 | }
61 | },
62 | "serve": {
63 | "builder": "@angular-devkit/build-angular:dev-server",
64 | "options": {
65 | "browserTarget": "help:build"
66 | },
67 | "configurations": {
68 | "production": {
69 | "browserTarget": "help:build:production"
70 | }
71 | }
72 | },
73 | "extract-i18n": {
74 | "builder": "@angular-devkit/build-angular:extract-i18n",
75 | "options": {
76 | "browserTarget": "help:build"
77 | }
78 | },
79 | "test": {
80 | "builder": "@angular-devkit/build-angular:karma",
81 | "options": {
82 | "main": "src/test.ts",
83 | "polyfills": "src/polyfills.ts",
84 | "tsConfig": "src/tsconfig.spec.json",
85 | "karmaConfig": "src/karma.conf.js",
86 | "styles": [
87 | "src/styles.css"
88 | ],
89 | "scripts": [],
90 | "assets": [
91 | "src/favicon.ico",
92 | "src/assets"
93 | ]
94 | }
95 | },
96 | "lint": {
97 | "builder": "@angular-devkit/build-angular:tslint",
98 | "options": {
99 | "tsConfig": [
100 | "src/tsconfig.app.json",
101 | "src/tsconfig.spec.json"
102 | ],
103 | "exclude": [
104 | "**/node_modules/**"
105 | ]
106 | }
107 | }
108 | }
109 | },
110 | "help-e2e": {
111 | "root": "e2e/",
112 | "projectType": "application",
113 | "prefix": "",
114 | "architect": {
115 | "e2e": {
116 | "builder": "@angular-devkit/build-angular:protractor",
117 | "options": {
118 | "protractorConfig": "e2e/protractor.conf.js",
119 | "devServerTarget": "help:serve"
120 | },
121 | "configurations": {
122 | "production": {
123 | "devServerTarget": "help:serve:production"
124 | }
125 | }
126 | },
127 | "lint": {
128 | "builder": "@angular-devkit/build-angular:tslint",
129 | "options": {
130 | "tsConfig": "e2e/tsconfig.e2e.json",
131 | "exclude": [
132 | "**/node_modules/**"
133 | ]
134 | }
135 | }
136 | }
137 | }
138 | },
139 | "defaultProject": "help"
140 | }
--------------------------------------------------------------------------------