├── .editorconfig ├── .github └── FUNDING.yml ├── .gitignore ├── .vscode ├── launch.json ├── settings.json └── tasks.json ├── LICENSE ├── README.md ├── api.sln ├── api.test ├── Controller │ └── Tests.cs ├── ControllerTests.cs ├── NuGet.Config ├── Unit │ └── Tests.cs └── api.test.csproj ├── api ├── .config │ └── dotnet-tools.json ├── Controllers │ ├── AuthController.cs │ └── ContactController.cs ├── Extensions │ └── StringExtension.cs ├── Middleware │ ├── SpaFallbackMiddleware.cs │ └── SpaFallbackOptions.cs ├── Migrations │ ├── 20171204210645_Initial.Designer.cs │ ├── 20171204210645_Initial.cs │ ├── 20200128145031_1580223026.Designer.cs │ ├── 20200128145031_1580223026.cs │ └── DefaultDbContextModelSnapshot.cs ├── Models │ ├── ApplicationUser.cs │ ├── Contact.cs │ ├── DefaultDbContext.cs │ ├── DefaultDbContextInitializer.cs │ └── DesignTimeDefaultDbContext.cs ├── NuGet.Config ├── Program.cs ├── Properties │ └── launchSettings.json ├── Services │ ├── EmailSender.cs │ ├── EmailSenderOptions.cs │ ├── IEmailSender.cs │ └── JwtOptions.cs ├── Startup.cs ├── ViewModels │ ├── ConfirmEmail.cs │ └── NewUser.cs ├── api.csproj ├── appsettings.json ├── log │ └── development-20170420.log └── wwwroot │ └── favicon.ico ├── client-react.test ├── build │ └── client-react │ │ └── styles ├── polyfill │ └── localStorage.js ├── tests.tsx ├── tests │ └── Contacts.tsx ├── tsconfig.json └── utils.ts ├── client-react ├── boot.tsx ├── components │ ├── Auth.tsx │ ├── ContactForm.tsx │ ├── Contacts.tsx │ ├── Error.tsx │ ├── Header.tsx │ └── Routes.tsx ├── index.ejs ├── polyfills │ ├── array-find.d.ts │ ├── array-find.ts │ ├── object-assign.d.ts │ └── object-assign.ts ├── services │ ├── Auth.ts │ ├── Contacts.ts │ └── RestUtilities.ts ├── stores │ └── Auth.ts ├── styles │ ├── auth.styl │ ├── contacts.styl │ ├── global.css │ └── styles.d.ts ├── tsconfig.json ├── webpack.config.js └── webpack.config.release.js ├── docker-compose.yml ├── global.json ├── ops ├── README.md ├── config.yml.example ├── deploy.yml ├── group_vars │ └── all ├── library │ └── ghetto_json ├── provision.yml └── roles │ ├── deploy │ ├── defaults │ │ └── main.yml │ ├── meta │ │ └── main.yml │ └── tasks │ │ └── main.yml │ ├── deploy_user │ ├── meta │ │ └── main.yml │ └── tasks │ │ └── main.yml │ ├── dotnetcore │ ├── defaults │ │ └── main.yml │ └── tasks │ │ └── main.yml │ ├── firewall │ ├── handlers │ │ └── main.yml │ └── tasks │ │ └── main.yml │ ├── nginx │ ├── defaults │ │ └── main.yml │ ├── handlers │ │ └── main.yml │ ├── tasks │ │ └── main.yml │ └── templates │ │ └── etc_nginx_sites-available.conf.j2 │ ├── postgresql │ ├── LICENSE │ ├── README.md │ ├── defaults │ │ └── main.yml │ ├── handlers │ │ └── main.yml │ ├── tasks │ │ ├── backup.yml │ │ ├── configure.yml │ │ ├── install.yml │ │ └── main.yml │ ├── templates │ │ └── pgsql_backup.sh.j2 │ └── vars │ │ └── main.yml │ ├── s3cmd │ ├── meta │ │ └── main.yml │ ├── tasks │ │ └── main.yml │ └── templates │ │ └── s3cfg.j2 │ ├── ssl │ ├── defaults │ │ └── main.yml │ ├── meta │ │ └── main.yml │ ├── tasks │ │ └── main.yml │ └── templates │ │ └── config.j2 │ └── supervisor │ ├── defaults │ └── main.yml │ ├── handlers │ └── main.yml │ ├── tasks │ └── main.yml │ └── templates │ └── etc_supervisor_conf.d_app_name.conf.j2 ├── package-lock.json ├── package.json └── scripts └── create-migration.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | charset = utf-8 6 | indent_style = space 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | indent_size = 2 10 | 11 | [*.{js,ts,tsx}] 12 | indent_size = 4 13 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [bradymholt] 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | # dotnet core 4 | bin/ 5 | obj/ 6 | project.lock.json 7 | NuGetScratch/ 8 | 9 | api/wwwroot/** 10 | !api/wwwroot/scratch.html 11 | !api/wwwroot/favicon.ico 12 | 13 | # node 14 | node_modules/ 15 | typings/ 16 | npm-debug.log 17 | 18 | # client-react 19 | client-react/components/*.js 20 | client-react.test/build 21 | !client-react.test/build/client-react/styles/ 22 | 23 | # ops 24 | ops/hosts 25 | ops/config.yml 26 | ops/*.retry 27 | 28 | # other 29 | *.js.map 30 | 31 | # IDE 32 | .idea/ 33 | .vs/ 34 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Debug api/ (server)", 6 | "type": "coreclr", 7 | "request": "launch", 8 | "preLaunchTask": "build", 9 | "program": "${workspaceRoot}/api/bin/Debug/netcoreapp3.1/api.dll", 10 | "args": [], 11 | "env": { 12 | "ASPNETCORE_ENVIRONMENT": "Development", 13 | "NODE_PATH": "../node_modules/" 14 | }, 15 | "cwd": "${workspaceRoot}/api", 16 | "externalConsole": false, 17 | "stopAtEntry": false, 18 | "internalConsoleOptions": "openOnSessionStart" 19 | }, 20 | { 21 | "name": "Debug client-react.test/ (Mocha tests)", 22 | "type": "node", 23 | "request": "launch", 24 | "protocol": "inspector", 25 | "program": "${workspaceRoot}/node_modules/mocha/bin/_mocha", 26 | "stopOnEntry": false, 27 | "args": [ 28 | "--require", 29 | "ignore-styles", 30 | "--recursive", 31 | "client-react.test/build/client-react.test" 32 | ], 33 | "cwd": "${workspaceRoot}", 34 | "preLaunchTask": "pretest:client", 35 | "runtimeArgs": [ 36 | "--nolazy" 37 | ], 38 | "env": { 39 | "NODE_ENV": "development" 40 | }, 41 | "sourceMaps": true 42 | }, 43 | { 44 | "name": ".NET Core Attach", 45 | "type": "coreclr", 46 | "request": "attach", 47 | "processId": "${command:pickProcess}" 48 | } 49 | ] 50 | } 51 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.exclude": { 3 | "**/.git": true, 4 | "**/.svn": true, 5 | "**/.hg": true, 6 | "**/CVS": true, 7 | "**/.DS_Store": true, 8 | "**/bin": true, 9 | "**/obj": true 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "0.1.0", 5 | "command": "npm", 6 | "isShellCommand": true, 7 | "showOutput": "always", 8 | "suppressTaskName": true, 9 | "tasks": [ 10 | { 11 | "taskName": "build", 12 | "args": [ 13 | "run", 14 | "build" 15 | ], 16 | "isBuildCommand": true 17 | }, 18 | { 19 | "taskName": "test", 20 | "args": [ 21 | "run", 22 | "test" 23 | ], 24 | "isTestCommand": true 25 | }, 26 | { 27 | "taskName": "pretest:client", 28 | "args":[ 29 | "run", 30 | "pretest:client" 31 | ] 32 | } 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Brady Holt 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ASP.NET Core / React SPA Template App 2 | 3 | 4 | 5 | 6 | 7 | **Would you take a quick second and ⭐️ my repo?** 8 | 9 | 10 | 11 | 12 | This app is a template application using ASP.NET Core 3.1 for a REST/JSON API server and React for a web client. 13 | 14 |  15 | 16 | ## Overview of Stack 17 | - Server 18 | - ASP.NET Core 3.1 19 | - PostgreSQL 10 20 | - Entity Framework Core w/ EF Migrations 21 | - JSON Web Token (JWT) authorization 22 | - Docker used for development PostgreSQL database and MailCatcher server 23 | - Client 24 | - React 16 25 | - Webpack for asset bundling and HMR (Hot Module Replacement) 26 | - CSS Modules 27 | - Fetch API for REST requests 28 | - Testing 29 | - xUnit for .NET Core 30 | - Enzyme for React 31 | - MailCatcher for development email delivery 32 | - DevOps 33 | - Ansible playbook for provisioning (Nginx reverse proxy, SSL via Let's Encrypt, PostgreSQL backups to S3) 34 | - Ansible playbook for deployment 35 | 36 | ## Demo 37 | 38 | [](https://www.youtube.com/watch?v=xh5plRGg3Nc) 39 | 40 | ## Setup 41 | 42 | 1. Install the following: 43 | - [.NET Core 3.1](https://www.microsoft.com/net/core) 44 | - [Node.js >= v8](https://nodejs.org/en/download/) 45 | - [Ansible >= 2.6](http://docs.ansible.com/ansible/intro_installation.html) 46 | - [Docker](https://docs.docker.com/engine/installation/) 47 | 2. Run `npm install && npm start` 48 | 3. Open browser and navigate to [http://localhost:5000](http://localhost:5000). 49 | 50 | This template was developed and tested on macOS Sierra but should run on Windows (for development) as well. If you experience any issues getting it to run on Windows and work through them, please submit a PR! The production provisioning and deployment scripts (`provision:prod` and `deploy:prod`) use Ansible and require a Linux/Ubuntu >= 16.04 target host. 51 | 52 | ## Scripts 53 | 54 | ### `npm install` 55 | 56 | When first cloning the repo or adding new dependencies, run this command. This will: 57 | 58 | - Install Node dependencies from package.json 59 | - Install .NET Core dependencies from api/api.csproj and api.test/api.test.csproj (using dotnet restore) 60 | 61 | ### `npm start` 62 | 63 | To start the app for development, run this command. This will: 64 | 65 | - Run `docker-compose up` to ensure the PostgreSQL and MailCatcher Docker images are up and running 66 | - Run `dotnet watch run` which will build the app (if changed), watch for changes and start the web server on http://localhost:5000 67 | - Run Webpack dev middleware with HMR via [ASP.NET JavaScriptServices](https://github.com/aspnet/JavaScriptServices) 68 | 69 | ### `npm run migrate` 70 | 71 | After making changes to Entity Framework models in `api/Models/`, run this command to generate and run a migration on the database. A timestamp will be used for the migration name. 72 | 73 | ### `npm test` 74 | 75 | This will run the xUnit tests in api.test/ and the Mocha/Enzyme tests in client-react.test/. 76 | 77 | ### `npm run provision:prod` 78 | 79 | _Before running this script, you need to create an ops/config.yml file first. See the [ops README](ops/) for instructions._ 80 | 81 | This will run the ops/provision.yml Ansible playbook and provision hosts in ops/hosts inventory file. Ubuntu 16.04 (Xenial) and Ubuntu 18.04 (Bionic) is supported and tested. 82 | 83 | This prepares the hosts to recieve deployments by doing the following: 84 | - Install Nginx 85 | - Generate a SSL certificate from [Let's Encrypt](https://letsencrypt.org/) and configure Nginx to use it 86 | - Install .Net Core 87 | - Install Supervisor (will run/manage the ASP.NET app) 88 | - Install PostgreSQL 89 | - Setup a cron job to automatically backup the PostgreSQL database, compress it, and upload it to S3. 90 | - Setup UFW (firewall) to lock everything down except inbound SSH and web traffic 91 | - Create a deploy user, directory for deployments and configure Nginx to serve from this directory 92 | 93 | ### `npm run deploy:prod` 94 | 95 | _Before running this script, you need to create a ops/config.yml file first. See the [ops README](ops/) for instructions._ 96 | 97 | This script will: 98 | - Build release Webpack bundles 99 | - Package the .NET Core application in Release mode (dotnet publish) 100 | - Run the ops/deploy.yml Ansible playbook to deploy this app to hosts in /ops/config.yml inventory file. 101 | 102 | This does the following: 103 | - Copies the build assets to the remote host(s) 104 | - Updates the `appsettings.json` file with PostgreSQL credentials specified in ops/group_vars/all file and the app URL (needed for JWT tokens) 105 | - Restarts the app so that changes will be picked up 106 | 107 | Entity Framework Migrations are [automatically applied upon startup](https://github.com/bradymholt/aspnet-core-react-template/blob/master/api/Program.cs#L23-L24) so they will run when the app restarts. 108 | 109 | ## Development Email Delivery 110 | 111 | This template includes a [MailCatcher](https://mailcatcher.me/) Docker image so that when email is sent during development (i.e. new user registration), it can be viewed 112 | in the MailCacher web interface at [http://localhost:1080/](http://localhost:1080/). 113 | 114 | ## Older Versions 115 | 116 | This template was originally created on .NET Core 1.0 and has been upgraded with new versions of .NET Core. Older versions can be found on the [Releases](https://github.com/bradymholt/aspnet-core-react-template/releases) page. 117 | 118 | ## Visual Studio Code config 119 | 120 | This project has [Visual Studio Code](https://code.visualstudio.com/) tasks and debugger launch config located in .vscode/. 121 | 122 | ### Tasks 123 | 124 | - **Command+Shift+B** - Runs the "build" task which builds the api/ project 125 | - **Command+Shift+T** - Runs the "test" task which runs the xUnit tests in api.test/ and Mocha/Enzyme tests in client-react.test/. 126 | 127 | ### Debug Launcher 128 | 129 | With the following debugger launch configs, you can set breakpoints in api/ or the the Mocha tests in client-react.test/ and have full debugging support. 130 | 131 | - **Debug api/ (server)** - Runs the vscode debugger (breakpoints) on the api/ .NET Core app 132 | - **Debug client-react.test/ (Mocha tests)** - Runs the vscode debugger on the client-react.test/ Mocha tests 133 | 134 | ## Credit 135 | 136 | The following resources were helpful in setting up this template: 137 | 138 | - [Sample for implementing Authentication with a React Flux app and JWTs](https://github.com/auth0-blog/react-flux-jwt-authentication-sample) 139 | - [Angular 2, React, and Knockout apps on ASP.NET Core](http://blog.stevensanderson.com/2016/05/02/angular2-react-knockout-apps-on-aspnet-core/) 140 | - [Setting up ASP.NET v5 (vNext) to use JWT tokens (using OpenIddict)](http://capesean.co.za/blog/asp-net-5-jwt-tokens/) 141 | - [Cross-platform Single Page Applications with ASP.NET Core 1.0, Angular 2 & TypeScript](https://chsakell.com/2016/01/01/cross-platform-single-page-applications-with-asp-net-5-angular-2-typescript/) 142 | - [Stack Overflow - Token Based Authentication in ASP.NET Core](http://stackoverflow.com/questions/30546542/token-based-authentication-in-asp-net-core-refreshed) 143 | - [SPA example of a token based authentication implementation using the Aurelia front end framework and ASP.NET core]( https://github.com/alexandre-spieser/AureliaAspNetCoreAuth) 144 | - [A Real-World React.js Setup for ASP.NET Core and MVC5](https://www.simple-talk.com/dotnet/asp-net/a-real-world-react-js-setup-for-asp-net-core-and-mvc) 145 | - [Customising ASP.NET Core Identity EF Core naming conventions for PostgreSQL](https://andrewlock.net/customising-asp-net-core-identity-ef-core-naming-conventions-for-postgresql) 146 | - My own perseverance because this took a _lot_ of time to get right 🤓 147 | -------------------------------------------------------------------------------- /api.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "api", "api\api.csproj", "{62842846-868D-4B6E-A191-C37121587533}" 4 | EndProject 5 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "api.test", "api.test\api.test.csproj", "{002B4B4F-3A5D-4342-98AB-C50F7147F863}" 6 | EndProject 7 | Global 8 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 9 | Debug|Any CPU = Debug|Any CPU 10 | Release|Any CPU = Release|Any CPU 11 | EndGlobalSection 12 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 13 | {62842846-868D-4B6E-A191-C37121587533}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 14 | {62842846-868D-4B6E-A191-C37121587533}.Debug|Any CPU.Build.0 = Debug|Any CPU 15 | {62842846-868D-4B6E-A191-C37121587533}.Release|Any CPU.ActiveCfg = Release|Any CPU 16 | {62842846-868D-4B6E-A191-C37121587533}.Release|Any CPU.Build.0 = Release|Any CPU 17 | {002B4B4F-3A5D-4342-98AB-C50F7147F863}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 18 | {002B4B4F-3A5D-4342-98AB-C50F7147F863}.Debug|Any CPU.Build.0 = Debug|Any CPU 19 | {002B4B4F-3A5D-4342-98AB-C50F7147F863}.Release|Any CPU.ActiveCfg = Release|Any CPU 20 | {002B4B4F-3A5D-4342-98AB-C50F7147F863}.Release|Any CPU.Build.0 = Release|Any CPU 21 | EndGlobalSection 22 | EndGlobal 23 | -------------------------------------------------------------------------------- /api.test/Controller/Tests.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using app = aspnetCoreReactTemplate; 4 | 5 | namespace Tests.Controller 6 | { 7 | public class Tests 8 | { 9 | // [Fact] 10 | // public void Index_ReturnsAViewResult_WithAListOfBrainstormSessions() 11 | // { 12 | // var controller = new app.Controllers.ContactsController(null); 13 | // var result = (IEnumerable)controller.Get(); 14 | 15 | // Assert.NotEqual(result.Count(), 0); 16 | // } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /api.test/ControllerTests.cs: -------------------------------------------------------------------------------- 1 | using Xunit; 2 | 3 | namespace Tests 4 | { 5 | public class ControllerTests 6 | { 7 | [Fact] 8 | public void Test1() 9 | { 10 | var contact = new aspnetCoreReactTemplate.Models.Contact(); 11 | Assert.True(string.IsNullOrEmpty(contact.Email)); 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /api.test/NuGet.Config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /api.test/Unit/Tests.cs: -------------------------------------------------------------------------------- 1 | using Xunit; 2 | using app = aspnetCoreReactTemplate; 3 | 4 | namespace Tests.Unit 5 | { 6 | public class Tests 7 | { 8 | [Fact] 9 | public void TestNewContactProperties() 10 | { 11 | var contact = new app.Models.Contact(); 12 | 13 | Assert.True(string.IsNullOrEmpty(contact.LastName)); 14 | Assert.True(string.IsNullOrEmpty(contact.FirstName)); 15 | Assert.True(string.IsNullOrEmpty(contact.Email)); 16 | Assert.True(string.IsNullOrEmpty(contact.Phone)); 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /api.test/api.test.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net5.0 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | runtime; build; native; contentfiles; analyzers; buildtransitive 13 | all 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /api/.config/dotnet-tools.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "isRoot": true, 4 | "tools": { 5 | "dotnet-ef": { 6 | "version": "3.1.1", 7 | "commands": [ 8 | "dotnet-ef" 9 | ] 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /api/Controllers/AuthController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Authorization; 6 | using Microsoft.AspNetCore.Identity; 7 | using Microsoft.AspNetCore.Mvc; 8 | using Microsoft.Extensions.Logging; 9 | using Microsoft.Extensions.Options; 10 | using aspnetCoreReactTemplate.Models; 11 | using aspnetCoreReactTemplate.Services; 12 | using aspnetCoreReactTemplate.ViewModels; 13 | using System.Security.Claims; 14 | using System.IdentityModel.Tokens.Jwt; 15 | using Microsoft.IdentityModel.Tokens; 16 | 17 | namespace aspnetCoreReactTemplate.Controllers 18 | { 19 | public class AuthController : Controller 20 | { 21 | private readonly UserManager _userManager; 22 | private readonly IOptions _identityOptions; 23 | private readonly JwtOptions _jwtOptions; 24 | private readonly IEmailSender _emailSender; 25 | private readonly SignInManager _signInManager; 26 | private readonly ILogger _logger; 27 | 28 | public AuthController( 29 | UserManager userManager, 30 | IOptions identityOptions, 31 | IOptions jwtOptions, 32 | IEmailSender emailSender, 33 | SignInManager signInManager, 34 | ILoggerFactory loggerFactory) 35 | { 36 | _userManager = userManager; 37 | _identityOptions = identityOptions; 38 | _jwtOptions = jwtOptions.Value; 39 | _emailSender = emailSender; 40 | _signInManager = signInManager; 41 | _logger = loggerFactory.CreateLogger(); 42 | } 43 | 44 | [AllowAnonymous] 45 | [HttpPost("~/api/auth/login")] 46 | [Produces("application/json")] 47 | public async Task Login(string username, string password) 48 | { 49 | // Ensure the username and password is valid. 50 | var user = await _userManager.FindByNameAsync(username); 51 | if (user == null || !await _userManager.CheckPasswordAsync(user, password)) 52 | { 53 | return BadRequest(new 54 | { 55 | error = "", //OpenIdConnectConstants.Errors.InvalidGrant, 56 | error_description = "The username or password is invalid." 57 | }); 58 | } 59 | 60 | // Ensure the email is confirmed. 61 | if (!await _userManager.IsEmailConfirmedAsync(user)) 62 | { 63 | return BadRequest(new 64 | { 65 | error = "email_not_confirmed", 66 | error_description = "You must have a confirmed email to log in." 67 | }); 68 | } 69 | 70 | _logger.LogInformation($"User logged in (id: {user.Id})"); 71 | 72 | // Generate and issue a JWT token 73 | var claims = new [] { 74 | new Claim(ClaimTypes.Name, user.Email), 75 | new Claim(JwtRegisteredClaimNames.Sub, user.Email), 76 | new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()) 77 | }; 78 | 79 | var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_jwtOptions.key)); 80 | var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); 81 | 82 | var token = new JwtSecurityToken( 83 | issuer: _jwtOptions.issuer, 84 | audience: _jwtOptions.issuer, 85 | claims: claims, 86 | expires: DateTime.Now.AddMinutes(30), 87 | signingCredentials: creds); 88 | 89 | return Ok(new { token = new JwtSecurityTokenHandler().WriteToken(token) }); 90 | } 91 | 92 | [AllowAnonymous] 93 | [HttpPost("~/api/auth/register")] 94 | public async Task Register(NewUser model) 95 | { 96 | if (!ModelState.IsValid) 97 | { 98 | return BadRequest(ModelState); 99 | } 100 | 101 | var user = new ApplicationUser { UserName = model.username, Email = model.username }; 102 | var result = await _userManager.CreateAsync(user, model.password); 103 | if (result.Succeeded) 104 | { 105 | _logger.LogInformation($"New user registered (id: {user.Id})"); 106 | 107 | if (!user.EmailConfirmed) 108 | { 109 | // Send email confirmation email 110 | var confirmToken = await _userManager.GenerateEmailConfirmationTokenAsync(user); 111 | var emailConfirmUrl = Url.RouteUrl("ConfirmEmail", new { uid = user.Id, token = confirmToken }, this.Request.Scheme); 112 | await _emailSender.SendEmailAsync(model.username, "Please confirm your account", 113 | $"Please confirm your account by clicking this link." 114 | ); 115 | 116 | _logger.LogInformation($"Sent email confirmation email (id: {user.Id})"); 117 | } 118 | 119 | // Create a new authentication ticket. 120 | //var ticket = await CreateTicket(user); 121 | 122 | _logger.LogInformation($"User logged in (id: {user.Id})"); 123 | 124 | return Ok(); 125 | } 126 | else 127 | { 128 | return BadRequest(new { general = result.Errors.Select(x => x.Description) }); 129 | } 130 | } 131 | 132 | [AllowAnonymous] 133 | [HttpGet("~/api/auth/confirm", Name = "ConfirmEmail")] 134 | public async Task Confirm(string uid, string token) 135 | { 136 | var user = await _userManager.FindByIdAsync(uid); 137 | var confirmResult = await _userManager.ConfirmEmailAsync(user, token); 138 | if (confirmResult.Succeeded) 139 | { 140 | return Redirect("/?confirmed=1"); 141 | } 142 | else 143 | { 144 | return Redirect("/error/email-confirm"); 145 | } 146 | } 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /api/Controllers/ContactController.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.Collections.Generic; 3 | using Microsoft.AspNetCore.Authorization; 4 | using Microsoft.AspNetCore.Mvc; 5 | using aspnetCoreReactTemplate.Models; 6 | using Microsoft.EntityFrameworkCore; 7 | using System.Threading.Tasks; 8 | 9 | namespace aspnetCoreReactTemplate.Controllers 10 | { 11 | [Authorize] 12 | [Route("api/[controller]")] 13 | public class ContactsController : Controller 14 | { 15 | private readonly DefaultDbContext _context; 16 | 17 | public ContactsController(DefaultDbContext context) 18 | { 19 | _context = context; 20 | } 21 | 22 | // GET api/contacts 23 | [HttpGet] 24 | public IEnumerable Get() 25 | { 26 | return _context.Contacts.OrderBy((o)=> o.LastName); 27 | } 28 | 29 | // GET api/contacts/5 30 | [HttpGet("{id}", Name = "GetContact")] 31 | public Contact Get(int id) 32 | { 33 | return _context.Contacts.Find(id); 34 | } 35 | 36 | // GET api/contacts/?= 37 | [HttpGet("search")] 38 | public IEnumerable Search(string q) 39 | { 40 | return _context.Contacts. 41 | Where((c)=> c.LastName.ToLower().Contains(q.ToLower()) || c.FirstName.ToLower().Contains(q.ToLower())). 42 | OrderBy((o) => o.LastName); 43 | } 44 | 45 | // POST api/contacts 46 | [HttpPost] 47 | public async Task Post([FromBody]Contact model) 48 | { 49 | if (!ModelState.IsValid) 50 | { 51 | return BadRequest(ModelState); 52 | } 53 | 54 | _context.Contacts.Add(model); 55 | await _context.SaveChangesAsync(); 56 | return CreatedAtRoute("GetContact", new { id = model.Id }, model); 57 | } 58 | 59 | // PUT api/contacts/5 60 | [HttpPut("{id}")] 61 | public async Task Put(int id, [FromBody]Contact model) 62 | { 63 | if (!ModelState.IsValid) 64 | { 65 | return BadRequest(ModelState); 66 | } 67 | 68 | model.Id = id; 69 | _context.Update(model); 70 | await _context.SaveChangesAsync(); 71 | return Ok(); 72 | } 73 | 74 | // DELETE api/contacts/5 75 | [HttpDelete("{id}")] 76 | public async Task Delete(int id) 77 | { 78 | var contact = new Contact() { Id = id }; 79 | _context.Entry(contact).State = EntityState.Deleted; 80 | 81 | await _context.SaveChangesAsync(); 82 | return Ok(); 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /api/Extensions/StringExtension.cs: -------------------------------------------------------------------------------- 1 | using System.Text.RegularExpressions; 2 | 3 | namespace aspnetCoreReactTemplate.Extensions 4 | { 5 | public static class StringExtensions 6 | { 7 | public static string ToSnakeCase(this string input) 8 | { 9 | if (string.IsNullOrEmpty(input)) 10 | { 11 | return input; 12 | } 13 | 14 | var startUnderscores = Regex.Match(input, @"^_+"); 15 | return startUnderscores + Regex.Replace(input, @"([a-z0-9])([A-Z])", "$1_$2").ToLower(); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /api/Middleware/SpaFallbackMiddleware.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Microsoft.AspNetCore.Builder; 3 | using Microsoft.AspNetCore.Http; 4 | using Microsoft.Extensions.Logging; 5 | 6 | namespace aspnetCoreReactTemplate 7 | { 8 | /* 9 | Middleware that will rewrite (not redirect!) nested SPA page requests to the SPA root path. 10 | For SPA apps that are using client-side routing, a refresh or direct request for a nested path will 11 | be received by the server but the root path page should be actually be served because the client 12 | is responsible for routing, not the server. Only those requests not prefixed with 13 | options.ApiPathPrefix and not containing a path extention (i.e. image.png, scripts.js) will 14 | be rewritten. 15 | 16 | (SpaFallbackOptions options): 17 | ApiPathPrefix - The api path prefix is what requests for the REST api begin with. These 18 | will be ignored and not rewritten. So, if this is supplied as 'api', 19 | any requests starting with 'api' will not be rewritten. 20 | RewritePath - What path to rewrite to (usually '/') 21 | 22 | Examples: 23 | (options.ApiPathPrefix == "api", options.RewritePath="/") 24 | http://localhost:5000/api/auth/login => (no rewrite) 25 | http://localhost:5000/style.css => (no rewrite) 26 | http://localhost:5000/contacts => / 27 | http://localhost:5000/contacts/5/edit => / 28 | */ 29 | 30 | 31 | public class SpaFallbackMiddleware 32 | { 33 | private readonly RequestDelegate _next; 34 | private readonly ILogger _logger; 35 | private SpaFallbackOptions _options; 36 | 37 | public SpaFallbackMiddleware(RequestDelegate next, ILoggerFactory loggerFactory, SpaFallbackOptions options) 38 | { 39 | _next = next; 40 | _logger = loggerFactory.CreateLogger(); 41 | _options = options; 42 | } 43 | 44 | public async Task Invoke(HttpContext context) 45 | { 46 | _logger.LogInformation("Handling request: " + context.Request.Path); 47 | 48 | // If request path starts with _apiPathPrefix and the path does not have an extension (i.e. .css, .js, .png) 49 | if (!context.Request.Path.Value.StartsWith(_options.ApiPathPrefix) && !context.Request.Path.Value.Contains(".")) 50 | { 51 | _logger.LogInformation($"Rewriting path: {context.Request.Path} > {_options.RewritePath}"); 52 | context.Request.Path = _options.RewritePath; 53 | } 54 | 55 | await _next.Invoke(context); 56 | _logger.LogInformation("Finished handling request."); 57 | } 58 | } 59 | 60 | public static class SpaFallbackExtensions 61 | { 62 | public static IApplicationBuilder UseSpaFallback(this IApplicationBuilder builder, SpaFallbackOptions options) 63 | { 64 | if (options == null) 65 | { 66 | options = new SpaFallbackOptions(); 67 | } 68 | return builder.UseMiddleware(options); 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /api/Middleware/SpaFallbackOptions.cs: -------------------------------------------------------------------------------- 1 | namespace aspnetCoreReactTemplate 2 | { 3 | public class SpaFallbackOptions 4 | { 5 | public SpaFallbackOptions() 6 | { 7 | this.ApiPathPrefix = "/api"; 8 | this.RewritePath = "/"; 9 | } 10 | public string ApiPathPrefix { get; set; } 11 | public string RewritePath { get; set; } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /api/Migrations/20171204210645_Initial.Designer.cs: -------------------------------------------------------------------------------- 1 | // 2 | using aspnetCoreReactTemplate.Models; 3 | using Microsoft.EntityFrameworkCore; 4 | using Microsoft.EntityFrameworkCore.Infrastructure; 5 | using Microsoft.EntityFrameworkCore.Metadata; 6 | using Microsoft.EntityFrameworkCore.Migrations; 7 | using Microsoft.EntityFrameworkCore.Storage; 8 | using Microsoft.EntityFrameworkCore.Storage.Internal; 9 | using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; 10 | using System; 11 | 12 | namespace api.Migrations 13 | { 14 | [DbContext(typeof(DefaultDbContext))] 15 | [Migration("20171204210645_Initial")] 16 | partial class Initial 17 | { 18 | protected override void BuildTargetModel(ModelBuilder modelBuilder) 19 | { 20 | #pragma warning disable 612, 618 21 | modelBuilder 22 | .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.SerialColumn) 23 | .HasAnnotation("ProductVersion", "2.0.0-rtm-26452"); 24 | 25 | modelBuilder.Entity("aspnetCoreReactTemplate.Models.ApplicationUser", b => 26 | { 27 | b.Property("Id") 28 | .ValueGeneratedOnAdd() 29 | .HasColumnName("id"); 30 | 31 | b.Property("AccessFailedCount") 32 | .HasColumnName("access_failed_count"); 33 | 34 | b.Property("ConcurrencyStamp") 35 | .IsConcurrencyToken() 36 | .HasColumnName("concurrency_stamp"); 37 | 38 | b.Property("Email") 39 | .HasColumnName("email") 40 | .HasMaxLength(256); 41 | 42 | b.Property("EmailConfirmed") 43 | .HasColumnName("email_confirmed"); 44 | 45 | b.Property("GivenName") 46 | .HasColumnName("given_name"); 47 | 48 | b.Property("LockoutEnabled") 49 | .HasColumnName("lockout_enabled"); 50 | 51 | b.Property("LockoutEnd") 52 | .HasColumnName("lockout_end"); 53 | 54 | b.Property("NormalizedEmail") 55 | .HasColumnName("normalized_email") 56 | .HasMaxLength(256); 57 | 58 | b.Property("NormalizedUserName") 59 | .HasColumnName("normalized_user_name") 60 | .HasMaxLength(256); 61 | 62 | b.Property("PasswordHash") 63 | .HasColumnName("password_hash"); 64 | 65 | b.Property("PhoneNumber") 66 | .HasColumnName("phone_number"); 67 | 68 | b.Property("PhoneNumberConfirmed") 69 | .HasColumnName("phone_number_confirmed"); 70 | 71 | b.Property("SecurityStamp") 72 | .HasColumnName("security_stamp"); 73 | 74 | b.Property("TwoFactorEnabled") 75 | .HasColumnName("two_factor_enabled"); 76 | 77 | b.Property("UserName") 78 | .HasColumnName("user_name") 79 | .HasMaxLength(256); 80 | 81 | b.HasKey("Id") 82 | .HasName("pk_users"); 83 | 84 | b.HasIndex("NormalizedEmail") 85 | .HasName("email_index"); 86 | 87 | b.HasIndex("NormalizedUserName") 88 | .IsUnique() 89 | .HasName("user_name_index"); 90 | 91 | b.ToTable("users"); 92 | }); 93 | 94 | modelBuilder.Entity("aspnetCoreReactTemplate.Models.Contact", b => 95 | { 96 | b.Property("id") 97 | .ValueGeneratedOnAdd() 98 | .HasColumnName("id"); 99 | 100 | b.Property("email") 101 | .HasColumnName("email") 102 | .HasMaxLength(30); 103 | 104 | b.Property("firstName") 105 | .IsRequired() 106 | .HasColumnName("first_name"); 107 | 108 | b.Property("lastName") 109 | .IsRequired() 110 | .HasColumnName("last_name"); 111 | 112 | b.Property("phone") 113 | .HasColumnName("phone"); 114 | 115 | b.HasKey("id") 116 | .HasName("pk_contacts"); 117 | 118 | b.ToTable("contacts"); 119 | }); 120 | 121 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => 122 | { 123 | b.Property("Id") 124 | .ValueGeneratedOnAdd() 125 | .HasColumnName("id"); 126 | 127 | b.Property("ConcurrencyStamp") 128 | .IsConcurrencyToken() 129 | .HasColumnName("concurrency_stamp"); 130 | 131 | b.Property("Name") 132 | .HasColumnName("name") 133 | .HasMaxLength(256); 134 | 135 | b.Property("NormalizedName") 136 | .HasColumnName("normalized_name") 137 | .HasMaxLength(256); 138 | 139 | b.HasKey("Id") 140 | .HasName("pk_roles"); 141 | 142 | b.HasIndex("NormalizedName") 143 | .IsUnique() 144 | .HasName("role_name_index"); 145 | 146 | b.ToTable("roles"); 147 | }); 148 | 149 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => 150 | { 151 | b.Property("Id") 152 | .ValueGeneratedOnAdd() 153 | .HasColumnName("id"); 154 | 155 | b.Property("ClaimType") 156 | .HasColumnName("claim_type"); 157 | 158 | b.Property("ClaimValue") 159 | .HasColumnName("claim_value"); 160 | 161 | b.Property("RoleId") 162 | .IsRequired() 163 | .HasColumnName("role_id"); 164 | 165 | b.HasKey("Id") 166 | .HasName("pk_role_claims"); 167 | 168 | b.HasIndex("RoleId") 169 | .HasName("ix_role_claims_role_id"); 170 | 171 | b.ToTable("role_claims"); 172 | }); 173 | 174 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => 175 | { 176 | b.Property("Id") 177 | .ValueGeneratedOnAdd() 178 | .HasColumnName("id"); 179 | 180 | b.Property("ClaimType") 181 | .HasColumnName("claim_type"); 182 | 183 | b.Property("ClaimValue") 184 | .HasColumnName("claim_value"); 185 | 186 | b.Property("UserId") 187 | .IsRequired() 188 | .HasColumnName("user_id"); 189 | 190 | b.HasKey("Id") 191 | .HasName("pk_user_claims"); 192 | 193 | b.HasIndex("UserId") 194 | .HasName("ix_user_claims_user_id"); 195 | 196 | b.ToTable("user_claims"); 197 | }); 198 | 199 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => 200 | { 201 | b.Property("LoginProvider") 202 | .HasColumnName("login_provider"); 203 | 204 | b.Property("ProviderKey") 205 | .HasColumnName("provider_key"); 206 | 207 | b.Property("ProviderDisplayName") 208 | .HasColumnName("provider_display_name"); 209 | 210 | b.Property("UserId") 211 | .IsRequired() 212 | .HasColumnName("user_id"); 213 | 214 | b.HasKey("LoginProvider", "ProviderKey") 215 | .HasName("pk_user_logins"); 216 | 217 | b.HasIndex("UserId") 218 | .HasName("ix_user_logins_user_id"); 219 | 220 | b.ToTable("user_logins"); 221 | }); 222 | 223 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => 224 | { 225 | b.Property("UserId") 226 | .HasColumnName("user_id"); 227 | 228 | b.Property("RoleId") 229 | .HasColumnName("role_id"); 230 | 231 | b.HasKey("UserId", "RoleId") 232 | .HasName("pk_user_roles"); 233 | 234 | b.HasIndex("RoleId") 235 | .HasName("ix_user_roles_role_id"); 236 | 237 | b.ToTable("user_roles"); 238 | }); 239 | 240 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => 241 | { 242 | b.Property("UserId") 243 | .HasColumnName("user_id"); 244 | 245 | b.Property("LoginProvider") 246 | .HasColumnName("login_provider"); 247 | 248 | b.Property("Name") 249 | .HasColumnName("name"); 250 | 251 | b.Property("Value") 252 | .HasColumnName("value"); 253 | 254 | b.HasKey("UserId", "LoginProvider", "Name") 255 | .HasName("pk_user_tokens"); 256 | 257 | b.ToTable("user_tokens"); 258 | }); 259 | 260 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => 261 | { 262 | b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole") 263 | .WithMany() 264 | .HasForeignKey("RoleId") 265 | .HasConstraintName("fk_role_claims_roles_role_id") 266 | .OnDelete(DeleteBehavior.Cascade); 267 | }); 268 | 269 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => 270 | { 271 | b.HasOne("aspnetCoreReactTemplate.Models.ApplicationUser") 272 | .WithMany() 273 | .HasForeignKey("UserId") 274 | .HasConstraintName("fk_user_claims_users_user_id") 275 | .OnDelete(DeleteBehavior.Cascade); 276 | }); 277 | 278 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => 279 | { 280 | b.HasOne("aspnetCoreReactTemplate.Models.ApplicationUser") 281 | .WithMany() 282 | .HasForeignKey("UserId") 283 | .HasConstraintName("fk_user_logins_users_user_id") 284 | .OnDelete(DeleteBehavior.Cascade); 285 | }); 286 | 287 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => 288 | { 289 | b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole") 290 | .WithMany() 291 | .HasForeignKey("RoleId") 292 | .HasConstraintName("fk_user_roles_roles_role_id") 293 | .OnDelete(DeleteBehavior.Cascade); 294 | 295 | b.HasOne("aspnetCoreReactTemplate.Models.ApplicationUser") 296 | .WithMany() 297 | .HasForeignKey("UserId") 298 | .HasConstraintName("fk_user_roles_users_user_id") 299 | .OnDelete(DeleteBehavior.Cascade); 300 | }); 301 | 302 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => 303 | { 304 | b.HasOne("aspnetCoreReactTemplate.Models.ApplicationUser") 305 | .WithMany() 306 | .HasForeignKey("UserId") 307 | .HasConstraintName("fk_user_tokens_users_user_id") 308 | .OnDelete(DeleteBehavior.Cascade); 309 | }); 310 | #pragma warning restore 612, 618 311 | } 312 | } 313 | } 314 | -------------------------------------------------------------------------------- /api/Migrations/20171204210645_Initial.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Metadata; 2 | using Microsoft.EntityFrameworkCore.Migrations; 3 | using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; 4 | using System; 5 | using System.Collections.Generic; 6 | 7 | namespace api.Migrations 8 | { 9 | public partial class Initial : Migration 10 | { 11 | protected override void Up(MigrationBuilder migrationBuilder) 12 | { 13 | migrationBuilder.CreateTable( 14 | name: "contacts", 15 | columns: table => new 16 | { 17 | id = table.Column(type: "int4", nullable: false) 18 | .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.SerialColumn), 19 | email = table.Column(type: "varchar(30)", maxLength: 30, nullable: true), 20 | first_name = table.Column(type: "text", nullable: false), 21 | last_name = table.Column(type: "text", nullable: false), 22 | phone = table.Column(type: "text", nullable: true) 23 | }, 24 | constraints: table => 25 | { 26 | table.PrimaryKey("pk_contacts", x => x.id); 27 | }); 28 | 29 | migrationBuilder.CreateTable( 30 | name: "roles", 31 | columns: table => new 32 | { 33 | id = table.Column(type: "text", nullable: false), 34 | concurrency_stamp = table.Column(type: "text", nullable: true), 35 | name = table.Column(type: "varchar(256)", maxLength: 256, nullable: true), 36 | normalized_name = table.Column(type: "varchar(256)", maxLength: 256, nullable: true) 37 | }, 38 | constraints: table => 39 | { 40 | table.PrimaryKey("pk_roles", x => x.id); 41 | }); 42 | 43 | migrationBuilder.CreateTable( 44 | name: "users", 45 | columns: table => new 46 | { 47 | id = table.Column(type: "text", nullable: false), 48 | access_failed_count = table.Column(type: "int4", nullable: false), 49 | concurrency_stamp = table.Column(type: "text", nullable: true), 50 | email = table.Column(type: "varchar(256)", maxLength: 256, nullable: true), 51 | email_confirmed = table.Column(type: "bool", nullable: false), 52 | given_name = table.Column(type: "text", nullable: true), 53 | lockout_enabled = table.Column(type: "bool", nullable: false), 54 | lockout_end = table.Column(type: "timestamptz", nullable: true), 55 | normalized_email = table.Column(type: "varchar(256)", maxLength: 256, nullable: true), 56 | normalized_user_name = table.Column(type: "varchar(256)", maxLength: 256, nullable: true), 57 | password_hash = table.Column(type: "text", nullable: true), 58 | phone_number = table.Column(type: "text", nullable: true), 59 | phone_number_confirmed = table.Column(type: "bool", nullable: false), 60 | security_stamp = table.Column(type: "text", nullable: true), 61 | two_factor_enabled = table.Column(type: "bool", nullable: false), 62 | user_name = table.Column(type: "varchar(256)", maxLength: 256, nullable: true) 63 | }, 64 | constraints: table => 65 | { 66 | table.PrimaryKey("pk_users", x => x.id); 67 | }); 68 | 69 | migrationBuilder.CreateTable( 70 | name: "role_claims", 71 | columns: table => new 72 | { 73 | id = table.Column(type: "int4", nullable: false) 74 | .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.SerialColumn), 75 | claim_type = table.Column(type: "text", nullable: true), 76 | claim_value = table.Column(type: "text", nullable: true), 77 | role_id = table.Column(type: "text", nullable: false) 78 | }, 79 | constraints: table => 80 | { 81 | table.PrimaryKey("pk_role_claims", x => x.id); 82 | table.ForeignKey( 83 | name: "fk_role_claims_roles_role_id", 84 | column: x => x.role_id, 85 | principalTable: "roles", 86 | principalColumn: "id", 87 | onDelete: ReferentialAction.Cascade); 88 | }); 89 | 90 | migrationBuilder.CreateTable( 91 | name: "user_claims", 92 | columns: table => new 93 | { 94 | id = table.Column(type: "int4", nullable: false) 95 | .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.SerialColumn), 96 | claim_type = table.Column(type: "text", nullable: true), 97 | claim_value = table.Column(type: "text", nullable: true), 98 | user_id = table.Column(type: "text", nullable: false) 99 | }, 100 | constraints: table => 101 | { 102 | table.PrimaryKey("pk_user_claims", x => x.id); 103 | table.ForeignKey( 104 | name: "fk_user_claims_users_user_id", 105 | column: x => x.user_id, 106 | principalTable: "users", 107 | principalColumn: "id", 108 | onDelete: ReferentialAction.Cascade); 109 | }); 110 | 111 | migrationBuilder.CreateTable( 112 | name: "user_logins", 113 | columns: table => new 114 | { 115 | login_provider = table.Column(type: "text", nullable: false), 116 | provider_key = table.Column(type: "text", nullable: false), 117 | provider_display_name = table.Column(type: "text", nullable: true), 118 | user_id = table.Column(type: "text", nullable: false) 119 | }, 120 | constraints: table => 121 | { 122 | table.PrimaryKey("pk_user_logins", x => new { x.login_provider, x.provider_key }); 123 | table.ForeignKey( 124 | name: "fk_user_logins_users_user_id", 125 | column: x => x.user_id, 126 | principalTable: "users", 127 | principalColumn: "id", 128 | onDelete: ReferentialAction.Cascade); 129 | }); 130 | 131 | migrationBuilder.CreateTable( 132 | name: "user_roles", 133 | columns: table => new 134 | { 135 | user_id = table.Column(type: "text", nullable: false), 136 | role_id = table.Column(type: "text", nullable: false) 137 | }, 138 | constraints: table => 139 | { 140 | table.PrimaryKey("pk_user_roles", x => new { x.user_id, x.role_id }); 141 | table.ForeignKey( 142 | name: "fk_user_roles_roles_role_id", 143 | column: x => x.role_id, 144 | principalTable: "roles", 145 | principalColumn: "id", 146 | onDelete: ReferentialAction.Cascade); 147 | table.ForeignKey( 148 | name: "fk_user_roles_users_user_id", 149 | column: x => x.user_id, 150 | principalTable: "users", 151 | principalColumn: "id", 152 | onDelete: ReferentialAction.Cascade); 153 | }); 154 | 155 | migrationBuilder.CreateTable( 156 | name: "user_tokens", 157 | columns: table => new 158 | { 159 | user_id = table.Column(type: "text", nullable: false), 160 | login_provider = table.Column(type: "text", nullable: false), 161 | name = table.Column(type: "text", nullable: false), 162 | value = table.Column(type: "text", nullable: true) 163 | }, 164 | constraints: table => 165 | { 166 | table.PrimaryKey("pk_user_tokens", x => new { x.user_id, x.login_provider, x.name }); 167 | table.ForeignKey( 168 | name: "fk_user_tokens_users_user_id", 169 | column: x => x.user_id, 170 | principalTable: "users", 171 | principalColumn: "id", 172 | onDelete: ReferentialAction.Cascade); 173 | }); 174 | 175 | migrationBuilder.CreateIndex( 176 | name: "ix_role_claims_role_id", 177 | table: "role_claims", 178 | column: "role_id"); 179 | 180 | migrationBuilder.CreateIndex( 181 | name: "role_name_index", 182 | table: "roles", 183 | column: "normalized_name", 184 | unique: true); 185 | 186 | migrationBuilder.CreateIndex( 187 | name: "ix_user_claims_user_id", 188 | table: "user_claims", 189 | column: "user_id"); 190 | 191 | migrationBuilder.CreateIndex( 192 | name: "ix_user_logins_user_id", 193 | table: "user_logins", 194 | column: "user_id"); 195 | 196 | migrationBuilder.CreateIndex( 197 | name: "ix_user_roles_role_id", 198 | table: "user_roles", 199 | column: "role_id"); 200 | 201 | migrationBuilder.CreateIndex( 202 | name: "email_index", 203 | table: "users", 204 | column: "normalized_email"); 205 | 206 | migrationBuilder.CreateIndex( 207 | name: "user_name_index", 208 | table: "users", 209 | column: "normalized_user_name", 210 | unique: true); 211 | } 212 | 213 | protected override void Down(MigrationBuilder migrationBuilder) 214 | { 215 | migrationBuilder.DropTable( 216 | name: "contacts"); 217 | 218 | migrationBuilder.DropTable( 219 | name: "role_claims"); 220 | 221 | migrationBuilder.DropTable( 222 | name: "user_claims"); 223 | 224 | migrationBuilder.DropTable( 225 | name: "user_logins"); 226 | 227 | migrationBuilder.DropTable( 228 | name: "user_roles"); 229 | 230 | migrationBuilder.DropTable( 231 | name: "user_tokens"); 232 | 233 | migrationBuilder.DropTable( 234 | name: "roles"); 235 | 236 | migrationBuilder.DropTable( 237 | name: "users"); 238 | } 239 | } 240 | } 241 | -------------------------------------------------------------------------------- /api/Migrations/20200128145031_1580223026.Designer.cs: -------------------------------------------------------------------------------- 1 | // 2 | using System; 3 | using Microsoft.EntityFrameworkCore; 4 | using Microsoft.EntityFrameworkCore.Infrastructure; 5 | using Microsoft.EntityFrameworkCore.Migrations; 6 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 7 | using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; 8 | using aspnetCoreReactTemplate.Models; 9 | 10 | namespace api.Migrations 11 | { 12 | [DbContext(typeof(DefaultDbContext))] 13 | [Migration("20200128145031_1580223026")] 14 | partial class _1580223026 15 | { 16 | protected override void BuildTargetModel(ModelBuilder modelBuilder) 17 | { 18 | #pragma warning disable 612, 618 19 | modelBuilder 20 | .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn) 21 | .HasAnnotation("ProductVersion", "3.1.1") 22 | .HasAnnotation("Relational:MaxIdentifierLength", 63); 23 | 24 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => 25 | { 26 | b.Property("Id") 27 | .HasColumnName("id") 28 | .HasColumnType("text"); 29 | 30 | b.Property("ConcurrencyStamp") 31 | .IsConcurrencyToken() 32 | .HasColumnName("concurrency_stamp") 33 | .HasColumnType("text"); 34 | 35 | b.Property("Name") 36 | .HasColumnName("name") 37 | .HasColumnType("character varying(256)") 38 | .HasMaxLength(256); 39 | 40 | b.Property("NormalizedName") 41 | .HasColumnName("normalized_name") 42 | .HasColumnType("character varying(256)") 43 | .HasMaxLength(256); 44 | 45 | b.HasKey("Id") 46 | .HasName("pk_roles"); 47 | 48 | b.HasIndex("NormalizedName") 49 | .IsUnique() 50 | .HasName("role_name_index"); 51 | 52 | b.ToTable("roles"); 53 | }); 54 | 55 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => 56 | { 57 | b.Property("Id") 58 | .ValueGeneratedOnAdd() 59 | .HasColumnName("id") 60 | .HasColumnType("integer") 61 | .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); 62 | 63 | b.Property("ClaimType") 64 | .HasColumnName("claim_type") 65 | .HasColumnType("text"); 66 | 67 | b.Property("ClaimValue") 68 | .HasColumnName("claim_value") 69 | .HasColumnType("text"); 70 | 71 | b.Property("RoleId") 72 | .IsRequired() 73 | .HasColumnName("role_id") 74 | .HasColumnType("text"); 75 | 76 | b.HasKey("Id") 77 | .HasName("pk_role_claims"); 78 | 79 | b.HasIndex("RoleId") 80 | .HasName("ix_role_claims_role_id"); 81 | 82 | b.ToTable("role_claims"); 83 | }); 84 | 85 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => 86 | { 87 | b.Property("Id") 88 | .ValueGeneratedOnAdd() 89 | .HasColumnName("id") 90 | .HasColumnType("integer") 91 | .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); 92 | 93 | b.Property("ClaimType") 94 | .HasColumnName("claim_type") 95 | .HasColumnType("text"); 96 | 97 | b.Property("ClaimValue") 98 | .HasColumnName("claim_value") 99 | .HasColumnType("text"); 100 | 101 | b.Property("UserId") 102 | .IsRequired() 103 | .HasColumnName("user_id") 104 | .HasColumnType("text"); 105 | 106 | b.HasKey("Id") 107 | .HasName("pk_user_claims"); 108 | 109 | b.HasIndex("UserId") 110 | .HasName("ix_user_claims_user_id"); 111 | 112 | b.ToTable("user_claims"); 113 | }); 114 | 115 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => 116 | { 117 | b.Property("LoginProvider") 118 | .HasColumnName("login_provider") 119 | .HasColumnType("text"); 120 | 121 | b.Property("ProviderKey") 122 | .HasColumnName("provider_key") 123 | .HasColumnType("text"); 124 | 125 | b.Property("ProviderDisplayName") 126 | .HasColumnName("provider_display_name") 127 | .HasColumnType("text"); 128 | 129 | b.Property("UserId") 130 | .IsRequired() 131 | .HasColumnName("user_id") 132 | .HasColumnType("text"); 133 | 134 | b.HasKey("LoginProvider", "ProviderKey") 135 | .HasName("pk_user_logins"); 136 | 137 | b.HasIndex("UserId") 138 | .HasName("ix_user_logins_user_id"); 139 | 140 | b.ToTable("user_logins"); 141 | }); 142 | 143 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => 144 | { 145 | b.Property("UserId") 146 | .HasColumnName("user_id") 147 | .HasColumnType("text"); 148 | 149 | b.Property("RoleId") 150 | .HasColumnName("role_id") 151 | .HasColumnType("text"); 152 | 153 | b.HasKey("UserId", "RoleId") 154 | .HasName("pk_user_roles"); 155 | 156 | b.HasIndex("RoleId") 157 | .HasName("ix_user_roles_role_id"); 158 | 159 | b.ToTable("user_roles"); 160 | }); 161 | 162 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => 163 | { 164 | b.Property("UserId") 165 | .HasColumnName("user_id") 166 | .HasColumnType("text"); 167 | 168 | b.Property("LoginProvider") 169 | .HasColumnName("login_provider") 170 | .HasColumnType("text"); 171 | 172 | b.Property("Name") 173 | .HasColumnName("name") 174 | .HasColumnType("text"); 175 | 176 | b.Property("Value") 177 | .HasColumnName("value") 178 | .HasColumnType("text"); 179 | 180 | b.HasKey("UserId", "LoginProvider", "Name") 181 | .HasName("pk_user_tokens"); 182 | 183 | b.ToTable("user_tokens"); 184 | }); 185 | 186 | modelBuilder.Entity("aspnetCoreReactTemplate.Models.ApplicationUser", b => 187 | { 188 | b.Property("Id") 189 | .HasColumnName("id") 190 | .HasColumnType("text"); 191 | 192 | b.Property("AccessFailedCount") 193 | .HasColumnName("access_failed_count") 194 | .HasColumnType("integer"); 195 | 196 | b.Property("ConcurrencyStamp") 197 | .IsConcurrencyToken() 198 | .HasColumnName("concurrency_stamp") 199 | .HasColumnType("text"); 200 | 201 | b.Property("Email") 202 | .HasColumnName("email") 203 | .HasColumnType("character varying(256)") 204 | .HasMaxLength(256); 205 | 206 | b.Property("EmailConfirmed") 207 | .HasColumnName("email_confirmed") 208 | .HasColumnType("boolean"); 209 | 210 | b.Property("GivenName") 211 | .HasColumnName("given_name") 212 | .HasColumnType("text"); 213 | 214 | b.Property("LockoutEnabled") 215 | .HasColumnName("lockout_enabled") 216 | .HasColumnType("boolean"); 217 | 218 | b.Property("LockoutEnd") 219 | .HasColumnName("lockout_end") 220 | .HasColumnType("timestamp with time zone"); 221 | 222 | b.Property("NormalizedEmail") 223 | .HasColumnName("normalized_email") 224 | .HasColumnType("character varying(256)") 225 | .HasMaxLength(256); 226 | 227 | b.Property("NormalizedUserName") 228 | .HasColumnName("normalized_user_name") 229 | .HasColumnType("character varying(256)") 230 | .HasMaxLength(256); 231 | 232 | b.Property("PasswordHash") 233 | .HasColumnName("password_hash") 234 | .HasColumnType("text"); 235 | 236 | b.Property("PhoneNumber") 237 | .HasColumnName("phone_number") 238 | .HasColumnType("text"); 239 | 240 | b.Property("PhoneNumberConfirmed") 241 | .HasColumnName("phone_number_confirmed") 242 | .HasColumnType("boolean"); 243 | 244 | b.Property("SecurityStamp") 245 | .HasColumnName("security_stamp") 246 | .HasColumnType("text"); 247 | 248 | b.Property("TwoFactorEnabled") 249 | .HasColumnName("two_factor_enabled") 250 | .HasColumnType("boolean"); 251 | 252 | b.Property("UserName") 253 | .HasColumnName("user_name") 254 | .HasColumnType("character varying(256)") 255 | .HasMaxLength(256); 256 | 257 | b.HasKey("Id") 258 | .HasName("pk_users"); 259 | 260 | b.HasIndex("NormalizedEmail") 261 | .HasName("email_index"); 262 | 263 | b.HasIndex("NormalizedUserName") 264 | .IsUnique() 265 | .HasName("user_name_index"); 266 | 267 | b.ToTable("users"); 268 | }); 269 | 270 | modelBuilder.Entity("aspnetCoreReactTemplate.Models.Contact", b => 271 | { 272 | b.Property("Id") 273 | .ValueGeneratedOnAdd() 274 | .HasColumnName("id") 275 | .HasColumnType("integer") 276 | .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); 277 | 278 | b.Property("Email") 279 | .HasColumnName("email") 280 | .HasColumnType("character varying(30)") 281 | .HasMaxLength(30); 282 | 283 | b.Property("FirstName") 284 | .IsRequired() 285 | .HasColumnName("first_name") 286 | .HasColumnType("text"); 287 | 288 | b.Property("LastName") 289 | .IsRequired() 290 | .HasColumnName("last_name") 291 | .HasColumnType("text"); 292 | 293 | b.Property("Phone") 294 | .HasColumnName("phone") 295 | .HasColumnType("text"); 296 | 297 | b.HasKey("Id") 298 | .HasName("pk_contacts"); 299 | 300 | b.ToTable("contacts"); 301 | }); 302 | 303 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => 304 | { 305 | b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) 306 | .WithMany() 307 | .HasForeignKey("RoleId") 308 | .HasConstraintName("fk_role_claims_roles_role_id") 309 | .OnDelete(DeleteBehavior.Cascade) 310 | .IsRequired(); 311 | }); 312 | 313 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => 314 | { 315 | b.HasOne("aspnetCoreReactTemplate.Models.ApplicationUser", null) 316 | .WithMany() 317 | .HasForeignKey("UserId") 318 | .HasConstraintName("fk_user_claims_asp_net_users_user_id") 319 | .OnDelete(DeleteBehavior.Cascade) 320 | .IsRequired(); 321 | }); 322 | 323 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => 324 | { 325 | b.HasOne("aspnetCoreReactTemplate.Models.ApplicationUser", null) 326 | .WithMany() 327 | .HasForeignKey("UserId") 328 | .HasConstraintName("fk_user_logins_asp_net_users_user_id") 329 | .OnDelete(DeleteBehavior.Cascade) 330 | .IsRequired(); 331 | }); 332 | 333 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => 334 | { 335 | b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) 336 | .WithMany() 337 | .HasForeignKey("RoleId") 338 | .HasConstraintName("fk_user_roles_roles_role_id") 339 | .OnDelete(DeleteBehavior.Cascade) 340 | .IsRequired(); 341 | 342 | b.HasOne("aspnetCoreReactTemplate.Models.ApplicationUser", null) 343 | .WithMany() 344 | .HasForeignKey("UserId") 345 | .HasConstraintName("fk_user_roles_asp_net_users_user_id") 346 | .OnDelete(DeleteBehavior.Cascade) 347 | .IsRequired(); 348 | }); 349 | 350 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => 351 | { 352 | b.HasOne("aspnetCoreReactTemplate.Models.ApplicationUser", null) 353 | .WithMany() 354 | .HasForeignKey("UserId") 355 | .HasConstraintName("fk_user_tokens_asp_net_users_user_id") 356 | .OnDelete(DeleteBehavior.Cascade) 357 | .IsRequired(); 358 | }); 359 | #pragma warning restore 612, 618 360 | } 361 | } 362 | } 363 | -------------------------------------------------------------------------------- /api/Migrations/20200128145031_1580223026.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Migrations; 2 | 3 | namespace api.Migrations 4 | { 5 | public partial class _1580223026 : Migration 6 | { 7 | protected override void Up(MigrationBuilder migrationBuilder) 8 | { 9 | 10 | } 11 | 12 | protected override void Down(MigrationBuilder migrationBuilder) 13 | { 14 | 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /api/Migrations/DefaultDbContextModelSnapshot.cs: -------------------------------------------------------------------------------- 1 | // 2 | using System; 3 | using Microsoft.EntityFrameworkCore; 4 | using Microsoft.EntityFrameworkCore.Infrastructure; 5 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 6 | using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; 7 | using aspnetCoreReactTemplate.Models; 8 | 9 | namespace api.Migrations 10 | { 11 | [DbContext(typeof(DefaultDbContext))] 12 | partial class DefaultDbContextModelSnapshot : ModelSnapshot 13 | { 14 | protected override void BuildModel(ModelBuilder modelBuilder) 15 | { 16 | #pragma warning disable 612, 618 17 | modelBuilder 18 | .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn) 19 | .HasAnnotation("ProductVersion", "3.1.1") 20 | .HasAnnotation("Relational:MaxIdentifierLength", 63); 21 | 22 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => 23 | { 24 | b.Property("Id") 25 | .HasColumnName("id") 26 | .HasColumnType("text"); 27 | 28 | b.Property("ConcurrencyStamp") 29 | .IsConcurrencyToken() 30 | .HasColumnName("concurrency_stamp") 31 | .HasColumnType("text"); 32 | 33 | b.Property("Name") 34 | .HasColumnName("name") 35 | .HasColumnType("character varying(256)") 36 | .HasMaxLength(256); 37 | 38 | b.Property("NormalizedName") 39 | .HasColumnName("normalized_name") 40 | .HasColumnType("character varying(256)") 41 | .HasMaxLength(256); 42 | 43 | b.HasKey("Id") 44 | .HasName("pk_roles"); 45 | 46 | b.HasIndex("NormalizedName") 47 | .IsUnique() 48 | .HasName("role_name_index"); 49 | 50 | b.ToTable("roles"); 51 | }); 52 | 53 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => 54 | { 55 | b.Property("Id") 56 | .ValueGeneratedOnAdd() 57 | .HasColumnName("id") 58 | .HasColumnType("integer") 59 | .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); 60 | 61 | b.Property("ClaimType") 62 | .HasColumnName("claim_type") 63 | .HasColumnType("text"); 64 | 65 | b.Property("ClaimValue") 66 | .HasColumnName("claim_value") 67 | .HasColumnType("text"); 68 | 69 | b.Property("RoleId") 70 | .IsRequired() 71 | .HasColumnName("role_id") 72 | .HasColumnType("text"); 73 | 74 | b.HasKey("Id") 75 | .HasName("pk_role_claims"); 76 | 77 | b.HasIndex("RoleId") 78 | .HasName("ix_role_claims_role_id"); 79 | 80 | b.ToTable("role_claims"); 81 | }); 82 | 83 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => 84 | { 85 | b.Property("Id") 86 | .ValueGeneratedOnAdd() 87 | .HasColumnName("id") 88 | .HasColumnType("integer") 89 | .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); 90 | 91 | b.Property("ClaimType") 92 | .HasColumnName("claim_type") 93 | .HasColumnType("text"); 94 | 95 | b.Property("ClaimValue") 96 | .HasColumnName("claim_value") 97 | .HasColumnType("text"); 98 | 99 | b.Property("UserId") 100 | .IsRequired() 101 | .HasColumnName("user_id") 102 | .HasColumnType("text"); 103 | 104 | b.HasKey("Id") 105 | .HasName("pk_user_claims"); 106 | 107 | b.HasIndex("UserId") 108 | .HasName("ix_user_claims_user_id"); 109 | 110 | b.ToTable("user_claims"); 111 | }); 112 | 113 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => 114 | { 115 | b.Property("LoginProvider") 116 | .HasColumnName("login_provider") 117 | .HasColumnType("text"); 118 | 119 | b.Property("ProviderKey") 120 | .HasColumnName("provider_key") 121 | .HasColumnType("text"); 122 | 123 | b.Property("ProviderDisplayName") 124 | .HasColumnName("provider_display_name") 125 | .HasColumnType("text"); 126 | 127 | b.Property("UserId") 128 | .IsRequired() 129 | .HasColumnName("user_id") 130 | .HasColumnType("text"); 131 | 132 | b.HasKey("LoginProvider", "ProviderKey") 133 | .HasName("pk_user_logins"); 134 | 135 | b.HasIndex("UserId") 136 | .HasName("ix_user_logins_user_id"); 137 | 138 | b.ToTable("user_logins"); 139 | }); 140 | 141 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => 142 | { 143 | b.Property("UserId") 144 | .HasColumnName("user_id") 145 | .HasColumnType("text"); 146 | 147 | b.Property("RoleId") 148 | .HasColumnName("role_id") 149 | .HasColumnType("text"); 150 | 151 | b.HasKey("UserId", "RoleId") 152 | .HasName("pk_user_roles"); 153 | 154 | b.HasIndex("RoleId") 155 | .HasName("ix_user_roles_role_id"); 156 | 157 | b.ToTable("user_roles"); 158 | }); 159 | 160 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => 161 | { 162 | b.Property("UserId") 163 | .HasColumnName("user_id") 164 | .HasColumnType("text"); 165 | 166 | b.Property("LoginProvider") 167 | .HasColumnName("login_provider") 168 | .HasColumnType("text"); 169 | 170 | b.Property("Name") 171 | .HasColumnName("name") 172 | .HasColumnType("text"); 173 | 174 | b.Property("Value") 175 | .HasColumnName("value") 176 | .HasColumnType("text"); 177 | 178 | b.HasKey("UserId", "LoginProvider", "Name") 179 | .HasName("pk_user_tokens"); 180 | 181 | b.ToTable("user_tokens"); 182 | }); 183 | 184 | modelBuilder.Entity("aspnetCoreReactTemplate.Models.ApplicationUser", b => 185 | { 186 | b.Property("Id") 187 | .HasColumnName("id") 188 | .HasColumnType("text"); 189 | 190 | b.Property("AccessFailedCount") 191 | .HasColumnName("access_failed_count") 192 | .HasColumnType("integer"); 193 | 194 | b.Property("ConcurrencyStamp") 195 | .IsConcurrencyToken() 196 | .HasColumnName("concurrency_stamp") 197 | .HasColumnType("text"); 198 | 199 | b.Property("Email") 200 | .HasColumnName("email") 201 | .HasColumnType("character varying(256)") 202 | .HasMaxLength(256); 203 | 204 | b.Property("EmailConfirmed") 205 | .HasColumnName("email_confirmed") 206 | .HasColumnType("boolean"); 207 | 208 | b.Property("GivenName") 209 | .HasColumnName("given_name") 210 | .HasColumnType("text"); 211 | 212 | b.Property("LockoutEnabled") 213 | .HasColumnName("lockout_enabled") 214 | .HasColumnType("boolean"); 215 | 216 | b.Property("LockoutEnd") 217 | .HasColumnName("lockout_end") 218 | .HasColumnType("timestamp with time zone"); 219 | 220 | b.Property("NormalizedEmail") 221 | .HasColumnName("normalized_email") 222 | .HasColumnType("character varying(256)") 223 | .HasMaxLength(256); 224 | 225 | b.Property("NormalizedUserName") 226 | .HasColumnName("normalized_user_name") 227 | .HasColumnType("character varying(256)") 228 | .HasMaxLength(256); 229 | 230 | b.Property("PasswordHash") 231 | .HasColumnName("password_hash") 232 | .HasColumnType("text"); 233 | 234 | b.Property("PhoneNumber") 235 | .HasColumnName("phone_number") 236 | .HasColumnType("text"); 237 | 238 | b.Property("PhoneNumberConfirmed") 239 | .HasColumnName("phone_number_confirmed") 240 | .HasColumnType("boolean"); 241 | 242 | b.Property("SecurityStamp") 243 | .HasColumnName("security_stamp") 244 | .HasColumnType("text"); 245 | 246 | b.Property("TwoFactorEnabled") 247 | .HasColumnName("two_factor_enabled") 248 | .HasColumnType("boolean"); 249 | 250 | b.Property("UserName") 251 | .HasColumnName("user_name") 252 | .HasColumnType("character varying(256)") 253 | .HasMaxLength(256); 254 | 255 | b.HasKey("Id") 256 | .HasName("pk_users"); 257 | 258 | b.HasIndex("NormalizedEmail") 259 | .HasName("email_index"); 260 | 261 | b.HasIndex("NormalizedUserName") 262 | .IsUnique() 263 | .HasName("user_name_index"); 264 | 265 | b.ToTable("users"); 266 | }); 267 | 268 | modelBuilder.Entity("aspnetCoreReactTemplate.Models.Contact", b => 269 | { 270 | b.Property("Id") 271 | .ValueGeneratedOnAdd() 272 | .HasColumnName("id") 273 | .HasColumnType("integer") 274 | .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); 275 | 276 | b.Property("Email") 277 | .HasColumnName("email") 278 | .HasColumnType("character varying(30)") 279 | .HasMaxLength(30); 280 | 281 | b.Property("FirstName") 282 | .IsRequired() 283 | .HasColumnName("first_name") 284 | .HasColumnType("text"); 285 | 286 | b.Property("LastName") 287 | .IsRequired() 288 | .HasColumnName("last_name") 289 | .HasColumnType("text"); 290 | 291 | b.Property("Phone") 292 | .HasColumnName("phone") 293 | .HasColumnType("text"); 294 | 295 | b.HasKey("Id") 296 | .HasName("pk_contacts"); 297 | 298 | b.ToTable("contacts"); 299 | }); 300 | 301 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => 302 | { 303 | b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) 304 | .WithMany() 305 | .HasForeignKey("RoleId") 306 | .HasConstraintName("fk_role_claims_roles_role_id") 307 | .OnDelete(DeleteBehavior.Cascade) 308 | .IsRequired(); 309 | }); 310 | 311 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => 312 | { 313 | b.HasOne("aspnetCoreReactTemplate.Models.ApplicationUser", null) 314 | .WithMany() 315 | .HasForeignKey("UserId") 316 | .HasConstraintName("fk_user_claims_asp_net_users_user_id") 317 | .OnDelete(DeleteBehavior.Cascade) 318 | .IsRequired(); 319 | }); 320 | 321 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => 322 | { 323 | b.HasOne("aspnetCoreReactTemplate.Models.ApplicationUser", null) 324 | .WithMany() 325 | .HasForeignKey("UserId") 326 | .HasConstraintName("fk_user_logins_asp_net_users_user_id") 327 | .OnDelete(DeleteBehavior.Cascade) 328 | .IsRequired(); 329 | }); 330 | 331 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => 332 | { 333 | b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) 334 | .WithMany() 335 | .HasForeignKey("RoleId") 336 | .HasConstraintName("fk_user_roles_roles_role_id") 337 | .OnDelete(DeleteBehavior.Cascade) 338 | .IsRequired(); 339 | 340 | b.HasOne("aspnetCoreReactTemplate.Models.ApplicationUser", null) 341 | .WithMany() 342 | .HasForeignKey("UserId") 343 | .HasConstraintName("fk_user_roles_asp_net_users_user_id") 344 | .OnDelete(DeleteBehavior.Cascade) 345 | .IsRequired(); 346 | }); 347 | 348 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => 349 | { 350 | b.HasOne("aspnetCoreReactTemplate.Models.ApplicationUser", null) 351 | .WithMany() 352 | .HasForeignKey("UserId") 353 | .HasConstraintName("fk_user_tokens_asp_net_users_user_id") 354 | .OnDelete(DeleteBehavior.Cascade) 355 | .IsRequired(); 356 | }); 357 | #pragma warning restore 612, 618 358 | } 359 | } 360 | } 361 | -------------------------------------------------------------------------------- /api/Models/ApplicationUser.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Identity; 2 | using Microsoft.AspNetCore.Identity.EntityFrameworkCore; 3 | 4 | namespace aspnetCoreReactTemplate.Models 5 | { 6 | public class ApplicationUser: IdentityUser 7 | { 8 | public string GivenName { get; set; } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /api/Models/Contact.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace aspnetCoreReactTemplate.Models 4 | { 5 | public class Contact 6 | { 7 | public int Id { get; set; } 8 | 9 | [Required] 10 | [MinLength(3)] 11 | public string LastName { get; set; } 12 | 13 | [Required] 14 | public string FirstName { get; set; } 15 | 16 | public string Phone { get; set; } 17 | 18 | [DataType(DataType.EmailAddress)] 19 | [StringLength(30, MinimumLength = 0)] 20 | public string Email { get; set; } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /api/Models/DefaultDbContext.cs: -------------------------------------------------------------------------------- 1 | using aspnetCoreReactTemplate.Extensions; 2 | using Microsoft.AspNetCore.Identity.EntityFrameworkCore; 3 | using Microsoft.EntityFrameworkCore; 4 | 5 | namespace aspnetCoreReactTemplate.Models 6 | { 7 | public class DefaultDbContext : IdentityDbContext 8 | { 9 | public DefaultDbContext(DbContextOptions options) 10 | : base(options) 11 | { 12 | } 13 | 14 | public DbSet ApplicationUsers { get; set; } 15 | public DbSet Contacts { get; set; } 16 | 17 | protected override void OnModelCreating(ModelBuilder modelBuilder) 18 | { 19 | base.OnModelCreating(modelBuilder); 20 | 21 | foreach (var entity in modelBuilder.Model.GetEntityTypes()) 22 | { 23 | // Remove 'AspNet' prefix and convert table name from PascalCase to snake_case. E.g. AspNetRoleClaims -> role_claims 24 | entity.SetTableName(entity.GetTableName().Replace("AspNet", "").ToSnakeCase()); 25 | 26 | // Convert column names from PascalCase to snake_case. 27 | foreach (var property in entity.GetProperties()) 28 | { 29 | property.SetColumnName(property.Name.ToSnakeCase()); 30 | } 31 | 32 | // Convert primary key names from PascalCase to snake_case. E.g. PK_users -> pk_users 33 | foreach (var key in entity.GetKeys()) 34 | { 35 | key.SetName(key.GetName().ToSnakeCase()); 36 | } 37 | 38 | // Convert foreign key names from PascalCase to snake_case. 39 | foreach (var key in entity.GetForeignKeys()) 40 | { 41 | key.SetConstraintName(key.GetConstraintName().ToSnakeCase()); 42 | } 43 | 44 | // Convert index names from PascalCase to snake_case. 45 | foreach (var index in entity.GetIndexes()) 46 | { 47 | index.SetName(index.GetName().ToSnakeCase()); 48 | } 49 | } 50 | 51 | } 52 | 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /api/Models/DefaultDbContextInitializer.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Identity; 2 | using System.Linq; 3 | using System.Threading.Tasks; 4 | 5 | namespace aspnetCoreReactTemplate.Models 6 | { 7 | public class DefaultDbContextInitializer : IDefaultDbContextInitializer 8 | { 9 | private readonly DefaultDbContext _context; 10 | private readonly UserManager _userManager; 11 | private readonly RoleManager _roleManager; 12 | 13 | public DefaultDbContextInitializer(DefaultDbContext context, UserManager userManager, RoleManager roleManager) 14 | { 15 | _userManager = userManager; 16 | _context = context; 17 | _roleManager = roleManager; 18 | } 19 | 20 | public bool EnsureCreated() 21 | { 22 | return _context.Database.EnsureCreated(); 23 | } 24 | 25 | public async Task Seed() 26 | { 27 | var email = "user@test.com"; 28 | if (await _userManager.FindByEmailAsync(email) == null) 29 | { 30 | var user = new ApplicationUser 31 | { 32 | UserName = email, 33 | Email = email, 34 | EmailConfirmed = true, 35 | GivenName = "John Doe" 36 | }; 37 | 38 | await _userManager.CreateAsync(user, "P2ssw0rd!"); 39 | } 40 | 41 | if (_context.Contacts.Any()) 42 | { 43 | foreach (var u in _context.Contacts) 44 | { 45 | _context.Remove(u); 46 | } 47 | } 48 | 49 | _context.Contacts.Add(new Contact() { LastName = "Finkley", FirstName = "Adam", Phone = "555-555-5555", Email = "adam@somewhere.com" }); 50 | _context.Contacts.Add(new Contact() { LastName = "Biles", FirstName = "Steven", Phone = "555-555-5555", Email = "sbiles@somewhere.com" }); 51 | _context.SaveChanges(); 52 | } 53 | } 54 | 55 | public interface IDefaultDbContextInitializer 56 | { 57 | bool EnsureCreated(); 58 | Task Seed(); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /api/Models/DesignTimeDefaultDbContext.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Microsoft.EntityFrameworkCore.Design; 3 | using Microsoft.Extensions.Configuration; 4 | 5 | namespace aspnetCoreReactTemplate.Models 6 | { 7 | public class BloggingContextFactory : IDesignTimeDbContextFactory 8 | { 9 | public DefaultDbContext CreateDbContext(string[] args) 10 | { 11 | var config = new ConfigurationBuilder() 12 | .SetBasePath(System.IO.Directory.GetCurrentDirectory()) 13 | .AddJsonFile("appsettings.json") 14 | .Build(); 15 | 16 | var options = new DbContextOptionsBuilder(); 17 | options.UseNpgsql(config.GetConnectionString("defaultConnection")); 18 | 19 | return new DefaultDbContext(options.Options); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /api/NuGet.Config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /api/Program.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using aspnetCoreReactTemplate.Models; 3 | using Microsoft.AspNetCore.Hosting; 4 | using Microsoft.EntityFrameworkCore; 5 | using Microsoft.Extensions.Configuration; 6 | using Microsoft.Extensions.DependencyInjection; 7 | using Microsoft.Extensions.Hosting; 8 | using Microsoft.Extensions.Logging; 9 | 10 | namespace aspnetCoreReactTemplate 11 | { 12 | public class Program 13 | { 14 | public static void Main(string[] args) 15 | { 16 | var config = new ConfigurationBuilder() 17 | .SetBasePath(Directory.GetCurrentDirectory()) 18 | .AddJsonFile("appsettings.json") 19 | .Build(); 20 | 21 | var host = CreateHostBuilder(config, args).Build(); 22 | using (var scope = host.Services.CreateScope()) 23 | { 24 | var dbContext = scope.ServiceProvider.GetService(); 25 | dbContext.Database.Migrate(); 26 | 27 | var env = scope.ServiceProvider.GetRequiredService(); 28 | if (env.IsDevelopment()) 29 | { 30 | // Seed the database in development mode 31 | var dbInitializer = scope.ServiceProvider.GetRequiredService(); 32 | dbInitializer.Seed().GetAwaiter().GetResult(); 33 | } 34 | } 35 | 36 | host.Run(); 37 | } 38 | 39 | public static IHostBuilder CreateHostBuilder(IConfigurationRoot config, string[] args) => 40 | Host.CreateDefaultBuilder(args) 41 | .ConfigureLogging(logging => 42 | { 43 | logging.ClearProviders(); 44 | // Log to console (stdout) - in production stdout will be written to /var/log/{{app_name}}.out.log 45 | logging.AddConsole(); 46 | logging.AddDebug(); 47 | }) 48 | .ConfigureWebHostDefaults(webBuilder => 49 | { 50 | webBuilder 51 | .UseUrls(config["serverBindingUrl"]) 52 | .UseStartup(); 53 | }); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /api/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:5000/", 7 | "sslPort": 0 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": true, 14 | "launchUrl": "http://localhost:5000", 15 | "environmentVariables": { 16 | "NODE_PATH": "../node_modules/", 17 | "ASPNETCORE_ENVIRONMENT": "Development" 18 | } 19 | }, 20 | "api": { 21 | "commandName": "Project", 22 | "launchBrowser": true, 23 | "environmentVariables": { 24 | "NODE_PATH": "../node_modules/", 25 | "ASPNETCORE_ENVIRONMENT": "Development" 26 | }, 27 | "applicationUrl": "http://localhost:5000/" 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /api/Services/EmailSender.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using System.Net.Mail; 3 | using System.Text; 4 | using System.Threading.Tasks; 5 | using Microsoft.Extensions.Options; 6 | 7 | namespace aspnetCoreReactTemplate.Services 8 | { 9 | public class EmailSender : IEmailSender 10 | { 11 | public EmailSender(IOptions optionsAccessor) 12 | { 13 | Options = optionsAccessor.Value; 14 | } 15 | 16 | public EmailSenderOptions Options { get; } 17 | 18 | public async Task SendEmailAsync(string toEmail, string subject, string htmlMessage, string textMessage = null) 19 | { 20 | MailMessage mailMessage = new MailMessage(); 21 | mailMessage.From = new MailAddress(this.Options.emailFromAddress, this.Options.emailFromName); 22 | mailMessage.To.Add(toEmail); 23 | mailMessage.Body = textMessage; 24 | mailMessage.BodyEncoding = Encoding.UTF8; 25 | mailMessage.Subject = subject; 26 | mailMessage.SubjectEncoding = Encoding.UTF8; 27 | 28 | if (!string.IsNullOrEmpty(htmlMessage)) 29 | { 30 | AlternateView htmlView = AlternateView.CreateAlternateViewFromString(htmlMessage); 31 | htmlView.ContentType = new System.Net.Mime.ContentType("text/html"); 32 | mailMessage.AlternateViews.Add(htmlView); 33 | } 34 | 35 | using (SmtpClient client = new SmtpClient(this.Options.host, this.Options.port)) 36 | { 37 | client.UseDefaultCredentials = false; 38 | client.Credentials = new NetworkCredential(this.Options.username, this.Options.password); 39 | client.EnableSsl = this.Options.enableSSL; 40 | await client.SendMailAsync(mailMessage); 41 | } 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /api/Services/EmailSenderOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text.RegularExpressions; 3 | 4 | namespace aspnetCoreReactTemplate.Services 5 | { 6 | public class EmailSenderOptions 7 | { 8 | private string _smtpConfig { get; set; } 9 | public string smtpConfig 10 | { 11 | get { return this._smtpConfig; } 12 | set 13 | { 14 | this._smtpConfig = value; 15 | 16 | // smtpConfig is in username:password@localhost:1025 format; extract the part 17 | var smtpConfigPartsRegEx = new Regex(@"(.*)\:(.*)@(.+)\:(.+)"); 18 | var smtpConfigPartsMatch = smtpConfigPartsRegEx.Match(value); 19 | 20 | this.username = smtpConfigPartsMatch.Groups[1].Value; 21 | this.password = smtpConfigPartsMatch.Groups[2].Value; 22 | this.host = smtpConfigPartsMatch.Groups[3].Value; 23 | this.port = Convert.ToInt32(smtpConfigPartsMatch.Groups[4].Value); 24 | } 25 | } 26 | 27 | public string emailFromName { get; set; } 28 | public string emailFromAddress { get; set; } 29 | public bool enableSSL { get; set; } 30 | public string username { get; protected set; } 31 | public string password { get; protected set; } 32 | public string host { get; protected set; } 33 | public int port { get; protected set; } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /api/Services/IEmailSender.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace aspnetCoreReactTemplate.Services 4 | { 5 | public interface IEmailSender 6 | { 7 | Task SendEmailAsync(string toEmail, string subject, string htmlMessage, string textMessage = null); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /api/Services/JwtOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text.RegularExpressions; 3 | 4 | namespace aspnetCoreReactTemplate.Services 5 | { 6 | public class JwtOptions 7 | { 8 | public string key { get; set; } 9 | public string issuer { get; set; } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /api/Startup.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Builder; 2 | using Microsoft.Extensions.Configuration; 3 | using Microsoft.Extensions.DependencyInjection; 4 | using Microsoft.Extensions.Logging; 5 | using Microsoft.EntityFrameworkCore; 6 | using aspnetCoreReactTemplate.Models; 7 | using aspnetCoreReactTemplate.Services; 8 | using Microsoft.AspNetCore.HttpOverrides; 9 | using Microsoft.AspNetCore.Identity; 10 | using Microsoft.AspNetCore.Authentication.JwtBearer; 11 | using Microsoft.IdentityModel.Tokens; 12 | using Microsoft.Extensions.Hosting; 13 | using System.Text; 14 | 15 | namespace aspnetCoreReactTemplate 16 | { 17 | public class Startup 18 | { 19 | public IHostEnvironment CurrentEnvironment { get; protected set; } 20 | public IConfiguration Configuration { get; } 21 | 22 | public Startup(IConfiguration configuration) 23 | { 24 | Configuration = configuration; 25 | } 26 | 27 | // This method gets called by the runtime. Use this method to add services to the container. 28 | public void ConfigureServices(IServiceCollection services) 29 | { 30 | services.AddControllersWithViews(); 31 | 32 | services.AddEntityFrameworkNpgsql().AddDbContext(options => 33 | { 34 | options.UseNpgsql(Configuration.GetConnectionString("defaultConnection")); 35 | }); 36 | 37 | // Configure Entity Framework Initializer for seeding 38 | services.AddTransient(); 39 | 40 | services.AddDatabaseDeveloperPageExceptionFilter(); 41 | 42 | // Configure Entity Framework Identity for Auth 43 | services.AddIdentity() 44 | .AddEntityFrameworkStores() 45 | .AddDefaultTokenProviders(); 46 | 47 | services.AddAuthentication(options => 48 | { 49 | options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; 50 | options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; 51 | }) 52 | 53 | .AddJwtBearer(config => 54 | { 55 | config.RequireHttpsMetadata = false; 56 | config.SaveToken = true; 57 | 58 | config.TokenValidationParameters = new TokenValidationParameters() 59 | { 60 | ValidIssuer = Configuration["jwt:issuer"], 61 | ValidAudience = Configuration["jwt:issuer"], 62 | IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["jwt:key"])) 63 | }; 64 | }); 65 | 66 | services.AddTransient(); 67 | services.Configure(Configuration.GetSection("email")); 68 | services.Configure(Configuration.GetSection("jwt")); 69 | } 70 | 71 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 72 | public void Configure(IApplicationBuilder app, IHostEnvironment env, ILoggerFactory loggerFactory) 73 | { 74 | // If not requesting /api*, rewrite to / so SPA app will be returned 75 | app.UseSpaFallback(new SpaFallbackOptions() 76 | { 77 | ApiPathPrefix = "/api", 78 | RewritePath = "/" 79 | }); 80 | 81 | app.UseDefaultFiles(); 82 | app.UseHttpsRedirection(); 83 | app.UseStaticFiles(); 84 | 85 | app.UseForwardedHeaders(new ForwardedHeadersOptions 86 | { 87 | // Read and use headers coming from reverse proxy: X-Forwarded-For X-Forwarded-Proto 88 | // This is particularly important so that HttpContet.Request.Scheme will be correct behind a SSL terminating proxy 89 | ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto 90 | }); 91 | 92 | app.UseRouting(); 93 | 94 | app.UseAuthentication(); 95 | app.UseAuthorization(); 96 | 97 | app.UseEndpoints(endpoints => 98 | { 99 | endpoints.MapControllerRoute( 100 | name: "default", 101 | pattern: "{controller=Home}/{action=Index}/{id?}"); 102 | }); 103 | 104 | if (env.IsDevelopment()) 105 | { 106 | app.UseSpa(spa => 107 | { 108 | spa.UseProxyToSpaDevelopmentServer("http://localhost:8080/"); 109 | }); 110 | 111 | app.UseDeveloperExceptionPage(); 112 | app.UseMigrationsEndPoint(); 113 | } 114 | 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /api/ViewModels/ConfirmEmail.cs: -------------------------------------------------------------------------------- 1 | namespace aspnetCoreReactTemplate.ViewModels 2 | { 3 | public class ConfirmEmail 4 | { 5 | public string user_id { get; set; } 6 | public string token { get; set; } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /api/ViewModels/NewUser.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace aspnetCoreReactTemplate.ViewModels 4 | { 5 | public class NewUser 6 | { 7 | [Required] 8 | [EmailAddress] 9 | public string username { get; set; } 10 | 11 | [Required] 12 | [MinLength(8)] 13 | public string password { get; set; } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /api/api.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net5.0 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | runtime; build; native; contentfiles; analyzers; buildtransitive 13 | all 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /api/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "connectionStrings": { 3 | "defaultConnection": "Host=localhost;Port=5433;Username=postgres;Password=postgres;Database=dotnetcore" 4 | }, 5 | "frontEndUrl": "http://localhost:5000", 6 | "serverBindingUrl": "http://0.0.0.0:5000", 7 | "logging": { 8 | "includeScopes": false, 9 | "logLevel": { 10 | "default": "Debug", 11 | "system": "Information", 12 | "microsoft": "Information" 13 | } 14 | }, 15 | "webClientPath": "../client-react", 16 | "jwt": { 17 | "key" : "2af4ff57-4ca0-4b3a-804b-178ad27aaf88", 18 | "issuer": "aspnet-core-react-template" 19 | }, 20 | "email": { 21 | "smtpConfig": ":@localhost:1025", 22 | "enableSSL": false, 23 | "emailFromName": "aspnet-core-react-template", 24 | "emailFromAddress": "noreply@aspnet-core-react-template.com" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /api/wwwroot/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bradymholt/aspnet-core-react-template/2564bc935ee13d92d6c94954646b8db52d6f1035/api/wwwroot/favicon.ico -------------------------------------------------------------------------------- /client-react.test/build/client-react/styles: -------------------------------------------------------------------------------- 1 | ../../../client-react/styles/ -------------------------------------------------------------------------------- /client-react.test/polyfill/localStorage.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | polyfill: function () { 3 | window.localStorage = window.sessionStorage = { 4 | getItem: function (key) { 5 | return this[key]; 6 | }, 7 | setItem: function (key, value) { 8 | this[key] = value; 9 | } 10 | }; 11 | } 12 | } 13 | 14 | -------------------------------------------------------------------------------- /client-react.test/tests.tsx: -------------------------------------------------------------------------------- 1 | import { JSDOM } from "jsdom"; 2 | import * as localStorage from "./polyfill/localStorage.js"; 3 | 4 | before(function() { 5 | const dom = new JSDOM("", { url: "http://localhost" }); 6 | 7 | (global as any).window = dom.window; 8 | (global as any).document = dom.window.document; 9 | 10 | localStorage.polyfill(); 11 | 12 | console.log( 13 | "Successfully mocked a DOM with jsdom and polyfilled localStorage." 14 | ); 15 | }); 16 | -------------------------------------------------------------------------------- /client-react.test/tests/Contacts.tsx: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import { mount, shallow, configure } from "enzyme"; 3 | import * as Adapter from "enzyme-adapter-react-16"; 4 | import * as React from "react"; 5 | import { MemoryRouter as Router, Route } from "react-router-dom"; 6 | import { stubFetch } from "../utils"; 7 | import { Contacts } from "../../client-react/components/Contacts"; 8 | 9 | configure({ adapter: new Adapter() }); 10 | 11 | describe(" component ", function() { 12 | it("renders a h1", function() { 13 | let fakeContactsData = [ 14 | { id: 1, lastName: "Smith", firstName: "John" } 15 | ]; 16 | let fetchStub = stubFetch(fakeContactsData); 17 | let emptyArgs: any = {}; 18 | const wrapper = shallow(); 19 | expect(wrapper.find("h1")).to.have.length(1); 20 | fetchStub.restore(); 21 | }); 22 | 23 | it("renders a list of contacts", function(done) { 24 | let fakeContactsData = [ 25 | { id: 1, lastName: "Smith", firstName: "John" } 26 | ]; 27 | let fetchStub = stubFetch(fakeContactsData); 28 | 29 | const wrapper = mount( 30 | 31 | 32 | 33 | ); 34 | 35 | setImmediate(function() { 36 | expect(wrapper.html()).to.contain( 37 | fakeContactsData[fakeContactsData.length - 1].lastName 38 | ); 39 | fetchStub.restore(); 40 | done(); 41 | }); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /client-react.test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "sourceMap": true, 4 | "noImplicitAny": true, 5 | "module": "commonjs", 6 | "target":"es2015", 7 | "jsx": "react", 8 | "allowJs": true, 9 | "outDir": "./build" 10 | }, 11 | "files": [ 12 | "./tests.tsx" 13 | ], 14 | "include": [ 15 | "./tests/*.ts*" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /client-react.test/utils.ts: -------------------------------------------------------------------------------- 1 | import * as sinon from 'sinon'; 2 | 3 | 4 | /** 5 | * Stubs browser Fetch API and returns given returnData object 6 | * 7 | * @param returnData 8 | */ 9 | function stubFetch(returnData: Object) { 10 | let g = (global as any); 11 | if (!g.fetch) { 12 | // If fetch is not defined; define it as a dummy function because sinon will only stub a defined function 13 | g.fetch = function () { } 14 | } 15 | 16 | if (!g.Headers) { 17 | g.Headers = function () { 18 | this.set = function(){} 19 | } 20 | } 21 | 22 | let res = { 23 | status: 200, 24 | headers: { 25 | get: function (key: string) { return 'application/json'; } 26 | }, 27 | json: function () { return Promise.resolve(returnData) } 28 | }; 29 | 30 | return sinon.stub(global, "fetch" as any).callsFake(()=> Promise.resolve(res)); 31 | 32 | } 33 | 34 | export { stubFetch } 35 | -------------------------------------------------------------------------------- /client-react/boot.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as ReactDOM from 'react-dom'; 3 | import { BrowserRouter as Router } from 'react-router-dom'; 4 | import Routes from './components/Routes'; 5 | 6 | // Polyfills 7 | import 'whatwg-fetch'; 8 | import './polyfills/object-assign'; 9 | import './polyfills/array-find'; 10 | 11 | // Styles 12 | import '../node_modules/bootstrap/dist/css/bootstrap.css'; 13 | import './styles/global.css'; 14 | 15 | ReactDOM.render( 16 | 17 | 18 | , 19 | document.getElementById("app") 20 | ); 21 | 22 | // Allow Hot Module Reloading 23 | declare var module: any; 24 | if (module.hot) { 25 | module.hot.accept(); 26 | } 27 | -------------------------------------------------------------------------------- /client-react/components/Auth.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Link, Redirect, RouteComponentProps } from 'react-router-dom'; 3 | import { RoutePaths } from './Routes'; 4 | import AuthService from '../services/Auth'; 5 | let authStyle = require('../styles/auth.styl'); 6 | let authService = new AuthService(); 7 | 8 | export class SignIn extends React.Component, any> { 9 | refs: { 10 | username: HTMLInputElement; 11 | password: HTMLInputElement; 12 | }; 13 | 14 | state = { 15 | initialLoad: true, 16 | error: null as string 17 | }; 18 | 19 | handleSubmit(event: React.FormEvent) { 20 | event.preventDefault(); 21 | 22 | this.setState({ errors: null, initialLoad: false }); 23 | authService.signIn(this.refs.username.value, this.refs.password.value).then(response => { 24 | if (!response.is_error) { 25 | this.props.history.push(RoutePaths.Contacts); 26 | } else { 27 | this.setState({ error: response.error_content.error_description }); 28 | } 29 | }); 30 | } 31 | 32 | render() { 33 | const search = this.props.location.search; 34 | const params = new URLSearchParams(search); 35 | 36 | let initialLoadContent = null; 37 | if (this.state.initialLoad) { 38 | if (params.get('confirmed')) { 39 | initialLoadContent = 40 | Your email address has been successfully confirmed. 41 | 42 | } 43 | 44 | if (params.get('expired')) { 45 | initialLoadContent = 46 | Sesion Expired You need to sign in again. 47 | 48 | } 49 | 50 | if (this.props.history.location.state && this.props.history.location.state.signedOut) { 51 | initialLoadContent = 52 | Signed Out 53 | 54 | } 55 | } 56 | return 57 | this.handleSubmit(e)}> 58 | Please sign in 59 | {initialLoadContent} 60 | {this.state.error && 61 | 62 | {this.state.error} 63 | 64 | } 65 | Email address 66 | 67 | Password 68 | 69 | Sign in 70 | 71 | 72 | Register 73 | 74 | ; 75 | } 76 | } 77 | 78 | export class Register extends React.Component { 79 | refs: { 80 | email: HTMLInputElement; 81 | password: HTMLInputElement; 82 | }; 83 | 84 | state = { 85 | registerComplete: false, 86 | errors: {} as { [key: string]: string } 87 | }; 88 | 89 | handleSubmit(event: React.FormEvent) { 90 | event.preventDefault(); 91 | 92 | this.setState({ errors: {} }); 93 | authService.register(this.refs.email.value, this.refs.password.value).then(response => { 94 | if (!response.is_error) { 95 | this.setState({ registerComplete: true }) 96 | } else { 97 | this.setState({ errors: response.error_content }); 98 | } 99 | }); 100 | } 101 | 102 | _formGroupClass(field: string) { 103 | var className = "form-group "; 104 | if (field) { 105 | className += " has-danger" 106 | } 107 | return className; 108 | } 109 | 110 | render() { 111 | if (this.state.registerComplete) { 112 | return 113 | } else { 114 | return 115 | this.handleSubmit(e)}> 116 | Please register for access 117 | {this.state.errors.general && 118 | 119 | {this.state.errors.general} 120 | 121 | } 122 | 123 | Email address 124 | 125 | {this.state.errors.username} 126 | 127 | 128 | Password 129 | 130 | {this.state.errors.password} 131 | 132 | Sign up 133 | 134 | ; 135 | }; 136 | } 137 | } 138 | 139 | interface RegisterCompleteProps { 140 | email: string; 141 | } 142 | 143 | export class RegisterComplete extends React.Component { 144 | render() { 145 | return 146 | 147 | Success! Your account has been created. 148 | 149 | 150 | A confirmation email has been sent to {this.props.email}. You will need to follow the provided link to confirm your email address before signing in. 151 | 152 | Sign in 153 | ; 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /client-react/components/ContactForm.tsx: -------------------------------------------------------------------------------- 1 | import 'object-assign'; 2 | import * as React from 'react'; 3 | import { Link, Redirect, RouteComponentProps } from 'react-router-dom'; 4 | import ContactService, { IContact } from '../services/Contacts' 5 | import { RoutePaths } from './Routes'; 6 | 7 | let contactService = new ContactService(); 8 | 9 | export class ContactForm extends React.Component, any> { 10 | state = { 11 | contact: null as IContact, 12 | errors: {} as { [key: string]: string } 13 | } 14 | 15 | componentDidMount() { 16 | if (this.props.match.path == RoutePaths.ContactEdit) { 17 | contactService.fetch(this.props.match.params.id).then((response) => { 18 | this.setState({ contact: response.content }); 19 | }); 20 | } else { 21 | let newContact: IContact = { 22 | lastName: '', firstName: '', email: '', phone: '' 23 | }; 24 | this.setState({ contact: newContact }); 25 | } 26 | } 27 | 28 | handleSubmit(event: React.FormEvent) { 29 | event.preventDefault(); 30 | this.saveContact(this.state.contact); 31 | } 32 | 33 | handleInputChange(event: React.ChangeEvent) { 34 | const target = event.target; 35 | const value = target.type === 'checkbox' ? target.checked : target.value; 36 | const name = target.name; 37 | let contactUpdates = { 38 | [name]: value 39 | }; 40 | 41 | this.setState({ 42 | contact: Object.assign(this.state.contact, contactUpdates) 43 | }); 44 | } 45 | 46 | saveContact(contact: IContact) { 47 | this.setState({ errors: {} as { [key: string]: string } }); 48 | contactService.save(contact).then((response) => { 49 | if (!response.is_error) { 50 | this.props.history.push(RoutePaths.Contacts); 51 | } else { 52 | this.setState({ errors: response.error_content }); 53 | } 54 | }); 55 | } 56 | 57 | _formGroupClass(field: string) { 58 | var className = "form-group "; 59 | if (field) { 60 | className += " has-danger" 61 | } 62 | return className; 63 | } 64 | 65 | render() { 66 | if (!this.state.contact) { 67 | return Loading...; 68 | } 69 | else { 70 | return 71 | {this.state.contact.id ? "Edit Contact" : "New Contact" } 72 | this.handleSubmit(e)}> 73 | 74 |
150 | A confirmation email has been sent to {this.props.email}. You will need to follow the provided link to confirm your email address before signing in. 151 |