aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author杨宇千 <crupest@outlook.com>2019-04-13 15:47:40 +0800
committerGitHub <noreply@github.com>2019-04-13 15:47:40 +0800
commit56c51bdf844ce1e3642dcdc4099187e7e57008c7 (patch)
tree1aa37565aad734b604eb94ed3a62db2308f4b30d
parent19cae15eba2bcede41b818e1b8ab7fd5ac92eb05 (diff)
parent108ea333534445a8c76d4db632ebf21abf426c71 (diff)
downloadtimeline-56c51bdf844ce1e3642dcdc4099187e7e57008c7.tar.gz
timeline-56c51bdf844ce1e3642dcdc4099187e7e57008c7.tar.bz2
timeline-56c51bdf844ce1e3642dcdc4099187e7e57008c7.zip
Merge pull request #20 from crupest/separate
Separate front end and back end.
-rw-r--r--Timeline.Tests/AuthorizationUnitTest.cs6
-rw-r--r--Timeline.Tests/Helpers/Authentication/AuthenticationHttpClientExtensions.cs2
-rw-r--r--Timeline.Tests/Helpers/WebApplicationFactoryExtensions.cs50
-rw-r--r--Timeline.Tests/JwtTokenUnitTest.cs4
-rw-r--r--Timeline.Tests/Timeline.Tests-CI.csproj27
-rw-r--r--Timeline.Tests/Timeline.Tests.csproj5
-rw-r--r--Timeline/ClientApp/.editorconfig13
-rw-r--r--Timeline/ClientApp/.gitignore40
-rw-r--r--Timeline/ClientApp/.prettierrc.json4
-rw-r--r--Timeline/ClientApp/.vscode/launch.json15
-rw-r--r--Timeline/ClientApp/README.md27
-rw-r--r--Timeline/ClientApp/angular.json151
-rw-r--r--Timeline/ClientApp/e2e/protractor.conf.js28
-rw-r--r--Timeline/ClientApp/e2e/src/app.e2e-spec.ts14
-rw-r--r--Timeline/ClientApp/e2e/src/app.po.ts11
-rw-r--r--Timeline/ClientApp/e2e/tsconfig.e2e.json13
-rw-r--r--Timeline/ClientApp/package.json59
-rw-r--r--Timeline/ClientApp/src/app/app.component.css10
-rw-r--r--Timeline/ClientApp/src/app/app.component.html20
-rw-r--r--Timeline/ClientApp/src/app/app.component.ts13
-rw-r--r--Timeline/ClientApp/src/app/app.module.ts29
-rw-r--r--Timeline/ClientApp/src/app/app.server.module.ts11
-rw-r--r--Timeline/ClientApp/src/app/home/home.component.css49
-rw-r--r--Timeline/ClientApp/src/app/home/home.component.html3
-rw-r--r--Timeline/ClientApp/src/app/home/home.component.spec.ts26
-rw-r--r--Timeline/ClientApp/src/app/home/home.component.ts10
-rw-r--r--Timeline/ClientApp/src/app/home/home.module.ts17
-rw-r--r--Timeline/ClientApp/src/app/test-utilities/activated-route.mock.ts68
-rw-r--r--Timeline/ClientApp/src/app/test-utilities/mock.ts7
-rw-r--r--Timeline/ClientApp/src/app/test-utilities/router-link.mock.ts9
-rw-r--r--Timeline/ClientApp/src/app/test-utilities/storage.mock.ts28
-rw-r--r--Timeline/ClientApp/src/app/todo/todo-item.ts6
-rw-r--r--Timeline/ClientApp/src/app/todo/todo-item/todo-item.component.css25
-rw-r--r--Timeline/ClientApp/src/app/todo/todo-item/todo-item.component.html9
-rw-r--r--Timeline/ClientApp/src/app/todo/todo-item/todo-item.component.spec.ts46
-rw-r--r--Timeline/ClientApp/src/app/todo/todo-item/todo-item.component.ts12
-rw-r--r--Timeline/ClientApp/src/app/todo/todo-list-color-block.css7
-rw-r--r--Timeline/ClientApp/src/app/todo/todo-page/todo-page.component.css39
-rw-r--r--Timeline/ClientApp/src/app/todo/todo-page/todo-page.component.html21
-rw-r--r--Timeline/ClientApp/src/app/todo/todo-page/todo-page.component.spec.ts78
-rw-r--r--Timeline/ClientApp/src/app/todo/todo-page/todo-page.component.ts39
-rw-r--r--Timeline/ClientApp/src/app/todo/todo-service/http-entities.ts11
-rw-r--r--Timeline/ClientApp/src/app/todo/todo-service/todo.service.spec.ts54
-rw-r--r--Timeline/ClientApp/src/app/todo/todo-service/todo.service.ts33
-rw-r--r--Timeline/ClientApp/src/app/todo/todo.module.ts27
-rw-r--r--Timeline/ClientApp/src/app/user/auth.guard.spec.ts69
-rw-r--r--Timeline/ClientApp/src/app/user/auth.guard.ts80
-rw-r--r--Timeline/ClientApp/src/app/user/entities.ts9
-rw-r--r--Timeline/ClientApp/src/app/user/internal-user-service/errors.ts29
-rw-r--r--Timeline/ClientApp/src/app/user/internal-user-service/http-entities.ts21
-rw-r--r--Timeline/ClientApp/src/app/user/internal-user-service/internal-user.service.mock.ts5
-rw-r--r--Timeline/ClientApp/src/app/user/internal-user-service/internal-user.service.spec.ts123
-rw-r--r--Timeline/ClientApp/src/app/user/internal-user-service/internal-user.service.ts155
-rw-r--r--Timeline/ClientApp/src/app/user/redirect.component.ts15
-rw-r--r--Timeline/ClientApp/src/app/user/user-dialog/user-dialog.component.css0
-rw-r--r--Timeline/ClientApp/src/app/user/user-dialog/user-dialog.component.html1
-rw-r--r--Timeline/ClientApp/src/app/user/user-dialog/user-dialog.component.spec.ts43
-rw-r--r--Timeline/ClientApp/src/app/user/user-dialog/user-dialog.component.ts24
-rw-r--r--Timeline/ClientApp/src/app/user/user-login-success/user-login-success.component.css22
-rw-r--r--Timeline/ClientApp/src/app/user/user-login-success/user-login-success.component.html6
-rw-r--r--Timeline/ClientApp/src/app/user/user-login-success/user-login-success.component.spec.ts75
-rw-r--r--Timeline/ClientApp/src/app/user/user-login-success/user-login-success.component.ts25
-rw-r--r--Timeline/ClientApp/src/app/user/user-login/user-login.component.css24
-rw-r--r--Timeline/ClientApp/src/app/user/user-login/user-login.component.html19
-rw-r--r--Timeline/ClientApp/src/app/user/user-login/user-login.component.spec.ts123
-rw-r--r--Timeline/ClientApp/src/app/user/user-login/user-login.component.ts38
-rw-r--r--Timeline/ClientApp/src/app/user/user-logout/user-logout.component.css0
-rw-r--r--Timeline/ClientApp/src/app/user/user-logout/user-logout.component.html1
-rw-r--r--Timeline/ClientApp/src/app/user/user-logout/user-logout.component.spec.ts35
-rw-r--r--Timeline/ClientApp/src/app/user/user-logout/user-logout.component.ts16
-rw-r--r--Timeline/ClientApp/src/app/user/user.module.ts38
-rw-r--r--Timeline/ClientApp/src/app/user/user.service.ts55
-rw-r--r--Timeline/ClientApp/src/app/user/window-inject-token.ts3
-rw-r--r--Timeline/ClientApp/src/app/utilities/debounce-click.directive.spec.ts123
-rw-r--r--Timeline/ClientApp/src/app/utilities/debounce-click.directive.ts41
-rw-r--r--Timeline/ClientApp/src/app/utilities/language-untilities.ts18
-rw-r--r--Timeline/ClientApp/src/app/utilities/utility.module.ts11
-rw-r--r--Timeline/ClientApp/src/assets/.gitkeep0
-rw-r--r--Timeline/ClientApp/src/assets/icon.svg7
-rw-r--r--Timeline/ClientApp/src/browserslist9
-rw-r--r--Timeline/ClientApp/src/environments/environment.prod.ts3
-rw-r--r--Timeline/ClientApp/src/environments/environment.ts15
-rw-r--r--Timeline/ClientApp/src/index.html17
-rw-r--r--Timeline/ClientApp/src/karma.conf.js32
-rw-r--r--Timeline/ClientApp/src/main.ts20
-rw-r--r--Timeline/ClientApp/src/polyfills.ts80
-rw-r--r--Timeline/ClientApp/src/styles.css14
-rw-r--r--Timeline/ClientApp/src/test.ts22
-rw-r--r--Timeline/ClientApp/src/tsconfig.app.json15
-rw-r--r--Timeline/ClientApp/src/tsconfig.server.json9
-rw-r--r--Timeline/ClientApp/src/tsconfig.spec.json22
-rw-r--r--Timeline/ClientApp/src/tslint.json17
-rw-r--r--Timeline/ClientApp/tsconfig.json25
-rw-r--r--Timeline/ClientApp/tslint.json130
-rw-r--r--Timeline/Configs/DatabaseConfig.cs12
-rw-r--r--Timeline/Controllers/UserController.cs54
-rw-r--r--Timeline/Controllers/UserTestController.cs6
-rw-r--r--Timeline/Entities/Token.cs26
-rw-r--r--Timeline/Entities/User.cs44
-rw-r--r--Timeline/Entities/UserInfo.cs26
-rw-r--r--Timeline/Migrations/20190412102517_InitCreate.Designer.cs43
-rw-r--r--Timeline/Migrations/20190412102517_InitCreate.cs32
-rw-r--r--Timeline/Migrations/20190412144150_AddAdminUser.Designer.cs43
-rw-r--r--Timeline/Migrations/20190412144150_AddAdminUser.cs19
-rw-r--r--Timeline/Migrations/20190412153003_MakeColumnsInUserNotNull.Designer.cs46
-rw-r--r--Timeline/Migrations/20190412153003_MakeColumnsInUserNotNull.cs52
-rw-r--r--Timeline/Migrations/DatabaseContextModelSnapshot.cs44
-rw-r--r--Timeline/Models/DatabaseContext.cs33
-rw-r--r--Timeline/Properties/launchSettings.json2
-rw-r--r--Timeline/Services/JwtService.cs53
-rw-r--r--Timeline/Services/PasswordService.cs205
-rw-r--r--Timeline/Services/UserService.cs115
-rw-r--r--Timeline/Startup.cs71
-rw-r--r--Timeline/Timeline-CI.csproj35
-rw-r--r--Timeline/Timeline.csproj63
-rw-r--r--Timeline/appsettings.Test.json4
-rw-r--r--Timeline/appsettings.json4
-rw-r--r--Timeline/wwwroot/android-chrome-192x192.pngbin1834 -> 0 bytes
-rw-r--r--Timeline/wwwroot/android-chrome-512x512.pngbin6153 -> 0 bytes
-rw-r--r--Timeline/wwwroot/apple-touch-icon.pngbin1557 -> 0 bytes
-rw-r--r--Timeline/wwwroot/browserconfig.xml9
-rw-r--r--Timeline/wwwroot/favicon-16x16.pngbin536 -> 0 bytes
-rw-r--r--Timeline/wwwroot/favicon-32x32.pngbin678 -> 0 bytes
-rw-r--r--Timeline/wwwroot/favicon.icobin15086 -> 0 bytes
-rw-r--r--Timeline/wwwroot/mstile-144x144.pngbin1371 -> 0 bytes
-rw-r--r--Timeline/wwwroot/mstile-150x150.pngbin1400 -> 0 bytes
-rw-r--r--Timeline/wwwroot/mstile-310x150.pngbin1593 -> 0 bytes
-rw-r--r--Timeline/wwwroot/mstile-310x310.pngbin3333 -> 0 bytes
-rw-r--r--Timeline/wwwroot/mstile-70x70.pngbin1049 -> 0 bytes
-rw-r--r--Timeline/wwwroot/safari-pinned-tab.svg30
-rw-r--r--Timeline/wwwroot/site.webmanifest19
-rw-r--r--azure-pipelines.yml40
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
deleted file mode 100644
index 5dff06b1..00000000
--- a/Timeline/wwwroot/android-chrome-192x192.png
+++ /dev/null
Binary files differ
diff --git a/Timeline/wwwroot/android-chrome-512x512.png b/Timeline/wwwroot/android-chrome-512x512.png
deleted file mode 100644
index ab68aace..00000000
--- a/Timeline/wwwroot/android-chrome-512x512.png
+++ /dev/null
Binary files differ
diff --git a/Timeline/wwwroot/apple-touch-icon.png b/Timeline/wwwroot/apple-touch-icon.png
deleted file mode 100644
index 1397e419..00000000
--- a/Timeline/wwwroot/apple-touch-icon.png
+++ /dev/null
Binary files differ
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
deleted file mode 100644
index 0cee9398..00000000
--- a/Timeline/wwwroot/favicon-16x16.png
+++ /dev/null
Binary files differ
diff --git a/Timeline/wwwroot/favicon-32x32.png b/Timeline/wwwroot/favicon-32x32.png
deleted file mode 100644
index 2e358474..00000000
--- a/Timeline/wwwroot/favicon-32x32.png
+++ /dev/null
Binary files differ
diff --git a/Timeline/wwwroot/favicon.ico b/Timeline/wwwroot/favicon.ico
deleted file mode 100644
index fba217fd..00000000
--- a/Timeline/wwwroot/favicon.ico
+++ /dev/null
Binary files differ
diff --git a/Timeline/wwwroot/mstile-144x144.png b/Timeline/wwwroot/mstile-144x144.png
deleted file mode 100644
index b111e9f7..00000000
--- a/Timeline/wwwroot/mstile-144x144.png
+++ /dev/null
Binary files differ
diff --git a/Timeline/wwwroot/mstile-150x150.png b/Timeline/wwwroot/mstile-150x150.png
deleted file mode 100644
index 50eb11aa..00000000
--- a/Timeline/wwwroot/mstile-150x150.png
+++ /dev/null
Binary files differ
diff --git a/Timeline/wwwroot/mstile-310x150.png b/Timeline/wwwroot/mstile-310x150.png
deleted file mode 100644
index b2de3715..00000000
--- a/Timeline/wwwroot/mstile-310x150.png
+++ /dev/null
Binary files differ
diff --git a/Timeline/wwwroot/mstile-310x310.png b/Timeline/wwwroot/mstile-310x310.png
deleted file mode 100644
index 011b7b7e..00000000
--- a/Timeline/wwwroot/mstile-310x310.png
+++ /dev/null
Binary files differ
diff --git a/Timeline/wwwroot/mstile-70x70.png b/Timeline/wwwroot/mstile-70x70.png
deleted file mode 100644
index bcc60d24..00000000
--- a/Timeline/wwwroot/mstile-70x70.png
+++ /dev/null
Binary files differ
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