diff options
author | 杨宇千 <crupest@outlook.com> | 2019-04-13 15:47:40 +0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2019-04-13 15:47:40 +0800 |
commit | 56c51bdf844ce1e3642dcdc4099187e7e57008c7 (patch) | |
tree | 1aa37565aad734b604eb94ed3a62db2308f4b30d | |
parent | 19cae15eba2bcede41b818e1b8ab7fd5ac92eb05 (diff) | |
parent | 108ea333534445a8c76d4db632ebf21abf426c71 (diff) | |
download | timeline-56c51bdf844ce1e3642dcdc4099187e7e57008c7.tar.gz timeline-56c51bdf844ce1e3642dcdc4099187e7e57008c7.tar.bz2 timeline-56c51bdf844ce1e3642dcdc4099187e7e57008c7.zip |
Merge pull request #20 from crupest/separate
Separate front end and back end.
132 files changed, 869 insertions, 3121 deletions
diff --git a/Timeline.Tests/AuthorizationUnitTest.cs b/Timeline.Tests/AuthorizationUnitTest.cs index 2693366c..e450af06 100644 --- a/Timeline.Tests/AuthorizationUnitTest.cs +++ b/Timeline.Tests/AuthorizationUnitTest.cs @@ -10,9 +10,9 @@ namespace Timeline.Tests { public class AuthorizationUnitTest : IClassFixture<WebApplicationFactory<Startup>> { - private const string NeedAuthorizeUrl = "api/test/User/NeedAuthorize"; - private const string BothUserAndAdminUrl = "api/test/User/BothUserAndAdmin"; - private const string OnlyAdminUrl = "api/test/User/OnlyAdmin"; + private const string NeedAuthorizeUrl = "Test/User/NeedAuthorize"; + private const string BothUserAndAdminUrl = "Test/User/BothUserAndAdmin"; + private const string OnlyAdminUrl = "Test/User/OnlyAdmin"; private readonly WebApplicationFactory<Startup> _factory; diff --git a/Timeline.Tests/Helpers/Authentication/AuthenticationHttpClientExtensions.cs b/Timeline.Tests/Helpers/Authentication/AuthenticationHttpClientExtensions.cs index ccb2a372..1949df9b 100644 --- a/Timeline.Tests/Helpers/Authentication/AuthenticationHttpClientExtensions.cs +++ b/Timeline.Tests/Helpers/Authentication/AuthenticationHttpClientExtensions.cs @@ -10,7 +10,7 @@ namespace Timeline.Tests.Helpers.Authentication { public static class AuthenticationHttpClientExtensions { - private const string CreateTokenUrl = "/api/User/CreateToken"; + private const string CreateTokenUrl = "/User/CreateToken"; public static async Task<CreateTokenResponse> CreateUserTokenAsync(this HttpClient client, string username, string password, bool assertSuccess = true) { diff --git a/Timeline.Tests/Helpers/WebApplicationFactoryExtensions.cs b/Timeline.Tests/Helpers/WebApplicationFactoryExtensions.cs index bb8fc71b..4a7f87fb 100644 --- a/Timeline.Tests/Helpers/WebApplicationFactoryExtensions.cs +++ b/Timeline.Tests/Helpers/WebApplicationFactoryExtensions.cs @@ -1,6 +1,10 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Timeline.Models; +using Timeline.Services; using Xunit.Abstractions; namespace Timeline.Tests.Helpers @@ -16,6 +20,52 @@ namespace Timeline.Tests.Helpers .ConfigureLogging(logging => { logging.AddXunit(outputHelper); + }) + .ConfigureServices(services => + { + var serviceProvider = new ServiceCollection() + .AddEntityFrameworkInMemoryDatabase() + .BuildServiceProvider(); + + services.AddDbContext<DatabaseContext>(options => + { + options.UseInMemoryDatabase("timeline"); + options.UseInternalServiceProvider(serviceProvider); + }); + + var sp = services.BuildServiceProvider(); + + // Create a scope to obtain a reference to the database + // context (ApplicationDbContext). + using (var scope = sp.CreateScope()) + { + var scopedServices = scope.ServiceProvider; + var db = scopedServices.GetRequiredService<DatabaseContext>(); + + var passwordService = new PasswordService(null); + + // Ensure the database is created. + db.Database.EnsureCreated(); + + db.Users.AddRange(new User[] { + new User + { + Id = 0, + Name = "user", + EncryptedPassword = passwordService.HashPassword("user"), + RoleString = "user" + }, + new User + { + Id = 0, + Name = "admin", + EncryptedPassword = passwordService.HashPassword("admin"), + RoleString = "user,admin" + } + }); + + db.SaveChanges(); + } }); }); } diff --git a/Timeline.Tests/JwtTokenUnitTest.cs b/Timeline.Tests/JwtTokenUnitTest.cs index 7e881895..3c03dfc2 100644 --- a/Timeline.Tests/JwtTokenUnitTest.cs +++ b/Timeline.Tests/JwtTokenUnitTest.cs @@ -12,8 +12,8 @@ namespace Timeline.Tests { public class JwtTokenUnitTest : IClassFixture<WebApplicationFactory<Startup>> { - private const string CreateTokenUrl = "/api/User/CreateToken"; - private const string ValidateTokenUrl = "/api/User/ValidateToken"; + private const string CreateTokenUrl = "User/CreateToken"; + private const string ValidateTokenUrl = "User/ValidateToken"; private readonly WebApplicationFactory<Startup> _factory; diff --git a/Timeline.Tests/Timeline.Tests-CI.csproj b/Timeline.Tests/Timeline.Tests-CI.csproj deleted file mode 100644 index 3639dbf8..00000000 --- a/Timeline.Tests/Timeline.Tests-CI.csproj +++ /dev/null @@ -1,27 +0,0 @@ -<Project Sdk="Microsoft.NET.Sdk.Web"> - - <PropertyGroup> - <TargetFramework>netcoreapp2.2</TargetFramework> - </PropertyGroup> - - <ItemGroup> - <PackageReference Include="Microsoft.AspNetCore.App" /> - <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="2.2.0" /> - <PackageReference Include="Microsoft.Extensions.Logging.Testing" Version="2.2.0-rtm-35646" /> - <PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.9.0" /> - <PackageReference Include="xunit" Version="2.4.1" /> - <PackageReference Include="xunit.runner.visualstudio" Version="2.4.1"> - <PrivateAssets>all</PrivateAssets> - <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> - </PackageReference> - </ItemGroup> - - <ItemGroup> - <ProjectReference Include="..\Timeline\Timeline-CI.csproj" /> - </ItemGroup> - - <ItemGroup> - <Folder Include="Properties\" /> - </ItemGroup> - -</Project> diff --git a/Timeline.Tests/Timeline.Tests.csproj b/Timeline.Tests/Timeline.Tests.csproj index cbb8ab59..57e04fc0 100644 --- a/Timeline.Tests/Timeline.Tests.csproj +++ b/Timeline.Tests/Timeline.Tests.csproj @@ -19,9 +19,4 @@ <ItemGroup> <ProjectReference Include="..\Timeline\Timeline.csproj" /> </ItemGroup> - - <ItemGroup> - <Folder Include="Properties\" /> - </ItemGroup> - </Project> diff --git a/Timeline/ClientApp/.editorconfig b/Timeline/ClientApp/.editorconfig deleted file mode 100644 index 6e87a003..00000000 --- a/Timeline/ClientApp/.editorconfig +++ /dev/null @@ -1,13 +0,0 @@ -# Editor configuration, see http://editorconfig.org -root = true - -[*] -charset = utf-8 -indent_style = space -indent_size = 2 -insert_final_newline = true -trim_trailing_whitespace = true - -[*.md] -max_line_length = off -trim_trailing_whitespace = false diff --git a/Timeline/ClientApp/.gitignore b/Timeline/ClientApp/.gitignore deleted file mode 100644 index e1f679be..00000000 --- a/Timeline/ClientApp/.gitignore +++ /dev/null @@ -1,40 +0,0 @@ -# See http://help.github.com/ignore-files/ for more about ignoring files. - -# compiled output -/dist -/dist-server -/tmp -/out-tsc - -# dependencies -/node_modules - -# IDEs and editors -/.idea -.project -.classpath -.c9/ -*.launch -.settings/ -*.sublime-workspace - -# IDE - VSCode -.vscode/* -!.vscode/settings.json -!.vscode/tasks.json -!.vscode/launch.json -!.vscode/extensions.json - -# misc -/.sass-cache -/connect.lock -/coverage -/libpeerconnection.log -npm-debug.log -yarn-error.log -testem.log -/typings - -# System Files -.DS_Store -Thumbs.db diff --git a/Timeline/ClientApp/.prettierrc.json b/Timeline/ClientApp/.prettierrc.json deleted file mode 100644 index 6c70cb20..00000000 --- a/Timeline/ClientApp/.prettierrc.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "printWidth": 140, - "singleQuote": true -} diff --git a/Timeline/ClientApp/.vscode/launch.json b/Timeline/ClientApp/.vscode/launch.json deleted file mode 100644 index 73e17a72..00000000 --- a/Timeline/ClientApp/.vscode/launch.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - { - "type": "chrome", - "request": "launch", - "name": "Launch app", - "url": "https://localhost:5001", - "webRoot": "${workspaceFolder}" - } - ] -} diff --git a/Timeline/ClientApp/README.md b/Timeline/ClientApp/README.md deleted file mode 100644 index 9f66fc75..00000000 --- a/Timeline/ClientApp/README.md +++ /dev/null @@ -1,27 +0,0 @@ -# Timeline - -This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 6.0.0. - -## Development server - -Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files. - -## Code scaffolding - -Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. - -## Build - -Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `--prod` flag for a production build. - -## Running unit tests - -Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). - -## Running end-to-end tests - -Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/). - -## Further help - -To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md). diff --git a/Timeline/ClientApp/angular.json b/Timeline/ClientApp/angular.json deleted file mode 100644 index 9a696714..00000000 --- a/Timeline/ClientApp/angular.json +++ /dev/null @@ -1,151 +0,0 @@ -{ - "$schema": "./node_modules/@angular/cli/lib/config/schema.json", - "version": 1, - "cli": { - "packageManager": "yarn" - }, - "newProjectRoot": "projects", - "projects": { - "Timeline": { - "root": "", - "sourceRoot": "src", - "projectType": "application", - "prefix": "app", - "schematics": {}, - "architect": { - "build": { - "builder": "@angular-devkit/build-angular:browser", - "options": { - "progress": true, - "extractCss": true, - "outputPath": "dist", - "index": "src/index.html", - "main": "src/main.ts", - "polyfills": "src/polyfills.ts", - "tsConfig": "src/tsconfig.app.json", - "assets": [ - "src/assets" - ], - "styles": [ - "src/styles.css" - ], - "scripts": [] - }, - "configurations": { - "production": { - "fileReplacements": [ - { - "replace": "src/environments/environment.ts", - "with": "src/environments/environment.prod.ts" - } - ], - "optimization": true, - "outputHashing": "all", - "sourceMap": false, - "extractCss": true, - "namedChunks": false, - "aot": true, - "extractLicenses": true, - "vendorChunk": false, - "buildOptimizer": true - } - } - }, - "serve": { - "builder": "@angular-devkit/build-angular:dev-server", - "options": { - "browserTarget": "Timeline:build" - }, - "configurations": { - "production": { - "browserTarget": "Timeline:build:production" - } - } - }, - "extract-i18n": { - "builder": "@angular-devkit/build-angular:extract-i18n", - "options": { - "browserTarget": "Timeline:build" - } - }, - "test": { - "builder": "@angular-devkit/build-angular:karma", - "options": { - "main": "src/test.ts", - "polyfills": "src/polyfills.ts", - "tsConfig": "src/tsconfig.spec.json", - "karmaConfig": "src/karma.conf.js", - "styles": [ - "src/styles.css" - ], - "scripts": [], - "assets": [ - "src/assets" - ] - } - }, - "lint": { - "builder": "@angular-devkit/build-angular:tslint", - "options": { - "tsConfig": [ - "src/tsconfig.app.json", - "src/tsconfig.spec.json" - ], - "exclude": [ - "**/node_modules/**" - ] - } - }, - "server": { - "builder": "@angular-devkit/build-angular:server", - "options": { - "outputPath": "dist-server", - "main": "src/main.ts", - "tsConfig": "src/tsconfig.server.json" - }, - "configurations": { - "dev": { - "optimization": true, - "outputHashing": "all", - "sourceMap": false, - "namedChunks": false, - "extractLicenses": true, - "vendorChunk": true - }, - "production": { - "optimization": true, - "outputHashing": "all", - "sourceMap": false, - "namedChunks": false, - "extractLicenses": true, - "vendorChunk": false - } - } - } - } - }, - "Timeline-e2e": { - "root": "e2e/", - "projectType": "application", - "architect": { - "e2e": { - "builder": "@angular-devkit/build-angular:protractor", - "options": { - "protractorConfig": "e2e/protractor.conf.js", - "devServerTarget": "Timeline:serve" - } - }, - "lint": { - "builder": "@angular-devkit/build-angular:tslint", - "options": { - "tsConfig": "e2e/tsconfig.e2e.json", - "exclude": [ - "**/node_modules/**" - ] - } - } - } - } - }, - "defaultProject": "Timeline" -} diff --git a/Timeline/ClientApp/e2e/protractor.conf.js b/Timeline/ClientApp/e2e/protractor.conf.js deleted file mode 100644 index 86776a39..00000000 --- a/Timeline/ClientApp/e2e/protractor.conf.js +++ /dev/null @@ -1,28 +0,0 @@ -// Protractor configuration file, see link for more information -// https://github.com/angular/protractor/blob/master/lib/config.ts - -const { SpecReporter } = require('jasmine-spec-reporter'); - -exports.config = { - allScriptsTimeout: 11000, - specs: [ - './src/**/*.e2e-spec.ts' - ], - capabilities: { - 'browserName': 'chrome' - }, - directConnect: true, - baseUrl: 'http://localhost:4200/', - framework: 'jasmine', - jasmineNodeOpts: { - showColors: true, - defaultTimeoutInterval: 30000, - print: function() {} - }, - onPrepare() { - require('ts-node').register({ - project: require('path').join(__dirname, './tsconfig.e2e.json') - }); - jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); - } -};
\ No newline at end of file diff --git a/Timeline/ClientApp/e2e/src/app.e2e-spec.ts b/Timeline/ClientApp/e2e/src/app.e2e-spec.ts deleted file mode 100644 index 5b3b4b27..00000000 --- a/Timeline/ClientApp/e2e/src/app.e2e-spec.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { AppPage } from './app.po'; - -describe('App', () => { - let page: AppPage; - - beforeEach(() => { - page = new AppPage(); - }); - - it('should display welcome message', () => { - page.navigateTo(); - expect(page.getMainHeading()).toEqual('Hello, world!'); - }); -}); diff --git a/Timeline/ClientApp/e2e/src/app.po.ts b/Timeline/ClientApp/e2e/src/app.po.ts deleted file mode 100644 index 24bc8b3c..00000000 --- a/Timeline/ClientApp/e2e/src/app.po.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { browser, by, element } from 'protractor'; - -export class AppPage { - navigateTo() { - return browser.get('/'); - } - - getMainHeading() { - return element(by.css('app-root h1')).getText(); - } -} diff --git a/Timeline/ClientApp/e2e/tsconfig.e2e.json b/Timeline/ClientApp/e2e/tsconfig.e2e.json deleted file mode 100644 index a6dd6220..00000000 --- a/Timeline/ClientApp/e2e/tsconfig.e2e.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "extends": "../tsconfig.json", - "compilerOptions": { - "outDir": "../out-tsc/app", - "module": "commonjs", - "target": "es5", - "types": [ - "jasmine", - "jasminewd2", - "node" - ] - } -}
\ No newline at end of file diff --git a/Timeline/ClientApp/package.json b/Timeline/ClientApp/package.json deleted file mode 100644 index 7c6d28f0..00000000 --- a/Timeline/ClientApp/package.json +++ /dev/null @@ -1,59 +0,0 @@ -{ - "name": "timeline", - "version": "0.0.0", - "scripts": { - "ng": "ng", - "start": "ng serve", - "start-dotnet": "dotnet run --project ../Timeline.csproj", - "build": "ng build", - "build:ssr": "ng run Timeline:server:dev", - "test": "ng test", - "lint": "ng lint", - "e2e": "ng e2e" - }, - "private": true, - "dependencies": { - "@angular/animations": "^7.2.4", - "@angular/cdk": "^7.3.2", - "@angular/common": "^7.2.4", - "@angular/compiler": "^7.2.4", - "@angular/core": "^7.2.4", - "@angular/forms": "^7.2.4", - "@angular/http": "^7.2.4", - "@angular/material": "^7.3.2", - "@angular/platform-browser": "^7.2.4", - "@angular/platform-browser-dynamic": "^7.2.4", - "@angular/platform-server": "^7.2.4", - "@angular/router": "^7.2.4", - "@nguniversal/module-map-ngfactory-loader": "^7.1.0", - "aspnet-prerendering": "^3.0.1", - "core-js": "^2.6.3", - "rxjs": "^6.3.3", - "zone.js": "^0.8.29" - }, - "devDependencies": { - "@angular-devkit/build-angular": "^0.12.3", - "@angular/cli": "^7.3.1", - "@angular/compiler-cli": "^7.2.4", - "@angular/language-service": "^7.2.4", - "@types/jasmine": "^3.3.8", - "@types/jasminewd2": "^2.0.6", - "@types/node": "^10.12.19", - "codelyzer": "^4.5.0", - "jasmine-core": "^3.3.0", - "jasmine-spec-reporter": "^4.2.1", - "karma": "^4.0.0", - "karma-chrome-launcher": "^2.2.0", - "karma-coverage-istanbul-reporter": "^2.0.4", - "karma-jasmine": "^2.0.1", - "karma-jasmine-html-reporter": "^1.4.0", - "karma-junit-reporter": "^1.2.0", - "tslint": "^5.12.1", - "typescript": "~3.2.4" - }, - "optionalDependencies": { - "node-sass": "^4.9.3", - "protractor": "^5.4.0", - "ts-node": "^5.0.1" - } -} diff --git a/Timeline/ClientApp/src/app/app.component.css b/Timeline/ClientApp/src/app/app.component.css deleted file mode 100644 index 13a44248..00000000 --- a/Timeline/ClientApp/src/app/app.component.css +++ /dev/null @@ -1,10 +0,0 @@ -a.icp-record { - color: grey; - text-decoration: none; - font-size: 0.8rem; - margin: 0 5vw; -} - -footer { - display: flex; -} diff --git a/Timeline/ClientApp/src/app/app.component.html b/Timeline/ClientApp/src/app/app.component.html deleted file mode 100644 index 92c88625..00000000 --- a/Timeline/ClientApp/src/app/app.component.html +++ /dev/null @@ -1,20 +0,0 @@ -<body> - <mat-toolbar color="primary" class="mat-elevation-z4"> - <a mat-button routerLink="/"> - <img width="30" height="30" src="assets/icon.svg">Timeline</a> - <a mat-button routerLink="/todo">TodoList</a> - <span class="fill-remaining-space"></span> - <a mat-icon-button [routerLink]="[{outlets: {user: ['login']}}]"> - <mat-icon>account_circle</mat-icon> - </a> - </mat-toolbar> - - <div> - <router-outlet></router-outlet> - </div> - - <footer> - <span class="fill-remaining-space"></span> - <a class="icp-record" href="http://www.miitbeian.gov.cn/state/outPortal/loginPortal.action" target=”_blank”>鄂ICP备18030913号-1</a> - </footer> -</body> diff --git a/Timeline/ClientApp/src/app/app.component.ts b/Timeline/ClientApp/src/app/app.component.ts deleted file mode 100644 index 33f33048..00000000 --- a/Timeline/ClientApp/src/app/app.component.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Component } from '@angular/core'; - -import { UserService } from './user/user.service'; - -@Component({ - selector: 'app-root', - templateUrl: './app.component.html', - styleUrls: ['./app.component.css'] -}) -export class AppComponent { - // never remove userService because we need it explicit constructing. - constructor(userService: UserService) { } -} diff --git a/Timeline/ClientApp/src/app/app.module.ts b/Timeline/ClientApp/src/app/app.module.ts deleted file mode 100644 index b75e10e2..00000000 --- a/Timeline/ClientApp/src/app/app.module.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { BrowserModule } from '@angular/platform-browser'; -import { NgModule } from '@angular/core'; -import { RouterModule } from '@angular/router'; -import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { MatIconModule, MatButtonModule, MatToolbarModule, MatDialogModule } from '@angular/material'; - -import { AppComponent } from './app.component'; - -import { TodoModule } from './todo/todo.module'; -import { HomeModule } from './home/home.module'; -import { UserModule } from './user/user.module'; -import { UserService } from './user/user.service'; - - -@NgModule({ - declarations: [AppComponent], - imports: [ - BrowserModule.withServerTransition({ appId: 'ng-cli-universal' }), - BrowserAnimationsModule, - MatIconModule, MatButtonModule, MatToolbarModule, MatDialogModule, - HomeModule, TodoModule, UserModule, - RouterModule.forRoot([ - { path: '', redirectTo: '/home', pathMatch: 'full' }, - ]) - ], - providers: [UserService], - bootstrap: [AppComponent] -}) -export class AppModule { } diff --git a/Timeline/ClientApp/src/app/app.server.module.ts b/Timeline/ClientApp/src/app/app.server.module.ts deleted file mode 100644 index cfb0e021..00000000 --- a/Timeline/ClientApp/src/app/app.server.module.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { NgModule } from '@angular/core'; -import { ServerModule } from '@angular/platform-server'; -import { ModuleMapLoaderModule } from '@nguniversal/module-map-ngfactory-loader'; -import { AppComponent } from './app.component'; -import { AppModule } from './app.module'; - -@NgModule({ - imports: [AppModule, ServerModule, ModuleMapLoaderModule], - bootstrap: [AppComponent] -}) -export class AppServerModule { } diff --git a/Timeline/ClientApp/src/app/home/home.component.css b/Timeline/ClientApp/src/app/home/home.component.css deleted file mode 100644 index 76297a9e..00000000 --- a/Timeline/ClientApp/src/app/home/home.component.css +++ /dev/null @@ -1,49 +0,0 @@ -p { - font-size: 2rem; - margin: 0; -} - -.bold { - font-weight: 600; -} - -#loginBox { - display: inline-grid; - grid-template: "username-label username-input" auto - "password-label password-input" auto - "login-button login-button" auto - "message message" auto - / max-content max-content; - align-items: center; - padding: 10px; - border: solid black 1px; -} - -#usernameLabel { - grid-area: username-label; -} - -#usernameInput { - grid-area: username-input; - margin: 2px; -} - -#passwordLabel { - grid-area: password-label; -} - -#passwordInput { - grid-area: password-input; - margin: 2px; -} - -#loginButton { - grid-area: login-button; - justify-self: end; -} - -#loginMessage { - grid-area: message; - justify-self: end; - color: red; -} diff --git a/Timeline/ClientApp/src/app/home/home.component.html b/Timeline/ClientApp/src/app/home/home.component.html deleted file mode 100644 index 28ab3039..00000000 --- a/Timeline/ClientApp/src/app/home/home.component.html +++ /dev/null @@ -1,3 +0,0 @@ -<h2 class="mat-h2"> - This page is under <span class="bold">construction</span>! -</h2> diff --git a/Timeline/ClientApp/src/app/home/home.component.spec.ts b/Timeline/ClientApp/src/app/home/home.component.spec.ts deleted file mode 100644 index 74bedd08..00000000 --- a/Timeline/ClientApp/src/app/home/home.component.spec.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; - -import { HomeComponent } from './home.component'; - - -describe('HomeComponent', () => { - let component: HomeComponent; - let fixture: ComponentFixture<HomeComponent>; - - beforeEach(async(() => { - TestBed.configureTestingModule({ - declarations: [HomeComponent], - }) - .compileComponents(); - })); - - beforeEach(() => { - fixture = TestBed.createComponent(HomeComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/Timeline/ClientApp/src/app/home/home.component.ts b/Timeline/ClientApp/src/app/home/home.component.ts deleted file mode 100644 index 0cb0d0f5..00000000 --- a/Timeline/ClientApp/src/app/home/home.component.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Component } from '@angular/core'; - -@Component({ - selector: 'app-home', - templateUrl: './home.component.html', - styleUrls: ['./home.component.css'] -}) -export class HomeComponent { - -} diff --git a/Timeline/ClientApp/src/app/home/home.module.ts b/Timeline/ClientApp/src/app/home/home.module.ts deleted file mode 100644 index 98456238..00000000 --- a/Timeline/ClientApp/src/app/home/home.module.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { NgModule } from '@angular/core'; -import { CommonModule } from '@angular/common'; -import { RouterModule } from '@angular/router'; - -import { HomeComponent } from './home.component'; - -@NgModule({ - declarations: [HomeComponent], - imports: [ - CommonModule, - RouterModule.forChild([ - { path: 'home', component: HomeComponent } - ]) - ], - exports: [RouterModule] -}) -export class HomeModule { } diff --git a/Timeline/ClientApp/src/app/test-utilities/activated-route.mock.ts b/Timeline/ClientApp/src/app/test-utilities/activated-route.mock.ts deleted file mode 100644 index 40484387..00000000 --- a/Timeline/ClientApp/src/app/test-utilities/activated-route.mock.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { ParamMap, ActivatedRouteSnapshot, ActivatedRoute } from '@angular/router'; - -import { Observable, BehaviorSubject } from 'rxjs'; -import { map } from 'rxjs/operators'; - -import { PartialMock } from './mock'; - -export interface ParamMapCreator { [name: string]: string | string[]; } - -export class MockActivatedRouteSnapshot implements PartialMock<ActivatedRouteSnapshot> { - - private paramMapInternal: ParamMap; - - constructor({ mockParamMap }: { mockParamMap: ParamMapCreator } = { mockParamMap: {} }) { - this.paramMapInternal = { - keys: Object.keys(mockParamMap), - get(name: string): string | null { - const param = mockParamMap[name]; - if (typeof param === 'string') { - return param; - } else if (param instanceof Array) { - if (param.length === 0) { - return null; - } - return param[0]; - } - return null; - }, - getAll(name: string): string[] { - const param = mockParamMap[name]; - if (typeof param === 'string') { - return [param]; - } else if (param instanceof Array) { - return param; - } - return []; - }, - has(name: string): boolean { - return mockParamMap.hasOwnProperty(name); - } - }; - } - - get paramMap(): ParamMap { - return this.paramMapInternal; - } -} - -export class MockActivatedRoute implements PartialMock<ActivatedRoute> { - - snapshot$ = new BehaviorSubject<MockActivatedRouteSnapshot>(new MockActivatedRouteSnapshot()); - - get paramMap(): Observable<ParamMap> { - return this.snapshot$.pipe(map(snapshot => snapshot.paramMap)); - } - - get snapshot(): MockActivatedRouteSnapshot { - return this.snapshot$.value; - } - - pushSnapshot(snapshot: MockActivatedRouteSnapshot) { - this.snapshot$.next(snapshot); - } - - pushSnapshotWithParamMap(mockParamMap: ParamMapCreator) { - this.pushSnapshot(new MockActivatedRouteSnapshot({mockParamMap})); - } -} diff --git a/Timeline/ClientApp/src/app/test-utilities/mock.ts b/Timeline/ClientApp/src/app/test-utilities/mock.ts deleted file mode 100644 index c3e368f0..00000000 --- a/Timeline/ClientApp/src/app/test-utilities/mock.ts +++ /dev/null @@ -1,7 +0,0 @@ -export type Mock<T> = { - [P in keyof T]: T[P] extends Function ? T[P] : T[P] | Mock<T[P]>; -}; - -export type PartialMock<T> = { - [P in keyof T]?: T[P] extends Function ? T[P] : T[P] | PartialMock<T[P]> | Mock<T[P]>; -}; diff --git a/Timeline/ClientApp/src/app/test-utilities/router-link.mock.ts b/Timeline/ClientApp/src/app/test-utilities/router-link.mock.ts deleted file mode 100644 index 7f4cde4d..00000000 --- a/Timeline/ClientApp/src/app/test-utilities/router-link.mock.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Directive, Input } from '@angular/core'; - -@Directive({ - /* tslint:disable-next-line:directive-selector*/ - selector: '[routerLink]' -}) -export class RouterLinkStubDirective { - @Input('routerLink') linkParams: any; -} diff --git a/Timeline/ClientApp/src/app/test-utilities/storage.mock.ts b/Timeline/ClientApp/src/app/test-utilities/storage.mock.ts deleted file mode 100644 index 0ba5aa35..00000000 --- a/Timeline/ClientApp/src/app/test-utilities/storage.mock.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { Mock } from './mock'; -import { nullIfUndefined } from '../utilities/language-untilities'; - -export function createMockStorage(): Mock<Storage> { - const map: { [key: string]: string } = {}; - return { - get length(): number { - return Object.keys(map).length; - }, - key(index: number): string | null { - const keys = Object.keys(map); - if (index >= keys.length) { return null; } - return keys[index]; - }, - clear() { - Object.keys(map).forEach(key => delete map.key); - }, - getItem(key: string): string | null { - return nullIfUndefined(map[key]); - }, - setItem(key: string, value: string) { - map[key] = value; - }, - removeItem(key: string) { - delete map[key]; - } - }; -} diff --git a/Timeline/ClientApp/src/app/todo/todo-item.ts b/Timeline/ClientApp/src/app/todo/todo-item.ts deleted file mode 100644 index b19d8335..00000000 --- a/Timeline/ClientApp/src/app/todo/todo-item.ts +++ /dev/null @@ -1,6 +0,0 @@ -export interface TodoItem { - number: number; - title: string; - isClosed: boolean; - detailUrl: string; -} diff --git a/Timeline/ClientApp/src/app/todo/todo-item/todo-item.component.css b/Timeline/ClientApp/src/app/todo/todo-item/todo-item.component.css deleted file mode 100644 index dcf25fd8..00000000 --- a/Timeline/ClientApp/src/app/todo/todo-item/todo-item.component.css +++ /dev/null @@ -1,25 +0,0 @@ -.item-card { - padding: 0; - display: flex; - overflow: hidden; -} - -.item-body-box { - margin: 5px!important -} - -.item-color-block { - width: 15px; - align-self: stretch; - flex: 0 0 auto; -} - -.item-title { - vertical-align: middle; -} - -.item-detail-button { - width: unset; - height: unset; - line-height: unset; -} diff --git a/Timeline/ClientApp/src/app/todo/todo-item/todo-item.component.html b/Timeline/ClientApp/src/app/todo/todo-item/todo-item.component.html deleted file mode 100644 index 6f76e73b..00000000 --- a/Timeline/ClientApp/src/app/todo/todo-item/todo-item.component.html +++ /dev/null @@ -1,9 +0,0 @@ -<mat-card class="mat-elevation-z2 item-card"> - <span class="item-color-block" [class.color-block-closed]="item.isClosed" [class.color-block-open]="!item.isClosed"></span> - <div class="mat-h3 item-body-box"> - <span class="item-title">{{ item.number }}. {{ item.title }}</span> - <a mat-icon-button class="item-detail-button" [href]="item.detailUrl"> - <mat-icon>arrow_forward</mat-icon> - </a> - </div> -</mat-card> diff --git a/Timeline/ClientApp/src/app/todo/todo-item/todo-item.component.spec.ts b/Timeline/ClientApp/src/app/todo/todo-item/todo-item.component.spec.ts deleted file mode 100644 index 239ffc42..00000000 --- a/Timeline/ClientApp/src/app/todo/todo-item/todo-item.component.spec.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; -import { By } from '@angular/platform-browser'; - -import { TodoItem } from '../todo-item'; -import { TodoItemComponent } from '../todo-item/todo-item.component'; - -describe('TodoItemComponent', () => { - let component: TodoItemComponent; - let fixture: ComponentFixture<TodoItemComponent>; - - const mockTodoItem: TodoItem = { - number: 1, - title: 'Title', - isClosed: true, - detailUrl: '/detail', - }; - - beforeEach(async(() => { - TestBed.configureTestingModule({ - declarations: [TodoItemComponent], - schemas: [NO_ERRORS_SCHEMA] - }).compileComponents(); - })); - - beforeEach(() => { - fixture = TestBed.createComponent(TodoItemComponent); - component = fixture.componentInstance; - component.item = mockTodoItem; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); - - it('should set title', () => { - expect((fixture.debugElement.query(By.css('span.item-title')).nativeElement as HTMLSpanElement).textContent).toBe( - mockTodoItem.number + '. ' + mockTodoItem.title - ); - }); - - it('should set detail link', () => { - expect(fixture.debugElement.query(By.css('a.item-detail-button')).properties['href']).toBe(mockTodoItem.detailUrl); - }); -}); diff --git a/Timeline/ClientApp/src/app/todo/todo-item/todo-item.component.ts b/Timeline/ClientApp/src/app/todo/todo-item/todo-item.component.ts deleted file mode 100644 index b5c51229..00000000 --- a/Timeline/ClientApp/src/app/todo/todo-item/todo-item.component.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Component, Input } from '@angular/core'; - -import { TodoItem } from '../todo-item'; - -@Component({ - selector: 'app-todo-item', - templateUrl: './todo-item.component.html', - styleUrls: ['./todo-item.component.css', '../todo-list-color-block.css'] -}) -export class TodoItemComponent { - @Input() item!: TodoItem; -} diff --git a/Timeline/ClientApp/src/app/todo/todo-list-color-block.css b/Timeline/ClientApp/src/app/todo/todo-list-color-block.css deleted file mode 100644 index 5e0d4ba9..00000000 --- a/Timeline/ClientApp/src/app/todo/todo-list-color-block.css +++ /dev/null @@ -1,7 +0,0 @@ -.color-block-open { - background: red; -} - -.color-block-closed { - background: green; -} diff --git a/Timeline/ClientApp/src/app/todo/todo-page/todo-page.component.css b/Timeline/ClientApp/src/app/todo/todo-page/todo-page.component.css deleted file mode 100644 index 754b786e..00000000 --- a/Timeline/ClientApp/src/app/todo/todo-page/todo-page.component.css +++ /dev/null @@ -1,39 +0,0 @@ -.align-self-bottom { - align-self: flex-end; -} - -.item-box { - display: flex; - width: 100%; - box-sizing: border-box; -} - -.first-item-box { - justify-content: space-between; - padding: 0 0 5px 5px; -} - -.non-first-item-box { - padding: 5px; -} - -.space { - flex: 1 4 20px; -} - -.sample-box { - box-sizing: border-box; - align-self: flex-start; -} - -.sample-item { - display: flex; - align-items: center; -} - -.sample-color-block { - border-radius: 0.2em; - width: 1em; - height: 1em; - margin-right: 2px; -} diff --git a/Timeline/ClientApp/src/app/todo/todo-page/todo-page.component.html b/Timeline/ClientApp/src/app/todo/todo-page/todo-page.component.html deleted file mode 100644 index 50180fe8..00000000 --- a/Timeline/ClientApp/src/app/todo/todo-page/todo-page.component.html +++ /dev/null @@ -1,21 +0,0 @@ -<mat-progress-bar *ngIf="!isLoadCompleted" mode="indeterminate"></mat-progress-bar> - -<mat-list> - <mat-list-item *ngFor="let item of items; let i = index" style="height:unset;"> - <div class="item-box" [class.first-item-box]="i === 0" [class.non-first-item-box]="i !== 0"> - <app-todo-item @itemEnter [class.align-self-bottom]="i === 0" [item]="item"></app-todo-item> - <div class="space"></div> - <div class="sample-box" *ngIf="i === 0"> - <div class="mat-caption sample-item"> - <span class="sample-color-block color-block-open"></span> - <span> means working now.</span> - </div> - <div class="mat-caption sample-item"> - <span class="sample-color-block color-block-closed"></span> - <span> means completed.</span> - </div> - <div class="mat-caption">click on item to see details.</div> - </div> - </div> - </mat-list-item> -</mat-list> diff --git a/Timeline/ClientApp/src/app/todo/todo-page/todo-page.component.spec.ts b/Timeline/ClientApp/src/app/todo/todo-page/todo-page.component.spec.ts deleted file mode 100644 index 16c77376..00000000 --- a/Timeline/ClientApp/src/app/todo/todo-page/todo-page.component.spec.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { Component, NO_ERRORS_SCHEMA } from '@angular/core'; -import { async, ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing'; -import { By } from '@angular/platform-browser'; -import { NoopAnimationsModule } from '@angular/platform-browser/animations'; - -import { Observable, from } from 'rxjs'; -import { delay } from 'rxjs/operators'; - -import { TodoItem } from '../todo-item'; -import { TodoPageComponent } from './todo-page.component'; -import { TodoService } from '../todo-service/todo.service'; - - -@Component({ - /* tslint:disable-next-line:component-selector*/ - selector: 'mat-progress-bar', - template: '' -}) -class MatProgressBarStubComponent { } - -function asyncArray<T>(data: T[]): Observable<T> { - return from(data).pipe(delay(0)); -} - -describe('TodoListPageComponent', () => { - let component: TodoPageComponent; - let fixture: ComponentFixture<TodoPageComponent>; - - const mockTodoItems: TodoItem[] = [ - { - number: 0, - title: 'Test title 1', - isClosed: true, - detailUrl: 'test_url1' - }, - { - number: 1, - title: 'Test title 2', - isClosed: false, - detailUrl: 'test_url2' - } - ]; - - beforeEach(async(() => { - const mockTodoService: jasmine.SpyObj<TodoService> = jasmine.createSpyObj('TodoService', ['getWorkItemList']); - - mockTodoService.getWorkItemList.and.returnValue(asyncArray(mockTodoItems)); - - TestBed.configureTestingModule({ - declarations: [TodoPageComponent, MatProgressBarStubComponent], - imports: [NoopAnimationsModule], - providers: [{ provide: TodoService, useValue: mockTodoService }], - schemas: [NO_ERRORS_SCHEMA] - }).compileComponents(); - })); - - beforeEach(() => { - fixture = TestBed.createComponent(TodoPageComponent); - component = fixture.componentInstance; - }); - - it('should create', () => { - fixture.detectChanges(); - expect(component).toBeTruthy(); - }); - - it('should show progress bar during loading', () => { - fixture.detectChanges(); - expect(fixture.debugElement.query(By.css('mat-progress-bar'))).toBeTruthy(); - }); - - it('should hide progress bar after loading', fakeAsync(() => { - fixture.detectChanges(); - tick(); - fixture.detectChanges(); - expect(fixture.debugElement.query(By.css('mat-progress-bar'))).toBeFalsy(); - })); -}); diff --git a/Timeline/ClientApp/src/app/todo/todo-page/todo-page.component.ts b/Timeline/ClientApp/src/app/todo/todo-page/todo-page.component.ts deleted file mode 100644 index 7b658228..00000000 --- a/Timeline/ClientApp/src/app/todo/todo-page/todo-page.component.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { Component, OnInit } from '@angular/core'; -import { trigger, transition, style, animate } from '@angular/animations'; - - -import { TodoItem } from '../todo-item'; -import { TodoService } from '../todo-service/todo.service'; - -@Component({ - selector: 'app-todo-page', - templateUrl: './todo-page.component.html', - styleUrls: ['./todo-page.component.css', '../todo-list-color-block.css'], - animations: [ - trigger('itemEnter', [ - transition(':enter', [ - style({ - transform: 'translateX(-100%) translateX(-20px)' - }), - animate('400ms ease-out', style({ - transform: 'none' - })) - ]) - ]) - ] -}) -export class TodoPageComponent implements OnInit { - - items: TodoItem[] = []; - isLoadCompleted = false; - - constructor(private todoService: TodoService) { - } - - ngOnInit() { - this.todoService.getWorkItemList().subscribe({ - next: result => this.items.push(result), - complete: () => { this.isLoadCompleted = true; } - }); - } -} diff --git a/Timeline/ClientApp/src/app/todo/todo-service/http-entities.ts b/Timeline/ClientApp/src/app/todo/todo-service/http-entities.ts deleted file mode 100644 index 3971617c..00000000 --- a/Timeline/ClientApp/src/app/todo/todo-service/http-entities.ts +++ /dev/null @@ -1,11 +0,0 @@ -export const githubBaseUrl = 'https://api.github.com/repos/crupest/Timeline'; - -export interface IssueResponseItem { - number: number; - title: string; - state: string; - html_url: string; - pull_request?: any; -} - -export type IssueResponse = IssueResponseItem[]; diff --git a/Timeline/ClientApp/src/app/todo/todo-service/todo.service.spec.ts b/Timeline/ClientApp/src/app/todo/todo-service/todo.service.spec.ts deleted file mode 100644 index 679dc8b7..00000000 --- a/Timeline/ClientApp/src/app/todo/todo-service/todo.service.spec.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { TestBed } from '@angular/core/testing'; -import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; -import { toArray } from 'rxjs/operators'; - -import { TodoItem } from '../todo-item'; -import { TodoService } from './todo.service'; -import { IssueResponse, githubBaseUrl } from './http-entities'; - - -describe('TodoService', () => { - beforeEach(() => TestBed.configureTestingModule({ - imports: [HttpClientTestingModule] - })); - - it('should be created', () => { - const service: TodoService = TestBed.get(TodoService); - expect(service).toBeTruthy(); - }); - - it('should work well', () => { - const service: TodoService = TestBed.get(TodoService); - - const mockIssueList: IssueResponse = [{ - number: 1, - title: 'Issue title 1', - state: 'open', - html_url: 'test_url1' - }, { - number: 2, - title: 'Issue title 2', - state: 'closed', - html_url: 'test_url2', - pull_request: {} - }]; - - const mockTodoItemList: TodoItem[] = [{ - number: 1, - title: 'Issue title 1', - isClosed: false, - detailUrl: 'test_url1' - }]; - - service.getWorkItemList().pipe(toArray()).subscribe(data => { - expect(data).toEqual(mockTodoItemList); - }); - - const httpController: HttpTestingController = TestBed.get(HttpTestingController); - - httpController.expectOne(request => request.url === githubBaseUrl + '/issues' && - request.params.get('state') === 'all').flush(mockIssueList); - - httpController.verify(); - }); -}); diff --git a/Timeline/ClientApp/src/app/todo/todo-service/todo.service.ts b/Timeline/ClientApp/src/app/todo/todo-service/todo.service.ts deleted file mode 100644 index df63636d..00000000 --- a/Timeline/ClientApp/src/app/todo/todo-service/todo.service.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { Injectable } from '@angular/core'; -import { HttpClient } from '@angular/common/http'; -import { Observable, from } from 'rxjs'; -import { switchMap, map, filter } from 'rxjs/operators'; - -import { IssueResponse, githubBaseUrl } from './http-entities'; -import { TodoItem } from '../todo-item'; - - -@Injectable({ - providedIn: 'root' -}) -export class TodoService { - - constructor(private client: HttpClient) { } - - getWorkItemList(): Observable<TodoItem> { - return this.client.get<IssueResponse>(`${githubBaseUrl}/issues`, { - params: { - state: 'all' - } - }).pipe( - switchMap(result => from(result)), - filter(result => result.pull_request === undefined), // filter out pull requests. - map(result => <TodoItem>{ - number: result.number, - title: result.title, - isClosed: result.state === 'closed', - detailUrl: result.html_url - }) - ); - } -} diff --git a/Timeline/ClientApp/src/app/todo/todo.module.ts b/Timeline/ClientApp/src/app/todo/todo.module.ts deleted file mode 100644 index 5bcfefbd..00000000 --- a/Timeline/ClientApp/src/app/todo/todo.module.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { NgModule } from '@angular/core'; -import { CommonModule } from '@angular/common'; -import { RouterModule } from '@angular/router'; -import { MatListModule, MatIconModule, MatCardModule, MatProgressBarModule, MatButtonModule } from '@angular/material'; -import { HttpClientModule } from '@angular/common/http'; -import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; - -import { TodoItemComponent } from './todo-item/todo-item.component'; -import { TodoPageComponent } from './todo-page/todo-page.component'; - -@NgModule({ - declarations: [ - TodoItemComponent, - TodoPageComponent - ], - imports: [ - CommonModule, HttpClientModule, BrowserAnimationsModule, - MatListModule, MatCardModule, MatIconModule, MatProgressBarModule, MatButtonModule, - RouterModule.forChild([ - { path: 'todo', component: TodoPageComponent } - ]) - ], - exports: [ - RouterModule - ] -}) -export class TodoModule { } diff --git a/Timeline/ClientApp/src/app/user/auth.guard.spec.ts b/Timeline/ClientApp/src/app/user/auth.guard.spec.ts deleted file mode 100644 index 6a36fea6..00000000 --- a/Timeline/ClientApp/src/app/user/auth.guard.spec.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { Observable, of } from 'rxjs'; - -import { AuthGuard, AuthStrategy } from './auth.guard'; -import { UserInfo } from './entities'; - -describe('AuthGuard', () => { - class ConfiurableAuthGuard extends AuthGuard { - constructor(mockInternalUserService: any) { - super(mockInternalUserService); - } - - authStrategy: AuthStrategy = 'all'; - } - - let mockUserService: { userInfo$: Observable<UserInfo | null> }; - let guard: ConfiurableAuthGuard; - let onAuthFialedSpy: jasmine.Spy; - - const mockRoles = ['role1', 'role2']; - - interface ActivateResultMap { - nologin: boolean; - loginWithNoRole: boolean; - loginWithMockRoles: boolean; - } - - - function createTest(authStrategy: AuthStrategy, result: ActivateResultMap): () => void { - return () => { - guard.authStrategy = authStrategy; - - function testWith(userInfo: UserInfo | null, r: boolean) { - mockUserService.userInfo$ = of(userInfo); - - const rawResult = guard.canActivate(<any>null, <any>null); - if (typeof rawResult === 'boolean') { - expect(rawResult).toBe(r); - } else if (rawResult instanceof Observable) { - rawResult.subscribe(next => expect(next).toBe(r)); - } else { - throw new Error('Unsupported return type.'); - } - } - - testWith(null, result.nologin); - testWith({ username: 'user', roles: [] }, result.loginWithNoRole); - testWith({ username: 'user', roles: mockRoles }, result.loginWithMockRoles); - }; - } - - beforeEach(() => { - mockUserService = { userInfo$: of(null) }; - guard = new ConfiurableAuthGuard(mockUserService); - onAuthFialedSpy = spyOn(guard, 'onAuthFailed'); - }); - - - it('all should work', createTest('all', { nologin: true, loginWithNoRole: true, loginWithMockRoles: true })); - it('require login should work', createTest('requirelogin', { nologin: false, loginWithNoRole: true, loginWithMockRoles: true })); - it('require no login should work', createTest('requirenologin', { nologin: true, loginWithNoRole: false, loginWithMockRoles: false })); - it('good roles should work', createTest(mockRoles, { nologin: false, loginWithNoRole: false, loginWithMockRoles: true })); - it('bad roles should work', createTest(['role3'], { nologin: false, loginWithNoRole: false, loginWithMockRoles: false })); - - it('auth failed callback should be called', () => { - guard.authStrategy = 'requirelogin'; - (<Observable<boolean>>guard.canActivate(<any>null, <any>null)).subscribe(); - expect(onAuthFialedSpy).toHaveBeenCalled(); - }); -}); diff --git a/Timeline/ClientApp/src/app/user/auth.guard.ts b/Timeline/ClientApp/src/app/user/auth.guard.ts deleted file mode 100644 index 1fc7a7c0..00000000 --- a/Timeline/ClientApp/src/app/user/auth.guard.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { Injectable } from '@angular/core'; -import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, UrlTree } from '@angular/router'; -import { Observable } from 'rxjs'; -import { take, map } from 'rxjs/operators'; - -import { InternalUserService } from './internal-user-service/internal-user.service'; - -export type AuthStrategy = 'all' | 'requirelogin' | 'requirenologin' | string[]; - -export abstract class AuthGuard implements CanActivate { - - constructor(protected internalUserService: InternalUserService) { } - - onAuthFailed() { } - - abstract get authStrategy(): AuthStrategy; - - canActivate(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): - Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree { - - const { authStrategy } = this; - - if (authStrategy === 'all') { - return true; - } - - return this.internalUserService.userInfo$.pipe(take(1), map(userInfo => { - if (userInfo === null) { - if (authStrategy === 'requirenologin') { - return true; - } - } else { - if (authStrategy === 'requirelogin') { - return true; - } else if (authStrategy instanceof Array) { - const { roles } = userInfo; - if (authStrategy.every(value => roles.includes(value))) { - return true; - } - } - } - - // reach here means auth fails - this.onAuthFailed(); - return false; - })); - } -} - -@Injectable({ - providedIn: 'root' -}) -export class RequireLoginGuard extends AuthGuard { - readonly authStrategy: AuthStrategy = 'requirelogin'; - - // never remove this constructor or you will get an injection error. - constructor(internalUserService: InternalUserService) { - super(internalUserService); - } - - onAuthFailed() { - this.internalUserService.userRouteNavigate(['login']); - } -} - -@Injectable({ - providedIn: 'root' -}) -export class RequireNoLoginGuard extends AuthGuard { - readonly authStrategy: AuthStrategy = 'requirenologin'; - - // never remove this constructor or you will get an injection error. - constructor(internalUserService: InternalUserService) { - super(internalUserService); - } - - onAuthFailed() { - this.internalUserService.userRouteNavigate(['success']); - } -} diff --git a/Timeline/ClientApp/src/app/user/entities.ts b/Timeline/ClientApp/src/app/user/entities.ts deleted file mode 100644 index 6d432ec6..00000000 --- a/Timeline/ClientApp/src/app/user/entities.ts +++ /dev/null @@ -1,9 +0,0 @@ -export interface UserCredentials { - username: string; - password: string; -} - -export interface UserInfo { - username: string; - roles: string[]; -} diff --git a/Timeline/ClientApp/src/app/user/internal-user-service/errors.ts b/Timeline/ClientApp/src/app/user/internal-user-service/errors.ts deleted file mode 100644 index 3358a9d9..00000000 --- a/Timeline/ClientApp/src/app/user/internal-user-service/errors.ts +++ /dev/null @@ -1,29 +0,0 @@ -export class BadNetworkError extends Error { - constructor() { - super('Network is bad.'); - } -} - -export class AlreadyLoginError extends Error { - constructor() { - super('Internal logical error. There is already a token saved. Please call validateUserLoginState first.'); - } -} - -export class BadCredentialsError extends Error { - constructor() { - super('Username or password is wrong.'); - } -} - -export class UnknownError extends Error { - constructor(public internalError?: any) { - super('Sorry, unknown error occured!'); - } -} - -export class ServerInternalError extends Error { - constructor(message?: string) { - super('Wrong server response. ' + message); - } -} diff --git a/Timeline/ClientApp/src/app/user/internal-user-service/http-entities.ts b/Timeline/ClientApp/src/app/user/internal-user-service/http-entities.ts deleted file mode 100644 index f52233c9..00000000 --- a/Timeline/ClientApp/src/app/user/internal-user-service/http-entities.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { UserCredentials, UserInfo } from '../entities'; - -export const createTokenUrl = '/api/User/CreateToken'; -export const validateTokenUrl = '/api/User/ValidateToken'; - -export type CreateTokenRequest = UserCredentials; - -export interface CreateTokenResponse { - success: boolean; - token?: string; - userInfo?: UserInfo; -} - -export interface ValidateTokenRequest { - token: string; -} - -export interface ValidateTokenResponse { - isValid: boolean; - userInfo?: UserInfo; -} diff --git a/Timeline/ClientApp/src/app/user/internal-user-service/internal-user.service.mock.ts b/Timeline/ClientApp/src/app/user/internal-user-service/internal-user.service.mock.ts deleted file mode 100644 index f4a85262..00000000 --- a/Timeline/ClientApp/src/app/user/internal-user-service/internal-user.service.mock.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { InternalUserService } from './internal-user.service'; - -export function createMockInternalUserService(): jasmine.SpyObj<InternalUserService> { - return jasmine.createSpyObj('InternalUserService', ['userRouteNavigate', 'refreshAndGetUserState', 'tryLogin']); -} diff --git a/Timeline/ClientApp/src/app/user/internal-user-service/internal-user.service.spec.ts b/Timeline/ClientApp/src/app/user/internal-user-service/internal-user.service.spec.ts deleted file mode 100644 index 15755382..00000000 --- a/Timeline/ClientApp/src/app/user/internal-user-service/internal-user.service.spec.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { TestBed, fakeAsync, tick } from '@angular/core/testing'; -import { HttpRequest } from '@angular/common/http'; -import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; -import { Router } from '@angular/router'; -import { MatSnackBar } from '@angular/material'; - -import { Mock } from 'src/app/test-utilities/mock'; -import { createMockStorage } from 'src/app/test-utilities/storage.mock'; -import { WINDOW } from '../window-inject-token'; - -import { UserInfo, UserCredentials } from '../entities'; -import { - createTokenUrl, validateTokenUrl, CreateTokenRequest, - CreateTokenResponse, ValidateTokenRequest, ValidateTokenResponse -} from './http-entities'; -import { InternalUserService, SnackBarTextKey, snackBarText, TOKEN_STORAGE_KEY } from './internal-user.service'; -import { repeat } from 'src/app/utilities/language-untilities'; - - -describe('InternalUserService', () => { - let mockLocalStorage: Mock<Storage>; - let mockSnackBar: jasmine.SpyObj<MatSnackBar>; - - beforeEach(() => { - mockLocalStorage = createMockStorage(); - mockSnackBar = jasmine.createSpyObj('MatSnackBar', ['open']); - TestBed.configureTestingModule({ - imports: [HttpClientTestingModule], - providers: [ - { provide: WINDOW, useValue: { localStorage: mockLocalStorage } }, - { provide: Router, useValue: null }, - { provide: MatSnackBar, useValue: mockSnackBar } - ] - }); - }); - - it('should be created', () => { - const service: InternalUserService = TestBed.get(InternalUserService); - expect(service).toBeTruthy(); - }); - - const mockUserInfo: UserInfo = { - username: 'user', - roles: ['user', 'other'] - }; - - const mockToken = 'mock-token'; - - describe('validate token', () => { - const validateTokenRequestMatcher = (req: HttpRequest<ValidateTokenRequest>): boolean => - req.url === validateTokenUrl && req.body !== null && req.body.token === mockToken; - - function createTest( - expectSnackBarTextKey: SnackBarTextKey, - setStorageToken: boolean, - setHttpController?: (controller: HttpTestingController) => void - ): () => void { - return fakeAsync(() => { - if (setStorageToken) { - mockLocalStorage.setItem(TOKEN_STORAGE_KEY, mockToken); - } - TestBed.get(InternalUserService); - const controller = TestBed.get(HttpTestingController) as HttpTestingController; - if (setHttpController) { - setHttpController(controller); - } - controller.verify(); - tick(); - expect(mockSnackBar.open).toHaveBeenCalledWith(snackBarText[expectSnackBarTextKey], jasmine.anything(), jasmine.anything()); - }); - } - - it('no login should work well', createTest('noLogin', false)); - it('already login should work well', createTest('alreadyLogin', true, - controller => controller.expectOne(validateTokenRequestMatcher).flush( - <ValidateTokenResponse>{ isValid: true, userInfo: mockUserInfo }))); - it('invalid login should work well', createTest('invalidLogin', true, - controller => controller.expectOne(validateTokenRequestMatcher).flush(<ValidateTokenResponse>{ isValid: false }))); - it('check fail should work well', createTest('checkFail', true, - controller => repeat(4, () => { - controller.expectOne(validateTokenRequestMatcher).error(new ErrorEvent('Network error', { message: 'simulated network error' })); - }))); - }); - - describe('login should work well', () => { - const mockUserCredentials: UserCredentials = { - username: 'user', - password: 'user' - }; - - function createTest(rememberMe: boolean) { - return () => { - const service: InternalUserService = TestBed.get(InternalUserService); - - service.tryLogin({ ...mockUserCredentials, rememberMe: rememberMe }).subscribe(result => { - expect(result).toEqual(mockUserInfo); - }); - - const httpController = TestBed.get(HttpTestingController) as HttpTestingController; - - httpController.expectOne((request: HttpRequest<CreateTokenRequest>) => - request.url === createTokenUrl && request.body !== null && - request.body.username === mockUserCredentials.username && - request.body.password === mockUserCredentials.password).flush(<CreateTokenResponse>{ - success: true, - token: mockToken, - userInfo: mockUserInfo - }); - - expect(service.currentUserInfo).toEqual(mockUserInfo); - - httpController.verify(); - - expect(mockLocalStorage.getItem(TOKEN_STORAGE_KEY)).toBe(rememberMe ? mockToken : null); - }; - } - - it('remember me should work well', createTest(true)); - it('not remember me should work well', createTest(false)); - }); - - // TODO: test on error situations. -}); diff --git a/Timeline/ClientApp/src/app/user/internal-user-service/internal-user.service.ts b/Timeline/ClientApp/src/app/user/internal-user-service/internal-user.service.ts deleted file mode 100644 index 66eafde9..00000000 --- a/Timeline/ClientApp/src/app/user/internal-user-service/internal-user.service.ts +++ /dev/null @@ -1,155 +0,0 @@ -import { Injectable, Inject } from '@angular/core'; -import { HttpClient, HttpErrorResponse } from '@angular/common/http'; -import { Router } from '@angular/router'; - -import { Observable, throwError, BehaviorSubject, of } from 'rxjs'; -import { map, catchError, retry, switchMap, tap, filter } from 'rxjs/operators'; - -import { AlreadyLoginError, BadCredentialsError, BadNetworkError, UnknownError, ServerInternalError } from './errors'; -import { - createTokenUrl, validateTokenUrl, CreateTokenRequest, - CreateTokenResponse, ValidateTokenRequest, ValidateTokenResponse -} from './http-entities'; -import { UserCredentials, UserInfo } from '../entities'; -import { MatSnackBar } from '@angular/material'; -import { WINDOW } from '../window-inject-token'; - -export const snackBarText = { - checkFail: 'Failed to check last login', - noLogin: 'No login before!', - alreadyLogin: 'You have login already!', - invalidLogin: 'Last login is no longer invalid!', - ok: 'ok' -}; - -export type SnackBarTextKey = Exclude<keyof typeof snackBarText, 'ok'>; - -export const TOKEN_STORAGE_KEY = 'token'; - -export interface LoginInfo extends UserCredentials { - rememberMe: boolean; -} - -/** - * This service is only used internal in user module. - */ -@Injectable({ - providedIn: 'root' -}) -export class InternalUserService { - - private token: string | null = null; - private userInfoSubject = new BehaviorSubject<UserInfo | null | undefined>(undefined); - - readonly userInfo$: Observable<UserInfo | null> = - <Observable<UserInfo | null>>this.userInfoSubject.pipe(filter(value => value !== undefined)); - - get currentUserInfo(): UserInfo | null | undefined { - return this.userInfoSubject.value; - } - - private openSnackBar(snackBar: MatSnackBar, textKey: SnackBarTextKey) { - setTimeout(() => snackBar.open(snackBarText[textKey], snackBarText.ok, { duration: 2000 }), 0); - } - - constructor(@Inject(WINDOW) private window: Window, private httpClient: HttpClient, private router: Router, snackBar: MatSnackBar) { - const savedToken = this.window.localStorage.getItem(TOKEN_STORAGE_KEY); - if (savedToken === null) { - this.openSnackBar(snackBar, 'noLogin'); - this.userInfoSubject.next(null); - } else { - this.validateToken(savedToken).subscribe(result => { - if (result === null) { - this.window.localStorage.removeItem(TOKEN_STORAGE_KEY); - this.openSnackBar(snackBar, 'invalidLogin'); - this.userInfoSubject.next(null); - } else { - this.token = savedToken; - this.userInfoSubject.next(result); - this.openSnackBar(snackBar, 'alreadyLogin'); - } - }, _ => { - this.openSnackBar(snackBar, 'checkFail'); - this.userInfoSubject.next(null); - }); - } - } - - private validateToken(token: string): Observable<UserInfo | null> { - return this.httpClient.post<ValidateTokenResponse>(validateTokenUrl, <ValidateTokenRequest>{ token: token }).pipe( - retry(3), - switchMap(result => { - if (result.isValid) { - const { userInfo } = result; - if (userInfo) { - return of(userInfo); - } else { - return throwError(new ServerInternalError('IsValid is true but UserInfo is null.')); - } - } else { - return of(null); - } - }), - tap({ - error: error => { - console.error('Failed to validate token.'); - console.error(error); - } - }), - ); - } - - userRouteNavigate(commands: any[] | null) { - this.router.navigate([{ - outlets: { - user: commands - } - }]); - } - - tryLogin(info: LoginInfo): Observable<UserInfo> { - if (this.token) { - return throwError(new AlreadyLoginError()); - } - - return this.httpClient.post<CreateTokenResponse>(createTokenUrl, <CreateTokenRequest>info).pipe( - catchError((error: HttpErrorResponse) => { - if (error.error instanceof ErrorEvent) { - console.error('An error occurred when login: ' + error.error.message); - return throwError(new BadNetworkError()); - } else { - console.error('An unknown error occurred when login: ' + error); - return throwError(new UnknownError(error)); - } - }), - switchMap(result => { - if (result.success) { - if (result.token && result.userInfo) { - this.token = result.token; - if (info.rememberMe) { - this.window.localStorage.setItem(TOKEN_STORAGE_KEY, result.token); - } - this.userInfoSubject.next(result.userInfo); - return of(result.userInfo); - } else { - console.error('An error occurred when login: server return wrong data.'); - return throwError(new ServerInternalError('Token or userInfo is null.')); - } - } else { - console.error('An error occurred when login: wrong credentials.'); - return throwError(new BadCredentialsError()); - } - }) - ); - } - - logout() { - if (this.currentUserInfo === null) { - throw new Error('No login now. You can\'t logout.'); - } - - this.window.localStorage.removeItem(TOKEN_STORAGE_KEY); - this.token = null; - this.userInfoSubject.next(null); - } -} diff --git a/Timeline/ClientApp/src/app/user/redirect.component.ts b/Timeline/ClientApp/src/app/user/redirect.component.ts deleted file mode 100644 index 438b38b9..00000000 --- a/Timeline/ClientApp/src/app/user/redirect.component.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Component, OnInit } from '@angular/core'; -import { InternalUserService } from './internal-user-service/internal-user.service'; - -@Component({ - selector: 'app-redirect', - template: '' -}) -export class RedirectComponent implements OnInit { - - constructor(private userService: InternalUserService) { } - - ngOnInit() { - this.userService.userRouteNavigate(['login']); - } -} diff --git a/Timeline/ClientApp/src/app/user/user-dialog/user-dialog.component.css b/Timeline/ClientApp/src/app/user/user-dialog/user-dialog.component.css deleted file mode 100644 index e69de29b..00000000 --- a/Timeline/ClientApp/src/app/user/user-dialog/user-dialog.component.css +++ /dev/null diff --git a/Timeline/ClientApp/src/app/user/user-dialog/user-dialog.component.html b/Timeline/ClientApp/src/app/user/user-dialog/user-dialog.component.html deleted file mode 100644 index e8dbb003..00000000 --- a/Timeline/ClientApp/src/app/user/user-dialog/user-dialog.component.html +++ /dev/null @@ -1 +0,0 @@ -<router-outlet name="user"></router-outlet> diff --git a/Timeline/ClientApp/src/app/user/user-dialog/user-dialog.component.spec.ts b/Timeline/ClientApp/src/app/user/user-dialog/user-dialog.component.spec.ts deleted file mode 100644 index 47860eee..00000000 --- a/Timeline/ClientApp/src/app/user/user-dialog/user-dialog.component.spec.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { Component } from '@angular/core'; -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; -import { Router, Event } from '@angular/router'; - -import { Observable } from 'rxjs'; - -import { UserDialogComponent } from './user-dialog.component'; - -@Component({ - /* tslint:disable-next-line:component-selector*/ - selector: 'router-outlet', - template: '' -}) -class RouterOutletStubComponent { } - - -describe('UserDialogComponent', () => { - let component: UserDialogComponent; - let fixture: ComponentFixture<UserDialogComponent>; - - - beforeEach(async(() => { - TestBed.configureTestingModule({ - declarations: [UserDialogComponent, RouterOutletStubComponent], - providers: [{ // for the workaround - provide: Router, useValue: { - events: new Observable<Event>() - } - }] - }) - .compileComponents(); - })); - - beforeEach(() => { - fixture = TestBed.createComponent(UserDialogComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/Timeline/ClientApp/src/app/user/user-dialog/user-dialog.component.ts b/Timeline/ClientApp/src/app/user/user-dialog/user-dialog.component.ts deleted file mode 100644 index 2887f0a6..00000000 --- a/Timeline/ClientApp/src/app/user/user-dialog/user-dialog.component.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { Component, OnInit, ViewChild } from '@angular/core'; -import { RouterOutlet, Router, ActivationStart } from '@angular/router'; - -@Component({ - selector: 'app-user-dialog', - templateUrl: './user-dialog.component.html', - styleUrls: ['./user-dialog.component.css'] -}) -export class UserDialogComponent implements OnInit { - - constructor(private router: Router) { } - - @ViewChild(RouterOutlet) outlet!: RouterOutlet; - - ngOnInit() { - // this is a workaround for a bug. see https://github.com/angular/angular/issues/20694 - const subscription = this.router.events.subscribe(e => { - if (e instanceof ActivationStart && e.snapshot.outlet === 'user') { - this.outlet.deactivate(); - subscription.unsubscribe(); - } - }); - } -} diff --git a/Timeline/ClientApp/src/app/user/user-login-success/user-login-success.component.css b/Timeline/ClientApp/src/app/user/user-login-success/user-login-success.component.css deleted file mode 100644 index b1101e2a..00000000 --- a/Timeline/ClientApp/src/app/user/user-login-success/user-login-success.component.css +++ /dev/null @@ -1,22 +0,0 @@ -.login-success-message { - color: green; -} - -.username { - color: blue; -} - -:host { - display: flex; - flex-wrap: wrap; -} - -:host p { - margin-top: 0.3em; - margin-bottom: 0.3em; - width: 100%; -} - -.logout-button { - margin-left: auto; -} diff --git a/Timeline/ClientApp/src/app/user/user-login-success/user-login-success.component.html b/Timeline/ClientApp/src/app/user/user-login-success/user-login-success.component.html deleted file mode 100644 index 685f6299..00000000 --- a/Timeline/ClientApp/src/app/user/user-login-success/user-login-success.component.html +++ /dev/null @@ -1,6 +0,0 @@ -<p *ngIf="displayLoginSuccessMessage" class="mat-body login-success-message"> - Login succeeds! -</p> -<p class="mat-body">You have been login as <span class="username">{{ userInfo.username }}</span>.</p> -<p class="mat-body">Your roles are <span class="roles">{{ userInfo.roles.join(', ') }}</span>.</p> -<a mat-flat-button class="logout-button" [routerLink]="['..','logout']">Logout</a> diff --git a/Timeline/ClientApp/src/app/user/user-login-success/user-login-success.component.spec.ts b/Timeline/ClientApp/src/app/user/user-login-success/user-login-success.component.spec.ts deleted file mode 100644 index 3eba2696..00000000 --- a/Timeline/ClientApp/src/app/user/user-login-success/user-login-success.component.spec.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; -import { By } from '@angular/platform-browser'; -import { ActivatedRoute } from '@angular/router'; - -import { RouterLinkStubDirective } from '../../test-utilities/router-link.mock'; -import { MockActivatedRoute } from '../../test-utilities/activated-route.mock'; -import { createMockInternalUserService } from '../internal-user-service/internal-user.service.mock'; - -import { UserLoginSuccessComponent } from './user-login-success.component'; -import { InternalUserService } from '../internal-user-service/internal-user.service'; - - -describe('UserLoginSuccessComponent', () => { - let component: UserLoginSuccessComponent; - let fixture: ComponentFixture<UserLoginSuccessComponent>; - - let mockInternalUserService: jasmine.SpyObj<InternalUserService>; - let mockActivatedRoute: MockActivatedRoute; - - const mockUserInfo = { - username: 'crupest', - roles: ['superman', 'coder'] - }; - - beforeEach(async(() => { - mockInternalUserService = createMockInternalUserService(); - mockActivatedRoute = new MockActivatedRoute(); - - // mock currentUserInfo property. because it only has a getter so cast it to any first. - (<any>mockInternalUserService).currentUserInfo = mockUserInfo; - - TestBed.configureTestingModule({ - declarations: [UserLoginSuccessComponent, RouterLinkStubDirective], - providers: [ - { provide: InternalUserService, useValue: mockInternalUserService }, - { provide: ActivatedRoute, useValue: mockActivatedRoute } - ] - }) - .compileComponents(); - })); - - beforeEach(() => { - fixture = TestBed.createComponent(UserLoginSuccessComponent); - component = fixture.componentInstance; - }); - - it('should create', () => { - fixture.detectChanges(); - expect(component).toBeTruthy(); - }); - - it('user info should work well', () => { - fixture.detectChanges(); - - expect((fixture.debugElement.query(By.css('p.login-success-message')))).toBeFalsy(); - - expect((fixture.debugElement.query(By.css('span.username')).nativeElement as HTMLSpanElement).textContent) - .toBe(mockUserInfo.username); - expect((fixture.debugElement.query(By.css('span.roles')).nativeElement as HTMLSpanElement).textContent) - .toBe(mockUserInfo.roles.join(', ')); - }); - - it('login success message should display well', () => { - mockActivatedRoute.pushSnapshotWithParamMap({ fromlogin: 'true' }); - fixture.detectChanges(); - expect((fixture.debugElement.query(By.css('p.login-success-message')))).toBeTruthy(); - }); - - it('logout button should be set well', () => { - fixture.detectChanges(); - const routerLinkDirective: RouterLinkStubDirective = - fixture.debugElement.query(By.css('a')).injector.get(RouterLinkStubDirective); - expect(routerLinkDirective.linkParams).toEqual(['..', 'logout']); - }); -}); diff --git a/Timeline/ClientApp/src/app/user/user-login-success/user-login-success.component.ts b/Timeline/ClientApp/src/app/user/user-login-success/user-login-success.component.ts deleted file mode 100644 index 2ae584d6..00000000 --- a/Timeline/ClientApp/src/app/user/user-login-success/user-login-success.component.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { Component, OnInit } from '@angular/core'; -import { ActivatedRoute } from '@angular/router'; - -import { UserInfo } from '../entities'; -import { InternalUserService } from '../internal-user-service/internal-user.service'; -import { throwIfNullOrUndefined } from 'src/app/utilities/language-untilities'; - -@Component({ - selector: 'app-user-login-success', - templateUrl: './user-login-success.component.html', - styleUrls: ['./user-login-success.component.css'] -}) -export class UserLoginSuccessComponent implements OnInit { - - displayLoginSuccessMessage = false; - - userInfo!: UserInfo; - - constructor(private route: ActivatedRoute, private userService: InternalUserService) { } - - ngOnInit() { - this.userInfo = throwIfNullOrUndefined(this.userService.currentUserInfo, () => 'Route error. No login now!'); - this.displayLoginSuccessMessage = this.route.snapshot.paramMap.get('fromlogin') === 'true'; - } -} diff --git a/Timeline/ClientApp/src/app/user/user-login/user-login.component.css b/Timeline/ClientApp/src/app/user/user-login/user-login.component.css deleted file mode 100644 index 8bf6b408..00000000 --- a/Timeline/ClientApp/src/app/user/user-login/user-login.component.css +++ /dev/null @@ -1,24 +0,0 @@ -form { - display: flex; - flex-wrap: wrap; -} - -div.w-100 { - width: 100%; -} - -.login-button { - margin-left: auto; -} - -.no-login-message { - color: blue; -} - -.invalid-login-message { - color: red; -} - -.error-message { - color: red; -} diff --git a/Timeline/ClientApp/src/app/user/user-login/user-login.component.html b/Timeline/ClientApp/src/app/user/user-login/user-login.component.html deleted file mode 100644 index 7398ece7..00000000 --- a/Timeline/ClientApp/src/app/user/user-login/user-login.component.html +++ /dev/null @@ -1,19 +0,0 @@ -<form [formGroup]="form"> - <ng-container *ngIf="message" [ngSwitch]="message"> - <p *ngSwitchCase="'nologin'" class="mat-h3 no-login-message">You haven't login.</p> - <p *ngSwitchCase="'invalidlogin'" class="mat-h3 invalid-login-message">Your login is no longer valid.</p> - <p *ngSwitchDefault class="mat-h3 error-message">{{ message }}</p> - </ng-container> - <mat-form-field> - <mat-label>Username</mat-label> - <input formControlName="username" matInput type="text" /> - </mat-form-field> - <div class="w-100"></div> - <mat-form-field> - <mat-label>Password</mat-label> - <input formControlName="password" matInput type="password" /> - </mat-form-field> - <mat-checkbox formControlName="rememberMe">Remember me!</mat-checkbox> - <div class="w-100"></div> - <button mat-flat-button class="login-button" (appDebounceClick)="onLoginButtonClick()">Login</button> -</form> diff --git a/Timeline/ClientApp/src/app/user/user-login/user-login.component.spec.ts b/Timeline/ClientApp/src/app/user/user-login/user-login.component.spec.ts deleted file mode 100644 index f010e4b7..00000000 --- a/Timeline/ClientApp/src/app/user/user-login/user-login.component.spec.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; -import { ReactiveFormsModule } from '@angular/forms'; -import { By } from '@angular/platform-browser'; - -import { of, throwError } from 'rxjs'; - -import { createMockInternalUserService } from '../internal-user-service/internal-user.service.mock'; -import { UserLoginComponent } from './user-login.component'; -import { InternalUserService } from '../internal-user-service/internal-user.service'; -import { UserInfo } from '../entities'; -import { MatCheckboxModule } from '@angular/material'; - -describe('UserLoginComponent', () => { - let component: UserLoginComponent; - let fixture: ComponentFixture<UserLoginComponent>; - let mockInternalUserService: jasmine.SpyObj<InternalUserService>; - - beforeEach(async(() => { - mockInternalUserService = createMockInternalUserService(); - - // mock property - (<any>mockInternalUserService).currentUserInfo = null; - - TestBed.configureTestingModule({ - declarations: [UserLoginComponent], - providers: [ - { provide: InternalUserService, useValue: mockInternalUserService } - ], - imports: [ReactiveFormsModule, MatCheckboxModule], - schemas: [NO_ERRORS_SCHEMA] - }) - .compileComponents(); - })); - - beforeEach(() => { - fixture = TestBed.createComponent(UserLoginComponent); - component = fixture.componentInstance; - }); - - it('should create', () => { - fixture.detectChanges(); - expect(component).toBeTruthy(); - }); - - it('reactive form should work well', () => { - fixture.detectChanges(); - - const usernameInput = fixture.debugElement.query(By.css('input[type=text]')).nativeElement as HTMLInputElement; - const passwordInput = fixture.debugElement.query(By.css('input[type=password]')).nativeElement as HTMLInputElement; - const rememberMeCheckbox = fixture.debugElement.query(By.css('input[type=checkbox]')).nativeElement as HTMLInputElement; - - usernameInput.value = 'user'; - usernameInput.dispatchEvent(new Event('input')); - passwordInput.value = 'user'; - passwordInput.dispatchEvent(new Event('input')); - rememberMeCheckbox.dispatchEvent(new MouseEvent('click')); - - fixture.detectChanges(); - - expect(component.form.value).toEqual({ - username: 'user', - password: 'user', - rememberMe: true - }); - }); - - it('login should work well', () => { - fixture.detectChanges(); - - const mockValue = { - username: 'user', - password: 'user', - rememberMe: true - }; - - mockInternalUserService.tryLogin.withArgs(mockValue).and.returnValue(of(<UserInfo>{ username: 'user', roles: ['user'] })); - - component.form.setValue(mockValue); - component.onLoginButtonClick(); - - expect(mockInternalUserService.tryLogin).toHaveBeenCalledWith(mockValue); - expect(mockInternalUserService.userRouteNavigate).toHaveBeenCalledWith(['success', { fromlogin: 'true' }]); - }); - - describe('message display', () => { - it('nologin reason should display', () => { - fixture.detectChanges(); - component.message = 'nologin'; - fixture.detectChanges(); - expect((fixture.debugElement.query(By.css('p')).nativeElement as - HTMLParagraphElement).textContent).toBe('You haven\'t login.'); - }); - - it('invalid login reason should display', () => { - fixture.detectChanges(); - component.message = 'invalidlogin'; - fixture.detectChanges(); - expect((fixture.debugElement.query(By.css('p')).nativeElement as - HTMLParagraphElement).textContent).toBe('Your login is no longer valid.'); - }); - - it('custom error message should display', () => { - const customMessage = 'custom message'; - - fixture.detectChanges(); - - const mockValue = { - username: 'user', - password: 'user', - rememberMe: false - }; - mockInternalUserService.tryLogin.withArgs(mockValue).and.returnValue(throwError(new Error(customMessage))); - component.form.setValue(mockValue); - component.onLoginButtonClick(); - - fixture.detectChanges(); - expect(component.message).toBe(customMessage); - expect((fixture.debugElement.query(By.css('p')).nativeElement as - HTMLParagraphElement).textContent).toBe(customMessage); - }); - }); -}); diff --git a/Timeline/ClientApp/src/app/user/user-login/user-login.component.ts b/Timeline/ClientApp/src/app/user/user-login/user-login.component.ts deleted file mode 100644 index 4395c5cf..00000000 --- a/Timeline/ClientApp/src/app/user/user-login/user-login.component.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { Component, OnInit } from '@angular/core'; -import { FormGroup, FormControl } from '@angular/forms'; - -import { InternalUserService } from '../internal-user-service/internal-user.service'; - - -export type LoginMessage = 'nologin' | 'invalidlogin' | string | null | undefined; - -@Component({ - selector: 'app-user-login', - templateUrl: './user-login.component.html', - styleUrls: ['./user-login.component.css'] -}) -export class UserLoginComponent implements OnInit { - - constructor(private userService: InternalUserService) { } - - message: LoginMessage; - - form = new FormGroup({ - username: new FormControl(''), - password: new FormControl(''), - rememberMe: new FormControl(false) - }); - - ngOnInit() { - if (this.userService.currentUserInfo) { - throw new Error('Route error! Already login!'); - } - this.message = 'nologin'; - } - - onLoginButtonClick() { - this.userService.tryLogin(this.form.value).subscribe(_ => { - this.userService.userRouteNavigate(['success', { fromlogin: 'true' }]); - }, (error: Error) => this.message = error.message); - } -} diff --git a/Timeline/ClientApp/src/app/user/user-logout/user-logout.component.css b/Timeline/ClientApp/src/app/user/user-logout/user-logout.component.css deleted file mode 100644 index e69de29b..00000000 --- a/Timeline/ClientApp/src/app/user/user-logout/user-logout.component.css +++ /dev/null diff --git a/Timeline/ClientApp/src/app/user/user-logout/user-logout.component.html b/Timeline/ClientApp/src/app/user/user-logout/user-logout.component.html deleted file mode 100644 index 309e5c83..00000000 --- a/Timeline/ClientApp/src/app/user/user-logout/user-logout.component.html +++ /dev/null @@ -1 +0,0 @@ -<p class="mat-body">Logout successfully!</p> diff --git a/Timeline/ClientApp/src/app/user/user-logout/user-logout.component.spec.ts b/Timeline/ClientApp/src/app/user/user-logout/user-logout.component.spec.ts deleted file mode 100644 index 855ea4a1..00000000 --- a/Timeline/ClientApp/src/app/user/user-logout/user-logout.component.spec.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; - -import { UserLogoutComponent } from './user-logout.component'; -import { InternalUserService } from '../internal-user-service/internal-user.service'; - -describe('UserLogoutComponent', () => { - let component: UserLogoutComponent; - let fixture: ComponentFixture<UserLogoutComponent>; - - let mockInternalUserService: jasmine.SpyObj<InternalUserService>; - - beforeEach(async(() => { - mockInternalUserService = jasmine.createSpyObj('InternalUserService', ['logout']); - - TestBed.configureTestingModule({ - declarations: [UserLogoutComponent], - providers: [{ provide: InternalUserService, useValue: mockInternalUserService }] - }) - .compileComponents(); - })); - - beforeEach(() => { - fixture = TestBed.createComponent(UserLogoutComponent); - component = fixture.componentInstance; - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); - - it('should logout on init', () => { - fixture.detectChanges(); - expect(mockInternalUserService.logout).toHaveBeenCalled(); - }); -}); diff --git a/Timeline/ClientApp/src/app/user/user-logout/user-logout.component.ts b/Timeline/ClientApp/src/app/user/user-logout/user-logout.component.ts deleted file mode 100644 index e004196f..00000000 --- a/Timeline/ClientApp/src/app/user/user-logout/user-logout.component.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Component, OnInit } from '@angular/core'; - -import { InternalUserService } from '../internal-user-service/internal-user.service'; - -@Component({ - selector: 'app-user-logout', - templateUrl: './user-logout.component.html', - styleUrls: ['./user-logout.component.css'] -}) -export class UserLogoutComponent implements OnInit { - constructor(private userService: InternalUserService) { } - - ngOnInit() { - this.userService.logout(); - } -} diff --git a/Timeline/ClientApp/src/app/user/user.module.ts b/Timeline/ClientApp/src/app/user/user.module.ts deleted file mode 100644 index 59193380..00000000 --- a/Timeline/ClientApp/src/app/user/user.module.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { NgModule } from '@angular/core'; -import { CommonModule } from '@angular/common'; -import { ReactiveFormsModule } from '@angular/forms'; -import { HttpClientModule } from '@angular/common/http'; -import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { RouterModule } from '@angular/router'; -import { - MatFormFieldModule, MatProgressSpinnerModule, - MatDialogModule, MatInputModule, MatButtonModule, MatSnackBarModule, MatCheckboxModule -} from '@angular/material'; - -import { RequireNoLoginGuard, RequireLoginGuard } from './auth.guard'; -import { UserDialogComponent } from './user-dialog/user-dialog.component'; -import { UserLoginComponent } from './user-login/user-login.component'; -import { UserLoginSuccessComponent } from './user-login-success/user-login-success.component'; -import { RedirectComponent } from './redirect.component'; -import { UtilityModule } from '../utilities/utility.module'; -import { WINDOW } from './window-inject-token'; -import { UserLogoutComponent } from './user-logout/user-logout.component'; - -@NgModule({ - declarations: [UserDialogComponent, UserLoginComponent, UserLoginSuccessComponent, RedirectComponent, UserLogoutComponent], - imports: [ - RouterModule.forChild([ - { path: 'login', canActivate: [RequireNoLoginGuard], component: UserLoginComponent, outlet: 'user' }, - { path: 'success', canActivate: [RequireLoginGuard], component: UserLoginSuccessComponent, outlet: 'user' }, - { path: 'logout', canActivate: [RequireLoginGuard], component: UserLogoutComponent, outlet: 'user' }, - { path: '**', component: RedirectComponent, outlet: 'user' } - ]), - CommonModule, HttpClientModule, ReactiveFormsModule, BrowserAnimationsModule, - MatFormFieldModule, MatProgressSpinnerModule, MatDialogModule, MatInputModule, MatButtonModule, MatCheckboxModule, MatSnackBarModule, - UtilityModule - ], - providers: [{ provide: WINDOW, useValue: window }], - exports: [RouterModule], - entryComponents: [UserDialogComponent] -}) -export class UserModule { } diff --git a/Timeline/ClientApp/src/app/user/user.service.ts b/Timeline/ClientApp/src/app/user/user.service.ts deleted file mode 100644 index 6cae2d31..00000000 --- a/Timeline/ClientApp/src/app/user/user.service.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { Injectable } from '@angular/core'; -import { MatDialog, MatDialogRef } from '@angular/material'; -import { Router, ActivationStart } from '@angular/router'; - -import { Observable } from 'rxjs'; - -import { UserInfo } from './entities'; -import { InternalUserService } from './internal-user-service/internal-user.service'; -import { UserDialogComponent } from './user-dialog/user-dialog.component'; - - -/** - * This service provides public api of user module. - */ -@Injectable({ - providedIn: 'root' -}) -export class UserService { - - private dialogRef: MatDialogRef<UserDialogComponent> | null = null; - - constructor(router: Router, private dialog: MatDialog, private internalService: InternalUserService) { - router.events.subscribe(event => { - if (event instanceof ActivationStart && event.snapshot.outlet === 'user') { - if (!this.dialogRef) { - setTimeout(() => this.openUserDialog(), 0); - } - } - }); - } - - get currentUserInfo(): UserInfo | null | undefined { - return this.internalService.currentUserInfo; - } - - get userInfo$(): Observable<UserInfo | null> { - return this.internalService.userInfo$; - } - - private openUserDialog() { - if (this.dialogRef) { - return; - } - - this.dialogRef = this.dialog.open(UserDialogComponent, { - width: '300px' - }); - - const subscription = this.dialogRef.afterClosed().subscribe(_ => { - this.internalService.userRouteNavigate(null); - this.dialogRef = null; - subscription.unsubscribe(); - }); - } -} diff --git a/Timeline/ClientApp/src/app/user/window-inject-token.ts b/Timeline/ClientApp/src/app/user/window-inject-token.ts deleted file mode 100644 index 9f8723f6..00000000 --- a/Timeline/ClientApp/src/app/user/window-inject-token.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { InjectionToken } from '@angular/core'; - -export const WINDOW = new InjectionToken<Window>('global window'); diff --git a/Timeline/ClientApp/src/app/utilities/debounce-click.directive.spec.ts b/Timeline/ClientApp/src/app/utilities/debounce-click.directive.spec.ts deleted file mode 100644 index 89f66b99..00000000 --- a/Timeline/ClientApp/src/app/utilities/debounce-click.directive.spec.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { Component, ViewChild } from '@angular/core'; -import { async, TestBed, ComponentFixture, fakeAsync, tick } from '@angular/core/testing'; -import { By } from '@angular/platform-browser'; - -import { DebounceClickDirective } from './debounce-click.directive'; - -interface TestComponent { - clickHandler: () => void; -} - -@Component({ - selector: 'app-default-test', - template: '<button (appDebounceClick)="clickHandler()"></button>' -}) -class DefaultDebounceTimeTestComponent { - - @ViewChild(DebounceClickDirective) directive!: DebounceClickDirective; - - clickHandler: () => void = () => { }; -} - -@Component({ - selector: 'app-default-test', - template: '<button (appDebounceClick)="clickHandler()" [appDebounceClickTime]="debounceTime"></button>' -}) -class CustomDebounceTimeTestComponent { - debounceTime: number | undefined; - - @ViewChild(DebounceClickDirective) directive!: DebounceClickDirective; - - clickHandler: () => void = () => { }; -} - - -describe('DebounceClickDirective', () => { - let counter: number; - - function initComponent(component: TestComponent) { - component.clickHandler = () => counter++; - } - - beforeEach(() => { - counter = 0; - }); - - describe('default debounce time', () => { - let component: DefaultDebounceTimeTestComponent; - let componentFixture: ComponentFixture<DefaultDebounceTimeTestComponent>; - - beforeEach(async(() => { - TestBed.configureTestingModule({ - declarations: [DebounceClickDirective, DefaultDebounceTimeTestComponent] - }).compileComponents(); - })); - - beforeEach(() => { - componentFixture = TestBed.createComponent(DefaultDebounceTimeTestComponent); - component = componentFixture.componentInstance; - initComponent(component); - }); - - it('should create an instance', () => { - componentFixture.detectChanges(); - expect(component.directive).toBeTruthy(); - }); - - it('should work well', fakeAsync(() => { - function click() { - (<HTMLButtonElement>componentFixture.debugElement.query(By.css('button')).nativeElement).dispatchEvent(new MouseEvent('click')); - } - componentFixture.detectChanges(); - expect(counter).toBe(0); - click(); - tick(300); - expect(counter).toBe(0); - click(); - tick(); - expect(counter).toBe(0); - tick(500); - expect(counter).toBe(1); - })); - }); - - - describe('custom debounce time', () => { - let component: CustomDebounceTimeTestComponent; - let componentFixture: ComponentFixture<CustomDebounceTimeTestComponent>; - - beforeEach(async(() => { - TestBed.configureTestingModule({ - declarations: [DebounceClickDirective, CustomDebounceTimeTestComponent] - }).compileComponents(); - })); - - beforeEach(() => { - componentFixture = TestBed.createComponent(CustomDebounceTimeTestComponent); - component = componentFixture.componentInstance; - initComponent(component); - component.debounceTime = 600; - }); - - it('should create an instance', () => { - componentFixture.detectChanges(); - expect(component.directive).toBeTruthy(); - }); - - it('should work well', fakeAsync(() => { - function click() { - (<HTMLButtonElement>componentFixture.debugElement.query(By.css('button')).nativeElement).dispatchEvent(new MouseEvent('click')); - } - componentFixture.detectChanges(); - expect(counter).toBe(0); - click(); - tick(300); - expect(counter).toBe(0); - click(); - tick(); - expect(counter).toBe(0); - tick(600); - expect(counter).toBe(1); - })); - }); -}); diff --git a/Timeline/ClientApp/src/app/utilities/debounce-click.directive.ts b/Timeline/ClientApp/src/app/utilities/debounce-click.directive.ts deleted file mode 100644 index 1d01b671..00000000 --- a/Timeline/ClientApp/src/app/utilities/debounce-click.directive.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { Directive, Output, Input, EventEmitter, ElementRef, OnInit, OnDestroy } from '@angular/core'; -import { fromEvent, Subscription } from 'rxjs'; -import { debounceTime } from 'rxjs/operators'; - -@Directive({ - selector: '[appDebounceClick]' -}) -export class DebounceClickDirective implements OnInit, OnDestroy { - - private subscription: Subscription | undefined; - - @Output('appDebounceClick') clickEvent = new EventEmitter<any>(); - - // tslint:disable-next-line:no-input-rename - @Input('appDebounceClickTime') - set debounceTime(value: number) { - if (this.subscription) { - this.subscription.unsubscribe(); - } - this.subscription = fromEvent(<HTMLElement>this.element.nativeElement, 'click').pipe( - debounceTime(value) - ).subscribe(o => this.clickEvent.emit(o)); - } - - constructor(private element: ElementRef) { - } - - ngOnInit() { - if (!this.subscription) { - this.subscription = fromEvent(<HTMLElement>this.element.nativeElement, 'click').pipe( - debounceTime(500) - ).subscribe(o => this.clickEvent.emit(o)); - } - } - - ngOnDestroy() { - if (this.subscription) { - this.subscription.unsubscribe(); - } - } -} diff --git a/Timeline/ClientApp/src/app/utilities/language-untilities.ts b/Timeline/ClientApp/src/app/utilities/language-untilities.ts deleted file mode 100644 index 94434665..00000000 --- a/Timeline/ClientApp/src/app/utilities/language-untilities.ts +++ /dev/null @@ -1,18 +0,0 @@ -export function nullIfUndefined<T>(value: T | undefined): T | null { - return value === undefined ? null : value; -} - -export function throwIfNullOrUndefined<T>(value: T | null | undefined, - message: string | (() => string) = 'Value mustn\'t be null or undefined'): T | never { - if (value === null || value === undefined) { - throw new Error(typeof message === 'string' ? message : message()); - } else { - return value; - } -} - -export function repeat(time: number, action: (index?: number) => void) { - for (let i = 0; i < time; i++) { - action(i); - } -} diff --git a/Timeline/ClientApp/src/app/utilities/utility.module.ts b/Timeline/ClientApp/src/app/utilities/utility.module.ts deleted file mode 100644 index dd686bf7..00000000 --- a/Timeline/ClientApp/src/app/utilities/utility.module.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { NgModule } from '@angular/core'; -import { CommonModule } from '@angular/common'; - -import { DebounceClickDirective } from './debounce-click.directive'; - -@NgModule({ - declarations: [DebounceClickDirective], - imports: [CommonModule], - exports: [DebounceClickDirective] -}) -export class UtilityModule { } diff --git a/Timeline/ClientApp/src/assets/.gitkeep b/Timeline/ClientApp/src/assets/.gitkeep deleted file mode 100644 index e69de29b..00000000 --- a/Timeline/ClientApp/src/assets/.gitkeep +++ /dev/null diff --git a/Timeline/ClientApp/src/assets/icon.svg b/Timeline/ClientApp/src/assets/icon.svg deleted file mode 100644 index a04bddac..00000000 --- a/Timeline/ClientApp/src/assets/icon.svg +++ /dev/null @@ -1,7 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> -<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 1000 1000"> -<line fill="none" stroke="#ffffff" stroke-width="200" x1="500" y1="0" x2="500" y2="250"/> -<circle fill="none" stroke="#ffffff" stroke-width="120" cx="500" cy="500" r="250"/> -<line fill="none" stroke="#ffffff" stroke-width="200" x1="500" y1="750" x2="500" y2="1000"/> -</svg> diff --git a/Timeline/ClientApp/src/browserslist b/Timeline/ClientApp/src/browserslist deleted file mode 100644 index 8e09ab49..00000000 --- a/Timeline/ClientApp/src/browserslist +++ /dev/null @@ -1,9 +0,0 @@ -# This file is currently used by autoprefixer to adjust CSS to support the below specified browsers -# For additional information regarding the format and rule options, please see: -# https://github.com/browserslist/browserslist#queries -# For IE 9-11 support, please uncomment the last line of the file and adjust as needed -> 0.5% -last 2 versions -Firefox ESR -not dead -# IE 9-11
\ No newline at end of file diff --git a/Timeline/ClientApp/src/environments/environment.prod.ts b/Timeline/ClientApp/src/environments/environment.prod.ts deleted file mode 100644 index 3612073b..00000000 --- a/Timeline/ClientApp/src/environments/environment.prod.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const environment = { - production: true -}; diff --git a/Timeline/ClientApp/src/environments/environment.ts b/Timeline/ClientApp/src/environments/environment.ts deleted file mode 100644 index 012182ef..00000000 --- a/Timeline/ClientApp/src/environments/environment.ts +++ /dev/null @@ -1,15 +0,0 @@ -// This file can be replaced during build by using the `fileReplacements` array. -// `ng build ---prod` replaces `environment.ts` with `environment.prod.ts`. -// The list of file replacements can be found in `angular.json`. - -export const environment = { - production: false -}; - -/* - * In development mode, to ignore zone related error stack frames such as - * `zone.run`, `zoneDelegate.invokeTask` for easier debugging, you can - * import the following file, but please comment it out in production mode - * because it will have performance impact when throw error - */ -// import 'zone.js/dist/zone-error'; // Included with Angular CLI. diff --git a/Timeline/ClientApp/src/index.html b/Timeline/ClientApp/src/index.html deleted file mode 100644 index 58959f75..00000000 --- a/Timeline/ClientApp/src/index.html +++ /dev/null @@ -1,17 +0,0 @@ -<!doctype html> -<html lang="en"> -<head> - <meta charset="utf-8"> - <title>Timeline</title> - <base href="/"> - - <meta name="viewport" content="width=device-width, initial-scale=1"> - <link rel="icon" type="image/x-icon" href="favicon.ico"> - - <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet"> - <link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500" rel="stylesheet"> -</head> -<body> - <app-root>Loading...</app-root> -</body> -</html> diff --git a/Timeline/ClientApp/src/karma.conf.js b/Timeline/ClientApp/src/karma.conf.js deleted file mode 100644 index 775e624c..00000000 --- a/Timeline/ClientApp/src/karma.conf.js +++ /dev/null @@ -1,32 +0,0 @@ -// Karma configuration file, see link for more information -// https://karma-runner.github.io/1.0/config/configuration-file.html - -module.exports = function (config) { - config.set({ - basePath: '', - frameworks: ['jasmine', '@angular-devkit/build-angular'], - plugins: [ - require('karma-jasmine'), - require('karma-chrome-launcher'), - require('karma-jasmine-html-reporter'), - require('karma-coverage-istanbul-reporter'), - require('karma-junit-reporter'), - require('@angular-devkit/build-angular/plugins/karma') - ], - client: { - clearContext: false // leave Jasmine Spec Runner output visible in browser - }, - coverageIstanbulReporter: { - dir: require('path').join(__dirname, '../coverage'), - reports: ['html', 'lcovonly'], - fixWebpackSourcePaths: true - }, - reporters: ['progress', 'kjhtml'], - port: 9876, - colors: true, - logLevel: config.LOG_INFO, - autoWatch: true, - browsers: ['Chrome'], - singleRun: false - }); -}; diff --git a/Timeline/ClientApp/src/main.ts b/Timeline/ClientApp/src/main.ts deleted file mode 100644 index a2f708cb..00000000 --- a/Timeline/ClientApp/src/main.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { enableProdMode } from '@angular/core'; -import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; - -import { AppModule } from './app/app.module'; -import { environment } from './environments/environment'; - -export function getBaseUrl() { - return document.getElementsByTagName('base')[0].href; -} - -const providers = [ - { provide: 'BASE_URL', useFactory: getBaseUrl, deps: [] } -]; - -if (environment.production) { - enableProdMode(); -} - -platformBrowserDynamic(providers).bootstrapModule(AppModule) - .catch(err => console.log(err)); diff --git a/Timeline/ClientApp/src/polyfills.ts b/Timeline/ClientApp/src/polyfills.ts deleted file mode 100644 index d310405a..00000000 --- a/Timeline/ClientApp/src/polyfills.ts +++ /dev/null @@ -1,80 +0,0 @@ -/** - * This file includes polyfills needed by Angular and is loaded before the app. - * You can add your own extra polyfills to this file. - * - * This file is divided into 2 sections: - * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. - * 2. Application imports. Files imported after ZoneJS that should be loaded before your main - * file. - * - * The current setup is for so-called "evergreen" browsers; the last versions of browsers that - * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), - * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. - * - * Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html - */ - -/*************************************************************************************************** - * BROWSER POLYFILLS - */ - -/** IE9, IE10 and IE11 requires all of the following polyfills. **/ -// import 'core-js/es6/symbol'; -// import 'core-js/es6/object'; -// import 'core-js/es6/function'; -// import 'core-js/es6/parse-int'; -// import 'core-js/es6/parse-float'; -// import 'core-js/es6/number'; -// import 'core-js/es6/math'; -// import 'core-js/es6/string'; -// import 'core-js/es6/date'; -// import 'core-js/es6/array'; -// import 'core-js/es6/regexp'; -// import 'core-js/es6/map'; -// import 'core-js/es6/weak-map'; -// import 'core-js/es6/set'; - -/** IE10 and IE11 requires the following for NgClass support on SVG elements */ -// import 'classlist.js'; // Run `npm install --save classlist.js`. - -/** IE10 and IE11 requires the following for the Reflect API. */ -// import 'core-js/es6/reflect'; - - -/** Evergreen browsers require these. **/ -// Used for reflect-metadata in JIT. If you use AOT (and only Angular decorators), you can remove. -import 'core-js/es7/reflect'; - - -/** - * Web Animations `@angular/platform-browser/animations` - * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. - * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). - **/ -// import 'web-animations-js'; // Run `npm install --save web-animations-js`. - -/** - * By default, zone.js will patch all possible macroTask and DomEvents - * user can disable parts of macroTask/DomEvents patch by setting following flags - */ - - // (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame - // (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick - // (window as any).__zone_symbol__BLACK_LISTED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames - - /* - * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js - * with the following flag, it will bypass `zone.js` patch for IE/Edge - */ -// (window as any).__Zone_enable_cross_context_check = true; - -/*************************************************************************************************** - * Zone JS is required by default for Angular itself. - */ -import 'zone.js/dist/zone'; // Included with Angular CLI. - - - -/*************************************************************************************************** - * APPLICATION IMPORTS - */ diff --git a/Timeline/ClientApp/src/styles.css b/Timeline/ClientApp/src/styles.css deleted file mode 100644 index f60c9204..00000000 --- a/Timeline/ClientApp/src/styles.css +++ /dev/null @@ -1,14 +0,0 @@ -/* You can add global styles to this file, and also import other style files */ -@import "~@angular/material/prebuilt-themes/indigo-pink.css"; - -html { - overflow: unset!important; /* why cdk-global-scrollblock add overflow-y: scroll ??????????? */ -} - -body { - margin: 0; -} - -.fill-remaining-space { - flex: 1 1 auto; -} diff --git a/Timeline/ClientApp/src/test.ts b/Timeline/ClientApp/src/test.ts deleted file mode 100644 index 688add40..00000000 --- a/Timeline/ClientApp/src/test.ts +++ /dev/null @@ -1,22 +0,0 @@ -// This file is required by karma.conf.js and loads recursively all the .spec and framework files - -import 'zone.js/dist/zone-testing'; -import 'zone.js/dist/zone-patch-rxjs-fake-async'; - -import { getTestBed } from '@angular/core/testing'; -import { - BrowserDynamicTestingModule, - platformBrowserDynamicTesting -} from '@angular/platform-browser-dynamic/testing'; - -declare const require: any; - -// First, initialize the Angular testing environment. -getTestBed().initTestEnvironment( - BrowserDynamicTestingModule, - platformBrowserDynamicTesting() -); -// Then we find all the tests. -const context = require.context('./', true, /\.spec\.ts$/); -// And load the modules. -context.keys().map(context); diff --git a/Timeline/ClientApp/src/tsconfig.app.json b/Timeline/ClientApp/src/tsconfig.app.json deleted file mode 100644 index 0d3b876e..00000000 --- a/Timeline/ClientApp/src/tsconfig.app.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "extends": "../tsconfig.json", - "compilerOptions": { - "outDir": "../out-tsc/app", - "module": "es2015", - "types": [] - }, - "exclude": [ - "src/test.ts", - "test-utilities/**/*", - "**/*.spec.ts", - "**/*.mock.ts", - "**/*.test.ts" - ] -} diff --git a/Timeline/ClientApp/src/tsconfig.server.json b/Timeline/ClientApp/src/tsconfig.server.json deleted file mode 100644 index 8019d415..00000000 --- a/Timeline/ClientApp/src/tsconfig.server.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "extends": "../tsconfig.json", - "compilerOptions": { - "module": "commonjs" - }, - "angularCompilerOptions": { - "entryModule": "app/app.server.module#AppServerModule" - } -}
\ No newline at end of file diff --git a/Timeline/ClientApp/src/tsconfig.spec.json b/Timeline/ClientApp/src/tsconfig.spec.json deleted file mode 100644 index 3bcc8926..00000000 --- a/Timeline/ClientApp/src/tsconfig.spec.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "extends": "../tsconfig.json", - "compilerOptions": { - "outDir": "../out-tsc/spec", - "module": "commonjs", - "types": [ - "jasmine", - "node" - ] - }, - "files": [ - "test.ts", - "polyfills.ts" - ], - "include": [ - "test-utilities/**/*", - "**/*.spec.ts", - "**/*.d.ts", - "**/*.mock.ts", - "**/*.test.ts" - ] -} diff --git a/Timeline/ClientApp/src/tslint.json b/Timeline/ClientApp/src/tslint.json deleted file mode 100644 index 52e2c1a5..00000000 --- a/Timeline/ClientApp/src/tslint.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "extends": "../tslint.json", - "rules": { - "directive-selector": [ - true, - "attribute", - "app", - "camelCase" - ], - "component-selector": [ - true, - "element", - "app", - "kebab-case" - ] - } -} diff --git a/Timeline/ClientApp/tsconfig.json b/Timeline/ClientApp/tsconfig.json deleted file mode 100644 index 86c42495..00000000 --- a/Timeline/ClientApp/tsconfig.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "compileOnSave": false, - "compilerOptions": { - "baseUrl": "./", - "outDir": "./dist/out-tsc", - "sourceMap": true, - "declaration": false, - "moduleResolution": "node", - "emitDecoratorMetadata": true, - "experimentalDecorators": true, - "target": "es5", - "typeRoots": [ - "node_modules/@types" - ], - "lib": [ - "es2017", - "dom" - ], - "strict": true - }, - "angularCompilerOptions": { - "fullTemplateTypeCheck": true, - "strictInjectionParameters": true - } -} diff --git a/Timeline/ClientApp/tslint.json b/Timeline/ClientApp/tslint.json deleted file mode 100644 index dcc5f765..00000000 --- a/Timeline/ClientApp/tslint.json +++ /dev/null @@ -1,130 +0,0 @@ -{ - "rulesDirectory": [ - "node_modules/codelyzer" - ], - "rules": { - "arrow-return-shorthand": true, - "callable-types": true, - "class-name": true, - "comment-format": [ - true, - "check-space" - ], - "curly": true, - "deprecation": { - "severity": "warn" - }, - "eofline": true, - "forin": true, - "import-blacklist": [ - true, - "rxjs/Rx" - ], - "import-spacing": true, - "indent": [ - true, - "spaces" - ], - "interface-over-type-literal": true, - "label-position": true, - "max-line-length": [ - true, - 140 - ], - "member-access": false, - "member-ordering": [ - true, - { - "order": [ - "static-field", - "instance-field", - "static-method", - "instance-method" - ] - } - ], - "no-arg": true, - "no-bitwise": true, - "no-console": [ - true, - "debug", - "info", - "time", - "timeEnd", - "trace" - ], - "no-construct": true, - "no-debugger": true, - "no-duplicate-super": true, - "no-empty": false, - "no-empty-interface": true, - "no-eval": true, - "no-inferrable-types": [ - true, - "ignore-params" - ], - "no-misused-new": true, - "no-non-null-assertion": false, - "no-shadowed-variable": true, - "no-string-literal": false, - "no-string-throw": true, - "no-switch-case-fall-through": true, - "no-trailing-whitespace": true, - "no-unnecessary-initializer": true, - "no-unused-expression": true, - "no-use-before-declare": true, - "no-var-keyword": true, - "object-literal-sort-keys": false, - "one-line": [ - true, - "check-open-brace", - "check-catch", - "check-else", - "check-whitespace" - ], - "prefer-const": true, - "quotemark": [ - true, - "single" - ], - "radix": true, - "semicolon": [ - true, - "always" - ], - "triple-equals": [ - true, - "allow-null-check" - ], - "typedef-whitespace": [ - true, - { - "call-signature": "nospace", - "index-signature": "nospace", - "parameter": "nospace", - "property-declaration": "nospace", - "variable-declaration": "nospace" - } - ], - "unified-signatures": true, - "variable-name": false, - "whitespace": [ - true, - "check-branch", - "check-decl", - "check-operator", - "check-separator", - "check-type" - ], - "no-output-on-prefix": true, - "use-input-property-decorator": true, - "use-output-property-decorator": true, - "use-host-property-decorator": true, - "no-input-rename": true, - "no-output-rename": true, - "use-life-cycle-interface": true, - "use-pipe-transform-interface": true, - "component-class-suffix": true, - "directive-class-suffix": true - } -} diff --git a/Timeline/Configs/DatabaseConfig.cs b/Timeline/Configs/DatabaseConfig.cs new file mode 100644 index 00000000..34e5e65f --- /dev/null +++ b/Timeline/Configs/DatabaseConfig.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Timeline.Configs +{ + public class DatabaseConfig + { + public string ConnectionString { get; set; } + } +} diff --git a/Timeline/Controllers/UserController.cs b/Timeline/Controllers/UserController.cs index eb1b8513..3b4e7b4f 100644 --- a/Timeline/Controllers/UserController.cs +++ b/Timeline/Controllers/UserController.cs @@ -1,12 +1,14 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; +using System; +using System.Threading.Tasks; using Timeline.Entities; using Timeline.Services; namespace Timeline.Controllers { - [Route("api/[controller]")] + [Route("[controller]")] public class UserController : Controller { private static class LoggingEventIds @@ -16,23 +18,22 @@ namespace Timeline.Controllers } private readonly IUserService _userService; - private readonly IJwtService _jwtService; private readonly ILogger<UserController> _logger; - public UserController(IUserService userService, IJwtService jwtService, ILogger<UserController> logger) + public UserController(IUserService userService, ILogger<UserController> logger) { _userService = userService; - _jwtService = jwtService; _logger = logger; } [HttpPost("[action]")] [AllowAnonymous] - public ActionResult<CreateTokenResponse> CreateToken([FromBody] CreateTokenRequest request) + public async Task<ActionResult<CreateTokenResponse>> CreateToken([FromBody] CreateTokenRequest request) { - var user = _userService.Authenticate(request.Username, request.Password); + var result = await _userService.CreateToken(request.Username, request.Password); - if (user == null) { + if (result == null) + { _logger.LogInformation(LoggingEventIds.LogInFailed, "Attemp to login with username: {} and password: {} failed.", request.Username, request.Password); return Ok(new CreateTokenResponse { @@ -45,17 +46,46 @@ namespace Timeline.Controllers return Ok(new CreateTokenResponse { Success = true, - Token = _jwtService.GenerateJwtToken(user), - UserInfo = user.GetUserInfo() + Token = result.Token, + UserInfo = result.UserInfo }); } [HttpPost("[action]")] [AllowAnonymous] - public ActionResult<TokenValidationResponse> ValidateToken([FromBody] TokenValidationRequest request) + public async Task<ActionResult<TokenValidationResponse>> ValidateToken([FromBody] TokenValidationRequest request) { - var result = _jwtService.ValidateJwtToken(request.Token); - return Ok(result); + var result = await _userService.VerifyToken(request.Token); + + if (result == null) + { + return Ok(new TokenValidationResponse + { + IsValid = false, + }); + } + + return Ok(new TokenValidationResponse + { + IsValid = true, + UserInfo = result + }); + } + + [HttpPost("[action]")] + [Authorize(Roles = "admin")] + public async Task<ActionResult<CreateUserResponse>> CreateUser([FromBody] CreateUserRequest request) + { + var result = await _userService.CreateUser(request.Username, request.Password, request.Roles); + switch (result) + { + case CreateUserResult.Success: + return Ok(new CreateUserResponse { ReturnCode = CreateUserResponse.SuccessCode }); + case CreateUserResult.AlreadyExists: + return Ok(new CreateUserResponse { ReturnCode = CreateUserResponse.AlreadyExistsCode }); + default: + throw new Exception("Unreachable code."); + } } } } diff --git a/Timeline/Controllers/UserTestController.cs b/Timeline/Controllers/UserTestController.cs index cf5cf074..f1edb0d5 100644 --- a/Timeline/Controllers/UserTestController.cs +++ b/Timeline/Controllers/UserTestController.cs @@ -3,7 +3,7 @@ using Microsoft.AspNetCore.Mvc; namespace Timeline.Controllers { - [Route("api/test/User")] + [Route("Test/User")] public class UserTestController : Controller { [HttpGet("[action]")] @@ -14,14 +14,14 @@ namespace Timeline.Controllers } [HttpGet("[action]")] - [Authorize(Roles = "User,Admin")] + [Authorize(Roles = "user,admin")] public ActionResult BothUserAndAdmin() { return Ok(); } [HttpGet("[action]")] - [Authorize(Roles = "Admin")] + [Authorize(Roles = "admin")] public ActionResult OnlyAdmin() { return Ok(); diff --git a/Timeline/Entities/Token.cs b/Timeline/Entities/Token.cs deleted file mode 100644 index ce5b92ff..00000000 --- a/Timeline/Entities/Token.cs +++ /dev/null @@ -1,26 +0,0 @@ -namespace Timeline.Entities -{ - public class CreateTokenRequest - { - public string Username { get; set; } - public string Password { get; set; } - } - - public class CreateTokenResponse - { - public bool Success { get; set; } - public string Token { get; set; } - public UserInfo UserInfo { get; set; } - } - - public class TokenValidationRequest - { - public string Token { get; set; } - } - - public class TokenValidationResponse - { - public bool IsValid { get; set; } - public UserInfo UserInfo { get; set; } - } -} diff --git a/Timeline/Entities/User.cs b/Timeline/Entities/User.cs index c77e895d..1cb5a894 100644 --- a/Timeline/Entities/User.cs +++ b/Timeline/Entities/User.cs @@ -1,25 +1,41 @@ -namespace Timeline.Entities +namespace Timeline.Entities { - public class User + public class CreateTokenRequest { - public int Id { get; set; } public string Username { get; set; } public string Password { get; set; } - public string[] Roles { get; set; } + } - public UserInfo GetUserInfo() - { - return new UserInfo - { - Username = Username, - Roles = Roles - }; - } + public class CreateTokenResponse + { + public bool Success { get; set; } + public string Token { get; set; } + public UserInfo UserInfo { get; set; } } - public class UserInfo + public class TokenValidationRequest + { + public string Token { get; set; } + } + + public class TokenValidationResponse + { + public bool IsValid { get; set; } + public UserInfo UserInfo { get; set; } + } + + public class CreateUserRequest { public string Username { get; set; } - public string[] Roles { get; set; } + public string Password { get; set; } + public string[] Roles { get; set; } + } + + public class CreateUserResponse + { + public const int SuccessCode = 0; + public const int AlreadyExistsCode = 1; + + public int ReturnCode { get; set; } } } diff --git a/Timeline/Entities/UserInfo.cs b/Timeline/Entities/UserInfo.cs new file mode 100644 index 00000000..d9c5acad --- /dev/null +++ b/Timeline/Entities/UserInfo.cs @@ -0,0 +1,26 @@ +using System; +using System.Linq; +using Timeline.Models; + +namespace Timeline.Entities +{ + public class UserInfo + { + public UserInfo() + { + + } + + public UserInfo(User user) + { + if (user == null) + throw new ArgumentNullException(nameof(user)); + + Username = user.Name; + Roles = user.RoleString.Split(',').Select(s => s.Trim()).ToArray(); + } + + public string Username { get; set; } + public string[] Roles { get; set; } + } +} diff --git a/Timeline/Migrations/20190412102517_InitCreate.Designer.cs b/Timeline/Migrations/20190412102517_InitCreate.Designer.cs new file mode 100644 index 00000000..c68183de --- /dev/null +++ b/Timeline/Migrations/20190412102517_InitCreate.Designer.cs @@ -0,0 +1,43 @@ +// <auto-generated /> +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Timeline.Models; + +namespace Timeline.Migrations +{ + [DbContext(typeof(DatabaseContext))] + [Migration("20190412102517_InitCreate")] + partial class InitCreate + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "2.2.3-servicing-35854") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + modelBuilder.Entity("Timeline.Models.User", b => + { + b.Property<long>("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id"); + + b.Property<string>("EncryptedPassword") + .HasColumnName("password"); + + b.Property<string>("Name") + .HasColumnName("name"); + + b.Property<string>("RoleString") + .HasColumnName("roles"); + + b.HasKey("Id"); + + b.ToTable("user"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Timeline/Migrations/20190412102517_InitCreate.cs b/Timeline/Migrations/20190412102517_InitCreate.cs new file mode 100644 index 00000000..c8f3b0ac --- /dev/null +++ b/Timeline/Migrations/20190412102517_InitCreate.cs @@ -0,0 +1,32 @@ +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Timeline.Migrations +{ + public partial class InitCreate : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "user", + columns: table => new + { + id = table.Column<long>(nullable: false) + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), + name = table.Column<string>(nullable: true), + password = table.Column<string>(nullable: true), + roles = table.Column<string>(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_user", x => x.id); + }); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "user"); + } + } +} diff --git a/Timeline/Migrations/20190412144150_AddAdminUser.Designer.cs b/Timeline/Migrations/20190412144150_AddAdminUser.Designer.cs new file mode 100644 index 00000000..319c646a --- /dev/null +++ b/Timeline/Migrations/20190412144150_AddAdminUser.Designer.cs @@ -0,0 +1,43 @@ +// <auto-generated /> +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Timeline.Models; + +namespace Timeline.Migrations +{ + [DbContext(typeof(DatabaseContext))] + [Migration("20190412144150_AddAdminUser")] + partial class AddAdminUser + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "2.2.3-servicing-35854") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + modelBuilder.Entity("Timeline.Models.User", b => + { + b.Property<long>("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id"); + + b.Property<string>("EncryptedPassword") + .HasColumnName("password"); + + b.Property<string>("Name") + .HasColumnName("name"); + + b.Property<string>("RoleString") + .HasColumnName("roles"); + + b.HasKey("Id"); + + b.ToTable("user"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Timeline/Migrations/20190412144150_AddAdminUser.cs b/Timeline/Migrations/20190412144150_AddAdminUser.cs new file mode 100644 index 00000000..9fac05ff --- /dev/null +++ b/Timeline/Migrations/20190412144150_AddAdminUser.cs @@ -0,0 +1,19 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using Timeline.Services; + +namespace Timeline.Migrations +{ + public partial class AddAdminUser : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.InsertData("user", new string[] { "name", "password", "roles" }, + new string[] { "crupest", new PasswordService(null).HashPassword("yang0101"), "user,admin" }); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DeleteData("user", "name", "crupest"); + } + } +} diff --git a/Timeline/Migrations/20190412153003_MakeColumnsInUserNotNull.Designer.cs b/Timeline/Migrations/20190412153003_MakeColumnsInUserNotNull.Designer.cs new file mode 100644 index 00000000..c1d1565f --- /dev/null +++ b/Timeline/Migrations/20190412153003_MakeColumnsInUserNotNull.Designer.cs @@ -0,0 +1,46 @@ +// <auto-generated /> +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Timeline.Models; + +namespace Timeline.Migrations +{ + [DbContext(typeof(DatabaseContext))] + [Migration("20190412153003_MakeColumnsInUserNotNull")] + partial class MakeColumnsInUserNotNull + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "2.2.3-servicing-35854") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + modelBuilder.Entity("Timeline.Models.User", b => + { + b.Property<long>("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id"); + + b.Property<string>("EncryptedPassword") + .IsRequired() + .HasColumnName("password"); + + b.Property<string>("Name") + .IsRequired() + .HasColumnName("name"); + + b.Property<string>("RoleString") + .IsRequired() + .HasColumnName("roles"); + + b.HasKey("Id"); + + b.ToTable("user"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Timeline/Migrations/20190412153003_MakeColumnsInUserNotNull.cs b/Timeline/Migrations/20190412153003_MakeColumnsInUserNotNull.cs new file mode 100644 index 00000000..0b7b5f08 --- /dev/null +++ b/Timeline/Migrations/20190412153003_MakeColumnsInUserNotNull.cs @@ -0,0 +1,52 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Timeline.Migrations +{ + public partial class MakeColumnsInUserNotNull : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn<string>( + name: "roles", + table: "user", + nullable: false, + oldClrType: typeof(string), + oldNullable: true); + + migrationBuilder.AlterColumn<string>( + name: "name", + table: "user", + nullable: false, + oldClrType: typeof(string), + oldNullable: true); + + migrationBuilder.AlterColumn<string>( + name: "password", + table: "user", + nullable: false, + oldClrType: typeof(string), + oldNullable: true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn<string>( + name: "roles", + table: "user", + nullable: true, + oldClrType: typeof(string)); + + migrationBuilder.AlterColumn<string>( + name: "name", + table: "user", + nullable: true, + oldClrType: typeof(string)); + + migrationBuilder.AlterColumn<string>( + name: "password", + table: "user", + nullable: true, + oldClrType: typeof(string)); + } + } +} diff --git a/Timeline/Migrations/DatabaseContextModelSnapshot.cs b/Timeline/Migrations/DatabaseContextModelSnapshot.cs new file mode 100644 index 00000000..a833d2dc --- /dev/null +++ b/Timeline/Migrations/DatabaseContextModelSnapshot.cs @@ -0,0 +1,44 @@ +// <auto-generated /> +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Timeline.Models; + +namespace Timeline.Migrations +{ + [DbContext(typeof(DatabaseContext))] + partial class DatabaseContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "2.2.3-servicing-35854") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + modelBuilder.Entity("Timeline.Models.User", b => + { + b.Property<long>("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id"); + + b.Property<string>("EncryptedPassword") + .IsRequired() + .HasColumnName("password"); + + b.Property<string>("Name") + .IsRequired() + .HasColumnName("name"); + + b.Property<string>("RoleString") + .IsRequired() + .HasColumnName("roles"); + + b.HasKey("Id"); + + b.ToTable("user"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Timeline/Models/DatabaseContext.cs b/Timeline/Models/DatabaseContext.cs new file mode 100644 index 00000000..1e89ea82 --- /dev/null +++ b/Timeline/Models/DatabaseContext.cs @@ -0,0 +1,33 @@ +using Microsoft.EntityFrameworkCore; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Timeline.Models +{ + [Table("user")] + public class User + { + [Column("id"), Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public long Id { get; set; } + + [Column("name"), Required] + public string Name { get; set; } + + [Column("password"), Required] + public string EncryptedPassword { get; set; } + + [Column("roles"), Required] + public string RoleString { get; set; } + } + + public class DatabaseContext : DbContext + { + public DatabaseContext(DbContextOptions<DatabaseContext> options) + : base(options) + { + + } + + public DbSet<User> Users { get; set; } + } +} diff --git a/Timeline/Properties/launchSettings.json b/Timeline/Properties/launchSettings.json index a07a7868..5d9312b5 100644 --- a/Timeline/Properties/launchSettings.json +++ b/Timeline/Properties/launchSettings.json @@ -10,14 +10,12 @@ "profiles": { "IIS Express": { "commandName": "IISExpress", - "launchBrowser": true, "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } }, "Timeline": { "commandName": "Project", - "launchBrowser": true, "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } diff --git a/Timeline/Services/JwtService.cs b/Timeline/Services/JwtService.cs index abdde908..91e7f879 100644 --- a/Timeline/Services/JwtService.cs +++ b/Timeline/Services/JwtService.cs @@ -7,34 +7,25 @@ using System.Linq; using System.Security.Claims; using System.Text; using Timeline.Configs; -using Timeline.Entities; namespace Timeline.Services { public interface IJwtService { /// <summary> - /// Create a JWT token for a given user. - /// Return null if <paramref name="user"/> is null. + /// Create a JWT token for a given user id. /// </summary> - /// <param name="user">The user to generate token.</param> - /// <returns>The generated token or null if <paramref name="user"/> is null.</returns> - string GenerateJwtToken(User user); + /// <param name="userId">The user id used to generate token.</param> + /// <returns>Return the generated token.</returns> + string GenerateJwtToken(long userId, string[] roles); /// <summary> - /// Validate a JWT token. + /// Verify a JWT token. /// Return null is <paramref name="token"/> is null. - /// If token is invalid, return a <see cref="TokenValidationResponse"/> with - /// <see cref="TokenValidationResponse.IsValid"/> set to false and - /// <see cref="TokenValidationResponse.UserInfo"/> set to null. - /// If token is valid, return a <see cref="TokenValidationResponse"/> with - /// <see cref="TokenValidationResponse.IsValid"/> set to true and - /// <see cref="TokenValidationResponse.UserInfo"/> filled with the user info - /// in the token. /// </summary> - /// <param name="token">The token string to validate.</param> - /// <returns>Null if <paramref name="token"/> is null. Or the result.</returns> - TokenValidationResponse ValidateJwtToken(string token); + /// <param name="token">The token string to verify.</param> + /// <returns>Return null if <paramref name="token"/> is null or token is invalid. Return the saved user id otherwise.</returns> + long? VerifyJwtToken(string token); } @@ -50,17 +41,13 @@ namespace Timeline.Services _logger = logger; } - public string GenerateJwtToken(User user) + public string GenerateJwtToken(long id, string[] roles) { - if (user == null) - return null; - var jwtConfig = _jwtConfig.CurrentValue; var identity = new ClaimsIdentity(); - identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, user.Id.ToString())); - identity.AddClaim(new Claim(identity.NameClaimType, user.Username)); - identity.AddClaims(user.Roles.Select(role => new Claim(identity.RoleClaimType, role))); + identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, id.ToString())); + identity.AddClaims(roles.Select(role => new Claim(identity.RoleClaimType, role))); var tokenDescriptor = new SecurityTokenDescriptor() { @@ -80,7 +67,7 @@ namespace Timeline.Services } - public TokenValidationResponse ValidateJwtToken(string token) + public long? VerifyJwtToken(string token) { if (token == null) return null; @@ -100,24 +87,12 @@ namespace Timeline.Services IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(config.SigningKey)) }, out SecurityToken validatedToken); - var identity = principal.Identity as ClaimsIdentity; - - var userInfo = new UserInfo - { - Username = identity.FindAll(identity.NameClaimType).Select(claim => claim.Value).Single(), - Roles = identity.FindAll(identity.RoleClaimType).Select(claim => claim.Value).ToArray() - }; - - return new TokenValidationResponse - { - IsValid = true, - UserInfo = userInfo - }; + return long.Parse(principal.FindAll(ClaimTypes.NameIdentifier).Single().Value); } catch (Exception e) { _logger.LogInformation(e, "Token validation failed! Token is {} .", token); - return new TokenValidationResponse { IsValid = false }; + return null; } } } diff --git a/Timeline/Services/PasswordService.cs b/Timeline/Services/PasswordService.cs new file mode 100644 index 00000000..8eab526e --- /dev/null +++ b/Timeline/Services/PasswordService.cs @@ -0,0 +1,205 @@ +using Microsoft.AspNetCore.Cryptography.KeyDerivation; +using Microsoft.Extensions.Logging; +using System; +using System.Runtime.CompilerServices; +using System.Security.Cryptography; + +namespace Timeline.Services +{ + public interface IPasswordService + { + /// <summary> + /// Returns a hashed representation of the supplied <paramref name="password"/>. + /// </summary> + /// <param name="password">The password to hash.</param> + /// <returns>A hashed representation of the supplied <paramref name="password"/>.</returns> + string HashPassword(string password); + + /// <summary> + /// Returns a boolean indicating the result of a password hash comparison. + /// </summary> + /// <param name="hashedPassword">The hash value for a user's stored password.</param> + /// <param name="providedPassword">The password supplied for comparison.</param> + /// <returns>True indicating success. Otherwise false.</returns> + bool VerifyPassword(string hashedPassword, string providedPassword); + } + + /// <summary> + /// Copied from https://github.com/aspnet/AspNetCore/blob/master/src/Identity/Extensions.Core/src/PasswordHasher.cs + /// Remove V2 format and unnecessary format version check. + /// Remove configuration options. + /// Remove user related parts. + /// Add log for wrong format. + /// </summary> + public class PasswordService : IPasswordService + { + /* ======================= + * HASHED PASSWORD FORMATS + * ======================= + * + * Version 3: + * PBKDF2 with HMAC-SHA256, 128-bit salt, 256-bit subkey, 10000 iterations. + * Format: { 0x01, prf (UInt32), iter count (UInt32), salt length (UInt32), salt, subkey } + * (All UInt32s are stored big-endian.) + */ + + private static EventId BadFormatEventId { get; } = new EventId(4000, "BadFormatPassword"); + + private readonly RandomNumberGenerator _rng = RandomNumberGenerator.Create(); + private readonly ILogger<PasswordService> _logger; + + public PasswordService(ILogger<PasswordService> logger) + { + _logger = logger; + } + + + // Compares two byte arrays for equality. The method is specifically written so that the loop is not optimized. + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] + private static bool ByteArraysEqual(byte[] a, byte[] b) + { + if (a == null && b == null) + { + return true; + } + if (a == null || b == null || a.Length != b.Length) + { + return false; + } + var areSame = true; + for (var i = 0; i < a.Length; i++) + { + areSame &= (a[i] == b[i]); + } + return areSame; + } + + public string HashPassword(string password) + { + if (password == null) + throw new ArgumentNullException(nameof(password)); + return Convert.ToBase64String(HashPasswordV3(password, _rng)); + } + + private byte[] HashPasswordV3(string password, RandomNumberGenerator rng) + { + return HashPasswordV3(password, rng, + prf: KeyDerivationPrf.HMACSHA256, + iterCount: 10000, + saltSize: 128 / 8, + numBytesRequested: 256 / 8); + } + + private static byte[] HashPasswordV3(string password, RandomNumberGenerator rng, KeyDerivationPrf prf, int iterCount, int saltSize, int numBytesRequested) + { + // Produce a version 3 (see comment above) text hash. + byte[] salt = new byte[saltSize]; + rng.GetBytes(salt); + byte[] subkey = KeyDerivation.Pbkdf2(password, salt, prf, iterCount, numBytesRequested); + + var outputBytes = new byte[13 + salt.Length + subkey.Length]; + outputBytes[0] = 0x01; // format marker + WriteNetworkByteOrder(outputBytes, 1, (uint)prf); + WriteNetworkByteOrder(outputBytes, 5, (uint)iterCount); + WriteNetworkByteOrder(outputBytes, 9, (uint)saltSize); + Buffer.BlockCopy(salt, 0, outputBytes, 13, salt.Length); + Buffer.BlockCopy(subkey, 0, outputBytes, 13 + saltSize, subkey.Length); + return outputBytes; + } + + private void LogBadFormatError(string hashedPassword, string message, Exception exception = null) + { + if (_logger == null) + return; + + if (exception != null) + _logger.LogError(BadFormatEventId, exception, $"{message} Hashed password is {hashedPassword} ."); + else + _logger.LogError(BadFormatEventId, $"{message} Hashed password is {hashedPassword} ."); + } + + public virtual bool VerifyPassword(string hashedPassword, string providedPassword) + { + if (hashedPassword == null) + throw new ArgumentNullException(nameof(hashedPassword)); + if (providedPassword == null) + throw new ArgumentNullException(nameof(providedPassword)); + + byte[] decodedHashedPassword = Convert.FromBase64String(hashedPassword); + + // read the format marker from the hashed password + if (decodedHashedPassword.Length == 0) + { + LogBadFormatError(hashedPassword, "Decoded hashed password is of length 0."); + return false; + } + switch (decodedHashedPassword[0]) + { + case 0x01: + return VerifyHashedPasswordV3(decodedHashedPassword, providedPassword, hashedPassword); + + default: + LogBadFormatError(hashedPassword, "Unknown format marker."); + return false; // unknown format marker + } + } + + private bool VerifyHashedPasswordV3(byte[] hashedPassword, string password, string hashedPasswordString) + { + try + { + // Read header information + KeyDerivationPrf prf = (KeyDerivationPrf)ReadNetworkByteOrder(hashedPassword, 1); + int iterCount = (int)ReadNetworkByteOrder(hashedPassword, 5); + int saltLength = (int)ReadNetworkByteOrder(hashedPassword, 9); + + // Read the salt: must be >= 128 bits + if (saltLength < 128 / 8) + { + LogBadFormatError(hashedPasswordString, "Salt length < 128 bits."); + return false; + } + byte[] salt = new byte[saltLength]; + Buffer.BlockCopy(hashedPassword, 13, salt, 0, salt.Length); + + // Read the subkey (the rest of the payload): must be >= 128 bits + int subkeyLength = hashedPassword.Length - 13 - salt.Length; + if (subkeyLength < 128 / 8) + { + LogBadFormatError(hashedPasswordString, "Subkey length < 128 bits."); + return false; + } + byte[] expectedSubkey = new byte[subkeyLength]; + Buffer.BlockCopy(hashedPassword, 13 + salt.Length, expectedSubkey, 0, expectedSubkey.Length); + + // Hash the incoming password and verify it + byte[] actualSubkey = KeyDerivation.Pbkdf2(password, salt, prf, iterCount, subkeyLength); + return ByteArraysEqual(actualSubkey, expectedSubkey); + } + catch (Exception e) + { + // This should never occur except in the case of a malformed payload, where + // we might go off the end of the array. Regardless, a malformed payload + // implies verification failed. + LogBadFormatError(hashedPasswordString, "See exception.", e); + return false; + } + } + + private static uint ReadNetworkByteOrder(byte[] buffer, int offset) + { + return ((uint)(buffer[offset + 0]) << 24) + | ((uint)(buffer[offset + 1]) << 16) + | ((uint)(buffer[offset + 2]) << 8) + | ((uint)(buffer[offset + 3])); + } + + private static void WriteNetworkByteOrder(byte[] buffer, int offset, uint value) + { + buffer[offset + 0] = (byte)(value >> 24); + buffer[offset + 1] = (byte)(value >> 16); + buffer[offset + 2] = (byte)(value >> 8); + buffer[offset + 3] = (byte)(value >> 0); + } + } +} diff --git a/Timeline/Services/UserService.cs b/Timeline/Services/UserService.cs index 1da6922d..ad36c37b 100644 --- a/Timeline/Services/UserService.cs +++ b/Timeline/Services/UserService.cs @@ -1,31 +1,126 @@ -using System.Collections.Generic; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; using System.Linq; +using System.Threading.Tasks; using Timeline.Entities; +using Timeline.Models; namespace Timeline.Services { + public class CreateTokenResult + { + public string Token { get; set; } + public UserInfo UserInfo { get; set; } + } + + public enum CreateUserResult + { + Success, + AlreadyExists + } + public interface IUserService { /// <summary> /// Try to anthenticate with the given username and password. + /// If success, create a token and return the user info. /// </summary> /// <param name="username">The username of the user to be anthenticated.</param> /// <param name="password">The password of the user to be anthenticated.</param> - /// <returns><c>null</c> if anthentication failed. - /// An instance of <see cref="User"/> if anthentication succeeded.</returns> - User Authenticate(string username, string password); + /// <returns>Return null if anthentication failed. An <see cref="CreateTokenResult"/> containing the created token and user info if anthentication succeeded.</returns> + Task<CreateTokenResult> CreateToken(string username, string password); + + /// <summary> + /// Verify the given token. + /// If success, return the user info. + /// </summary> + /// <param name="token">The token to verify.</param> + /// <returns>Return null if verification failed. The user info if verification succeeded.</returns> + Task<UserInfo> VerifyToken(string token); + + Task<CreateUserResult> CreateUser(string username, string password, string[] roles); } public class UserService : IUserService { - private readonly IList<User> _users = new List<User>{ - new User { Id = 0, Username = "admin", Password = "admin", Roles = new string[] { "User", "Admin" } }, - new User { Id = 1, Username = "user", Password = "user", Roles = new string[] { "User"} } - }; + private readonly ILogger<UserService> _logger; + private readonly DatabaseContext _databaseContext; + private readonly IJwtService _jwtService; + private readonly IPasswordService _passwordService; + + public UserService(ILogger<UserService> logger, DatabaseContext databaseContext, IJwtService jwtService, IPasswordService passwordService) + { + _logger = logger; + _databaseContext = databaseContext; + _jwtService = jwtService; + _passwordService = passwordService; + } - public User Authenticate(string username, string password) + public async Task<CreateTokenResult> CreateToken(string username, string password) { - return _users.FirstOrDefault(user => user.Username == username && user.Password == password); + var users = _databaseContext.Users.ToList(); + + var user = await _databaseContext.Users.Where(u => u.Name == username).SingleOrDefaultAsync(); + + if (user == null) + { + _logger.LogInformation($"Create token failed with invalid username. Username = {username} Password = {password} ."); + return null; + } + + var verifyResult = _passwordService.VerifyPassword(user.EncryptedPassword, password); + + if (verifyResult) + { + var userInfo = new UserInfo(user); + + return new CreateTokenResult + { + Token = _jwtService.GenerateJwtToken(user.Id, userInfo.Roles), + UserInfo = userInfo + }; + } + else + { + _logger.LogInformation($"Create token failed with invalid password. Username = {username} Password = {password} ."); + return null; + } + } + + public async Task<UserInfo> VerifyToken(string token) + { + var userId = _jwtService.VerifyJwtToken(token); + + if (userId == null) + { + _logger.LogInformation($"Verify token falied. Reason: invalid token. Token: {token} ."); + return null; + } + + var user = await _databaseContext.Users.Where(u => u.Id == userId.Value).SingleOrDefaultAsync(); + + if (user == null) + { + _logger.LogInformation($"Verify token falied. Reason: invalid user id. UserId: {userId} Token: {token} ."); + return null; + } + + return new UserInfo(user); + } + + public async Task<CreateUserResult> CreateUser(string username, string password, string[] roles) + { + var exists = (await _databaseContext.Users.Where(u => u.Name == username).ToListAsync()).Count != 0; + + if (exists) + { + return CreateUserResult.AlreadyExists; + } + + await _databaseContext.Users.AddAsync(new User { Name = username, EncryptedPassword = _passwordService.HashPassword(password), RoleString = string.Join(',', roles) }); + await _databaseContext.SaveChangesAsync(); + + return CreateUserResult.Success; } } } diff --git a/Timeline/Startup.cs b/Timeline/Startup.cs index 88348892..0c8d7052 100644 --- a/Timeline/Startup.cs +++ b/Timeline/Startup.cs @@ -1,26 +1,31 @@ +using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.HttpOverrides; using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.SpaServices.AngularCli; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.IdentityModel.Tokens; using System.Text; using Timeline.Configs; -using Timeline.Services; -using Microsoft.AspNetCore.HttpOverrides; using Timeline.Formatters; +using Timeline.Models; +using Timeline.Services; namespace Timeline { public class Startup { - public Startup(IConfiguration configuration) + private const string corsPolicyName = "MyPolicy"; + + public Startup(IConfiguration configuration, IHostingEnvironment environment) { + Environment = environment; Configuration = configuration; } + public IHostingEnvironment Environment { get; } public IConfiguration Configuration { get; } // This method gets called by the runtime. Use this method to add services to the container. @@ -31,14 +36,29 @@ namespace Timeline options.InputFormatters.Add(new StringInputFormatter()); }).SetCompatibilityVersion(CompatibilityVersion.Version_2_2); - // In production, the Angular files will be served from this directory - services.AddSpaStaticFiles(configuration => + if (Environment.IsDevelopment()) { - configuration.RootPath = "ClientApp/dist"; - }); + services.AddCors(options => + { + options.AddPolicy(corsPolicyName, builder => + { + builder.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader().AllowCredentials(); + }); + }); + } + else + { + services.AddCors(options => + { + options.AddPolicy(corsPolicyName, builder => + { + builder.WithOrigins("https://www.crupest.xyz", "https://crupest.xyz").AllowAnyMethod().AllowAnyHeader().AllowCredentials(); + }); + }); + } - services.Configure<JwtConfig>(Configuration.GetSection("JwtConfig")); - var jwtConfig = Configuration.GetSection("JwtConfig").Get<JwtConfig>(); + services.Configure<JwtConfig>(Configuration.GetSection(nameof(JwtConfig))); + var jwtConfig = Configuration.GetSection(nameof(JwtConfig)).Get<JwtConfig>(); services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(o => @@ -52,14 +72,22 @@ namespace Timeline o.TokenValidationParameters.IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(jwtConfig.SigningKey)); }); - services.AddSingleton<IUserService, UserService>(); - services.AddSingleton<IJwtService, JwtService>(); + services.AddScoped<IUserService, UserService>(); + services.AddScoped<IJwtService, JwtService>(); + services.AddTransient<IPasswordService, PasswordService>(); + + var databaseConfig = Configuration.GetSection(nameof(DatabaseConfig)).Get<DatabaseConfig>(); + + services.AddDbContext<DatabaseContext>(options => + { + options.UseMySql(databaseConfig.ConnectionString); + }); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - public void Configure(IApplicationBuilder app, IHostingEnvironment env) + public void Configure(IApplicationBuilder app) { - if (env.IsDevelopment()) + if (Environment.IsDevelopment()) { app.UseDeveloperExceptionPage(); } @@ -68,8 +96,7 @@ namespace Timeline app.UseExceptionHandler("/Error"); } - app.UseStaticFiles(); - app.UseSpaStaticFiles(); + app.UseCors(corsPolicyName); app.UseForwardedHeaders(new ForwardedHeadersOptions { @@ -84,16 +111,6 @@ namespace Timeline name: "default", template: "{controller}/{action=Index}/{id?}"); }); - - app.UseSpa(spa => - { - spa.Options.SourcePath = "ClientApp"; - - if (env.IsDevelopment()) - { - spa.UseAngularCliServer(npmScript: "start"); - } - }); } } } diff --git a/Timeline/Timeline-CI.csproj b/Timeline/Timeline-CI.csproj deleted file mode 100644 index 65bfacdf..00000000 --- a/Timeline/Timeline-CI.csproj +++ /dev/null @@ -1,35 +0,0 @@ -<Project Sdk="Microsoft.NET.Sdk.Web"> - - <PropertyGroup> - <TargetFramework>netcoreapp2.2</TargetFramework> - <TypeScriptCompileBlocked>true</TypeScriptCompileBlocked> - <TypeScriptToolsVersion>Latest</TypeScriptToolsVersion> - <IsPackable>false</IsPackable> - <SpaRoot>ClientApp\</SpaRoot> - <DefaultItemExcludes>$(DefaultItemExcludes);$(SpaRoot)node_modules\**</DefaultItemExcludes> - <Authors>crupest</Authors> - <AssemblyName>Timeline</AssemblyName> - </PropertyGroup> - - <ItemGroup> - <PackageReference Include="Microsoft.AspNetCore.App" /> - </ItemGroup> - - <ItemGroup> - <!-- Don't publish the SPA source files, but do show them in the project files list --> - <Content Remove="$(SpaRoot)**" /> - <None Remove="$(SpaRoot)**" /> - <None Include="$(SpaRoot)**" Exclude="$(SpaRoot)node_modules\**" /> - </ItemGroup> - - <Target Name="PublishRunWebpack" AfterTargets="ComputeFilesToPublish"> - <ItemGroup> - <DistFiles Include="$(SpaRoot)dist\**" /> - <ResolvedFileToPublish Include="@(DistFiles->'%(FullPath)')" Exclude="@(ResolvedFileToPublish)"> - <RelativePath>%(DistFiles.Identity)</RelativePath> - <CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory> - </ResolvedFileToPublish> - </ItemGroup> - </Target> - -</Project> diff --git a/Timeline/Timeline.csproj b/Timeline/Timeline.csproj index e55eb90d..93513bd3 100644 --- a/Timeline/Timeline.csproj +++ b/Timeline/Timeline.csproj @@ -1,57 +1,16 @@ <Project Sdk="Microsoft.NET.Sdk.Web"> + <PropertyGroup> + <TargetFramework>netcoreapp2.2</TargetFramework> + <IsPackable>false</IsPackable> + <UserSecretsId>1f6fb74d-4277-4bc0-aeea-b1fc5ffb0b43</UserSecretsId> + <Authors>crupest</Authors> + </PropertyGroup> - <PropertyGroup> - <TargetFramework>netcoreapp2.2</TargetFramework> - <TypeScriptCompileBlocked>true</TypeScriptCompileBlocked> - <TypeScriptToolsVersion>Latest</TypeScriptToolsVersion> - <IsPackable>false</IsPackable> - <SpaRoot>ClientApp\</SpaRoot> - <DefaultItemExcludes>$(DefaultItemExcludes);$(SpaRoot)node_modules\**</DefaultItemExcludes> - - <!-- Set this to true if you enable server-side prerendering --> - <BuildServerSideRenderer>false</BuildServerSideRenderer> - <UserSecretsId>1f6fb74d-4277-4bc0-aeea-b1fc5ffb0b43</UserSecretsId> - <Authors>crupest</Authors> - </PropertyGroup> - - <ItemGroup> - <PackageReference Include="Microsoft.AspNetCore.App" /> - <PackageReference Include="Microsoft.AspNetCore.Razor.Design" Version="2.2.0" PrivateAssets="All" /> - <PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="2.2.1" /> - </ItemGroup> - - <ItemGroup> - <!-- Don't publish the SPA source files, but do show them in the project files list --> - <Content Remove="$(SpaRoot)**" /> - <None Remove="$(SpaRoot)**" /> - <None Include="$(SpaRoot)**" Exclude="$(SpaRoot)node_modules\**" /> - </ItemGroup> - - <Target Name="DebugEnsureNodeEnv" BeforeTargets="Build" Condition=" '$(Configuration)' == 'Debug' And !Exists('$(SpaRoot)node_modules') "> - <!-- Ensure Node.js is installed --> - <Exec Command="node --version" ContinueOnError="true"> - <Output TaskParameter="ExitCode" PropertyName="ErrorCode" /> - </Exec> - <Error Condition="'$(ErrorCode)' != '0'" Text="Node.js is required to build and run this project. To continue, please install Node.js from https://nodejs.org/, and then restart your command prompt or IDE." /> - <Message Importance="high" Text="Restoring dependencies using 'yarn'. This may take several minutes..." /> - <Exec WorkingDirectory="$(SpaRoot)" Command="yarn install" /> - </Target> - - <Target Name="PublishRunWebpack" AfterTargets="ComputeFilesToPublish"> - <!-- As part of publishing, ensure the JS resources are freshly built in production mode --> - <Exec WorkingDirectory="$(SpaRoot)" Command="yarn install" /> - <Exec WorkingDirectory="$(SpaRoot)" Command="yarn run build --prod" /> - <Exec WorkingDirectory="$(SpaRoot)" Command="yarn run build:ssr --prod" Condition=" '$(BuildServerSideRenderer)' == 'true' " /> - - <!-- Include the newly-built files in the publish output --> <ItemGroup> - <DistFiles Include="$(SpaRoot)dist\**; $(SpaRoot)dist-server\**" /> - <DistFiles Include="$(SpaRoot)node_modules\**" Condition="'$(BuildServerSideRenderer)' == 'true'" /> - <ResolvedFileToPublish Include="@(DistFiles->'%(FullPath)')" Exclude="@(ResolvedFileToPublish)"> - <RelativePath>%(DistFiles.Identity)</RelativePath> - <CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory> - </ResolvedFileToPublish> + <PackageReference Include="Microsoft.AspNetCore.App" /> + <PackageReference Include="Microsoft.AspNetCore.Razor.Design" Version="2.2.0" PrivateAssets="All" /> + <PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="2.2.3" /> + <PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="2.2.0" /> + <PackageReference Include="Pomelo.EntityFrameworkCore.MySql.Design" Version="1.1.2" /> </ItemGroup> - </Target> - </Project> diff --git a/Timeline/appsettings.Test.json b/Timeline/appsettings.Test.json index b1cd5a3b..ea32348b 100644 --- a/Timeline/appsettings.Test.json +++ b/Timeline/appsettings.Test.json @@ -3,7 +3,9 @@ "LogLevel": { "Default": "Debug", "System": "Information", - "Microsoft": "Information" + "Microsoft": "Information", + "Microsoft.AspNetCore.Authentication": "Debug", + "Microsoft.AspNetCore.Authorization": "Debug" } }, "JwtConfig": { diff --git a/Timeline/appsettings.json b/Timeline/appsettings.json index 74d3da4e..81f83d68 100644 --- a/Timeline/appsettings.json +++ b/Timeline/appsettings.json @@ -5,8 +5,8 @@ } }, "JwtConfig": { - "Issuer": "crupest.xyz", - "Audience": "crupest.xyz" + "Issuer": "api.crupest.xyz", + "Audience": "api.crupest.xyz" }, "TodoPageConfig": { "GithubInfo": { diff --git a/Timeline/wwwroot/android-chrome-192x192.png b/Timeline/wwwroot/android-chrome-192x192.png Binary files differdeleted file mode 100644 index 5dff06b1..00000000 --- a/Timeline/wwwroot/android-chrome-192x192.png +++ /dev/null diff --git a/Timeline/wwwroot/android-chrome-512x512.png b/Timeline/wwwroot/android-chrome-512x512.png Binary files differdeleted file mode 100644 index ab68aace..00000000 --- a/Timeline/wwwroot/android-chrome-512x512.png +++ /dev/null diff --git a/Timeline/wwwroot/apple-touch-icon.png b/Timeline/wwwroot/apple-touch-icon.png Binary files differdeleted file mode 100644 index 1397e419..00000000 --- a/Timeline/wwwroot/apple-touch-icon.png +++ /dev/null diff --git a/Timeline/wwwroot/browserconfig.xml b/Timeline/wwwroot/browserconfig.xml deleted file mode 100644 index b3930d0f..00000000 --- a/Timeline/wwwroot/browserconfig.xml +++ /dev/null @@ -1,9 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<browserconfig> - <msapplication> - <tile> - <square150x150logo src="/mstile-150x150.png"/> - <TileColor>#da532c</TileColor> - </tile> - </msapplication> -</browserconfig> diff --git a/Timeline/wwwroot/favicon-16x16.png b/Timeline/wwwroot/favicon-16x16.png Binary files differdeleted file mode 100644 index 0cee9398..00000000 --- a/Timeline/wwwroot/favicon-16x16.png +++ /dev/null diff --git a/Timeline/wwwroot/favicon-32x32.png b/Timeline/wwwroot/favicon-32x32.png Binary files differdeleted file mode 100644 index 2e358474..00000000 --- a/Timeline/wwwroot/favicon-32x32.png +++ /dev/null diff --git a/Timeline/wwwroot/favicon.ico b/Timeline/wwwroot/favicon.ico Binary files differdeleted file mode 100644 index fba217fd..00000000 --- a/Timeline/wwwroot/favicon.ico +++ /dev/null diff --git a/Timeline/wwwroot/mstile-144x144.png b/Timeline/wwwroot/mstile-144x144.png Binary files differdeleted file mode 100644 index b111e9f7..00000000 --- a/Timeline/wwwroot/mstile-144x144.png +++ /dev/null diff --git a/Timeline/wwwroot/mstile-150x150.png b/Timeline/wwwroot/mstile-150x150.png Binary files differdeleted file mode 100644 index 50eb11aa..00000000 --- a/Timeline/wwwroot/mstile-150x150.png +++ /dev/null diff --git a/Timeline/wwwroot/mstile-310x150.png b/Timeline/wwwroot/mstile-310x150.png Binary files differdeleted file mode 100644 index b2de3715..00000000 --- a/Timeline/wwwroot/mstile-310x150.png +++ /dev/null diff --git a/Timeline/wwwroot/mstile-310x310.png b/Timeline/wwwroot/mstile-310x310.png Binary files differdeleted file mode 100644 index 011b7b7e..00000000 --- a/Timeline/wwwroot/mstile-310x310.png +++ /dev/null diff --git a/Timeline/wwwroot/mstile-70x70.png b/Timeline/wwwroot/mstile-70x70.png Binary files differdeleted file mode 100644 index bcc60d24..00000000 --- a/Timeline/wwwroot/mstile-70x70.png +++ /dev/null diff --git a/Timeline/wwwroot/safari-pinned-tab.svg b/Timeline/wwwroot/safari-pinned-tab.svg deleted file mode 100644 index 0716ee63..00000000 --- a/Timeline/wwwroot/safari-pinned-tab.svg +++ /dev/null @@ -1,30 +0,0 @@ -<?xml version="1.0" standalone="no"?> -<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN" - "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd"> -<svg version="1.0" xmlns="http://www.w3.org/2000/svg" - width="700.000000pt" height="700.000000pt" viewBox="0 0 700.000000 700.000000" - preserveAspectRatio="xMidYMid meet"> -<metadata> -Created by potrace 1.11, written by Peter Selinger 2001-2013 -</metadata> -<g transform="translate(0.000000,700.000000) scale(0.100000,-0.100000)" -fill="#000000" stroke="none"> -<path d="M2800 6277 l0 -722 -62 -24 c-185 -70 -379 -177 -538 -297 -93 -70 --107 -81 -200 -169 -50 -47 -177 -185 -199 -219 -8 -11 -18 -23 -22 -26 -4 -3 --42 -57 -84 -120 -63 -95 -174 -302 -206 -385 -59 -152 -111 -336 -125 -440 --3 -27 -8 -52 -9 -55 -21 -35 -26 -479 -6 -600 6 -41 14 -91 17 -110 22 -147 -114 -421 194 -579 107 -214 284 -454 438 -595 25 -22 55 -50 66 -61 48 -47 -208 -165 301 -222 111 -68 277 -150 373 -184 l62 -22 0 -723 0 -724 700 0 700 -0 0 724 0 723 43 17 c23 9 46 16 50 16 4 0 46 18 93 40 47 22 88 40 89 40 2 0 -55 30 117 67 508 301 862 770 1007 1333 16 63 32 133 35 155 9 65 18 119 23 -140 3 11 6 106 8 210 2 169 -1 232 -20 353 -3 17 -8 49 -11 70 -11 71 -65 268 --104 373 -117 320 -323 621 -585 855 -75 67 -267 214 -279 214 -3 0 -31 16 --62 36 -67 43 -254 133 -341 164 l-63 23 0 723 0 724 -700 0 -700 0 0 -723z -m920 -1467 c197 -38 348 -96 501 -195 242 -156 431 -394 528 -665 38 -108 49 --151 70 -285 13 -85 6 -315 -14 -415 -62 -313 -238 -602 -487 -796 -54 -42 --220 -149 -226 -145 -1 1 -15 -5 -32 -14 -73 -38 -239 -88 -365 -111 -72 -13 --298 -12 -385 2 -432 67 -801 331 -989 709 -77 154 -104 232 -132 385 -15 80 --20 296 -9 370 30 198 56 287 133 442 186 376 549 646 963 717 125 21 339 22 -444 1z"/> -</g> -</svg> diff --git a/Timeline/wwwroot/site.webmanifest b/Timeline/wwwroot/site.webmanifest deleted file mode 100644 index b20abb7c..00000000 --- a/Timeline/wwwroot/site.webmanifest +++ /dev/null @@ -1,19 +0,0 @@ -{ - "name": "", - "short_name": "", - "icons": [ - { - "src": "/android-chrome-192x192.png", - "sizes": "192x192", - "type": "image/png" - }, - { - "src": "/android-chrome-512x512.png", - "sizes": "512x512", - "type": "image/png" - } - ], - "theme_color": "#ffffff", - "background_color": "#ffffff", - "display": "standalone" -} diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 17087351..7d6bf4cb 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -7,46 +7,18 @@ trigger: - master variables: - ArtifactFeed: NodeModules buildConfiguration: 'Release' ASPNETCORE_ENVIRONMENT: 'Development' pool: vmImage: 'Ubuntu-16.04' steps: -- task: 1ESLighthouseEng.PipelineArtifactCaching.RestoreCacheV1.RestoreCache@1 - inputs: - keyfile: '**/yarn.lock, !**/node_modules/**/yarn.lock, !**/.*/**/yarn.lock' - targetfolder: '**/node_modules, !**/node_modules/**/node_modules' - vstsFeed: '$(ArtifactFeed)' - -- script: yarn install --non-interactive - condition: ne(variables['CacheRestored'], 'true') - workingDirectory: Timeline/ClientApp - displayName: Yarn Install - -- task: 1ESLighthouseEng.PipelineArtifactCaching.SaveCacheV1.SaveCache@1 - inputs: - keyfile: '**/yarn.lock, !**/node_modules/**/yarn.lock, !**/.*/**/yarn.lock' - targetfolder: '**/node_modules, !**/node_modules/**/node_modules' - vstsFeed: '$(ArtifactFeed)' - -- script: yarn run test --no-watch --browsers=ChromeHeadless --reporters junit - workingDirectory: Timeline/ClientApp - displayName: Angular Test - -- task: PublishTestResults@2 - condition: succeededOrFailed() - inputs: - testRunner: JUnit - testResultsFiles: '**/TESTS-*.xml' - - script: | - dotnet restore Timeline/Timeline-CI.csproj --configfile nuget.config - dotnet restore Timeline.Tests/Timeline.Tests-CI.csproj --configfile nuget.config + dotnet restore Timeline/Timeline.csproj --configfile nuget.config + dotnet restore Timeline.Tests/Timeline.Tests.csproj --configfile nuget.config displayName: Dotnet Restore -- script: dotnet test Timeline.Tests/Timeline.Tests-CI.csproj --configuration $(buildConfiguration) --no-restore --logger trx +- script: dotnet test Timeline.Tests/Timeline.Tests.csproj --configuration $(buildConfiguration) --no-restore --logger trx displayName: Dotnet Test - task: PublishTestResults@2 @@ -55,11 +27,7 @@ steps: testRunner: VSTest testResultsFiles: '**/*.trx' -- script: yarn build --prod - workingDirectory: Timeline/ClientApp - displayName: Client App Build - -- script: dotnet publish Timeline/Timeline-CI.csproj --configuration $(buildConfiguration) --no-restore --output ./publish/ +- script: dotnet publish Timeline/Timeline.csproj --configuration $(buildConfiguration) --no-restore --output ./publish/ displayName: Dotnet Publish - task: PublishPipelineArtifact@0 |