├── src └── ReactBoilerplate │ ├── Scripts │ ├── containers │ │ ├── SignIn │ │ │ └── SignIn.js │ │ ├── Home │ │ │ ├── logo.png │ │ │ ├── Banner-02-VS.png │ │ │ ├── Banner-01-Azure.png │ │ │ ├── ASP-NET-Banners-01.png │ │ │ ├── ASP-NET-Banners-02.png │ │ │ └── Home.js │ │ ├── NotFound │ │ │ └── NotFound.js │ │ ├── Manage │ │ │ ├── Index │ │ │ │ └── Index.js │ │ │ ├── ChangePassword │ │ │ │ └── ChangePassword.js │ │ │ ├── Manage.js │ │ │ ├── Security │ │ │ │ └── Security.js │ │ │ ├── Email │ │ │ │ └── Email.js │ │ │ └── Logins │ │ │ │ └── Logins.js │ │ ├── About │ │ │ └── About.js │ │ ├── Contact │ │ │ └── Contact.js │ │ ├── ResetPassword │ │ │ └── ResetPassword.js │ │ ├── ForgotPassword │ │ │ └── ForgotPassword.js │ │ ├── App │ │ │ ├── App.scss │ │ │ ├── Modals │ │ │ │ └── TwoFactorModal.js │ │ │ └── App.js │ │ ├── Register │ │ │ └── Register.js │ │ ├── index.js │ │ ├── ConfirmEmail │ │ │ └── ConfirmEmail.js │ │ └── Login │ │ │ └── Login.js │ ├── utils │ │ ├── promise-window-server.js │ │ ├── superagent-server.js │ │ ├── isEmpty.js │ │ └── modelState.js │ ├── config.js │ ├── components │ │ ├── Spinner.js │ │ ├── ExternalLoginButton.js │ │ ├── ErrorList.js │ │ ├── TwoFactor │ │ │ ├── TwoFactor.js │ │ │ ├── SendCodeForm.js │ │ │ └── VerifyCodeForm.js │ │ ├── index.js │ │ ├── ExternalLogin │ │ │ └── ExternalLogin.js │ │ ├── Form.js │ │ ├── LoginForm │ │ │ └── LoginForm.js │ │ ├── ForgotPasswordForm │ │ │ └── ForgotPasswordForm.js │ │ ├── ChangeEmailForm │ │ │ └── ChangeEmailForm.js │ │ ├── ChangePasswordForm │ │ │ └── ChangePasswordForm.js │ │ ├── ResetPasswordForm │ │ │ └── ResetPasswordForm.js │ │ ├── RegisterForm │ │ │ └── RegisterForm.js │ │ └── Input.js │ ├── redux │ │ ├── modules │ │ │ ├── manage │ │ │ │ ├── index.js │ │ │ │ ├── changePassword.js │ │ │ │ ├── email.js │ │ │ │ ├── security.js │ │ │ │ └── externalLogins.js │ │ │ ├── viewBag.js │ │ │ ├── auth.js │ │ │ ├── externalLogin.js │ │ │ └── account.js │ │ ├── reducer.js │ │ ├── configureStore.js │ │ └── middleware │ │ │ └── clientMiddleware.js │ ├── client.js │ ├── helpers │ │ ├── ApiClient.js │ │ └── Html.js │ ├── routes.js │ ├── server.js │ └── webpack │ │ ├── dev.config.js │ │ └── prod.config.js │ ├── .babelrc │ ├── wwwroot │ └── favicon.ico │ ├── .editorconfig │ ├── .bootstraprc │ ├── Services │ ├── ISmsSender.cs │ ├── IEmailSender.cs │ ├── SmsSender.cs │ └── EmailSender.cs │ ├── React.sublime-project │ ├── Models │ ├── ApplicationUser.cs │ ├── Api │ │ └── User.cs │ └── ApplicationDbContext.cs │ ├── Controllers │ ├── Manage │ │ ├── Models │ │ │ ├── SetTwoFactorModel.cs │ │ │ ├── RemoveExternalLoginModel.cs │ │ │ ├── ExternalLoginConfirmationModel.cs │ │ │ ├── ChangeEmailModel.cs │ │ │ └── ChangePasswordModel.cs │ │ ├── ServerController.cs │ │ └── ApiController.cs │ ├── Account │ │ ├── Models │ │ │ ├── ForgotPasswordModel.cs │ │ │ ├── SendCodeModel.cs │ │ │ ├── LoginModel.cs │ │ │ ├── VerifyCodeModel.cs │ │ │ ├── ResetPasswordModel.cs │ │ │ └── RegisterModel.cs │ │ ├── ServerController.cs │ │ └── ApiController.cs │ ├── Status │ │ └── ServerController.cs │ ├── Home │ │ └── HomeController.cs │ └── BaseController.cs │ ├── State │ ├── AuthState.cs │ ├── GlobalState.cs │ ├── ExternalLoginState.cs │ └── Manage │ │ └── ExternalLoginsState.cs │ ├── appsettings.json │ ├── Program.cs │ ├── web.config │ ├── Properties │ └── launchSettings.json │ ├── .eslintrc │ ├── ReactBoilerplate.csproj │ ├── package.json │ ├── gulpfile.js │ └── Startup.cs ├── resources ├── nutshell.gif └── preview-thumbnail.jpg ├── LICENSE ├── ReactBoilerplate.sln ├── README.md └── .gitignore /src/ReactBoilerplate/Scripts/containers/SignIn/SignIn.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/ReactBoilerplate/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["react", "es2015", "stage-0"], 3 | "plugins": [] 4 | } -------------------------------------------------------------------------------- /resources/nutshell.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pauldotknopf/react-aspnet-boilerplate/HEAD/resources/nutshell.gif -------------------------------------------------------------------------------- /resources/preview-thumbnail.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pauldotknopf/react-aspnet-boilerplate/HEAD/resources/preview-thumbnail.jpg -------------------------------------------------------------------------------- /src/ReactBoilerplate/wwwroot/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pauldotknopf/react-aspnet-boilerplate/HEAD/src/ReactBoilerplate/wwwroot/favicon.ico -------------------------------------------------------------------------------- /src/ReactBoilerplate/Scripts/containers/Home/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pauldotknopf/react-aspnet-boilerplate/HEAD/src/ReactBoilerplate/Scripts/containers/Home/logo.png -------------------------------------------------------------------------------- /src/ReactBoilerplate/.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | 8 | [*.md] 9 | trim_trailing_whitespace = false 10 | -------------------------------------------------------------------------------- /src/ReactBoilerplate/Scripts/containers/Home/Banner-02-VS.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pauldotknopf/react-aspnet-boilerplate/HEAD/src/ReactBoilerplate/Scripts/containers/Home/Banner-02-VS.png -------------------------------------------------------------------------------- /src/ReactBoilerplate/Scripts/containers/Home/Banner-01-Azure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pauldotknopf/react-aspnet-boilerplate/HEAD/src/ReactBoilerplate/Scripts/containers/Home/Banner-01-Azure.png -------------------------------------------------------------------------------- /src/ReactBoilerplate/Scripts/containers/Home/ASP-NET-Banners-01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pauldotknopf/react-aspnet-boilerplate/HEAD/src/ReactBoilerplate/Scripts/containers/Home/ASP-NET-Banners-01.png -------------------------------------------------------------------------------- /src/ReactBoilerplate/Scripts/containers/Home/ASP-NET-Banners-02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pauldotknopf/react-aspnet-boilerplate/HEAD/src/ReactBoilerplate/Scripts/containers/Home/ASP-NET-Banners-02.png -------------------------------------------------------------------------------- /src/ReactBoilerplate/Scripts/utils/promise-window-server.js: -------------------------------------------------------------------------------- 1 | export default class PromiseWindow { 2 | open() { 3 | throw new Error('you can not run this on the server, ding-dong.'); 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/ReactBoilerplate/.bootstraprc: -------------------------------------------------------------------------------- 1 | { 2 | "bootstrapVersion": 3, 3 | 4 | "useFlexbox": true, 5 | "extractStyles": true, 6 | "styleLoaders": ["style", "css", "sass"], 7 | 8 | "styles": true, 9 | 10 | "scripts": false 11 | } -------------------------------------------------------------------------------- /src/ReactBoilerplate/Services/ISmsSender.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace ReactBoilerplate.Services 4 | { 5 | public interface ISmsSender 6 | { 7 | Task SendSmsAsync(string number, string message); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/ReactBoilerplate/React.sublime-project: -------------------------------------------------------------------------------- 1 | { 2 | "folders": 3 | [ 4 | { 5 | "path": ".", 6 | "folder_exclude_patterns": ["bower_components", "node_modules"], 7 | "file_exclude_patterns": ["React.xproj*", "project.lock.json"] 8 | } 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /src/ReactBoilerplate/Services/IEmailSender.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace ReactBoilerplate.Services 4 | { 5 | public interface IEmailSender 6 | { 7 | Task SendEmailAsync(string email, string subject, string message); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/ReactBoilerplate/Models/ApplicationUser.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Identity; 2 | using Microsoft.AspNetCore.Identity.EntityFrameworkCore; 3 | 4 | namespace ReactBoilerplate.Models 5 | { 6 | public class ApplicationUser : IdentityUser 7 | { 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/ReactBoilerplate/Scripts/containers/NotFound/NotFound.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default function NotFound() { 4 | return ( 5 |
6 |

Doh! 404!

7 |

These are not the droids you are looking for!

8 |
9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /src/ReactBoilerplate/Controllers/Manage/Models/SetTwoFactorModel.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace ReactBoilerplate.Controllers.Manage.Models 4 | { 5 | public class SetTwoFactorModel 6 | { 7 | [JsonProperty("enabled")] 8 | public bool Enabled { get; set; } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/ReactBoilerplate/Scripts/utils/superagent-server.js: -------------------------------------------------------------------------------- 1 | // this replaces superagent on the server. 2 | // TODO: If the server attempts to use this, throw an exception. 3 | // This will ensure that not server-side-rendering depends on network requests. 4 | // The entire state should have been provided for the page already. 5 | -------------------------------------------------------------------------------- /src/ReactBoilerplate/Controllers/Account/Models/ForgotPasswordModel.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace ReactBoilerplate.Controllers.Account.Models 4 | { 5 | public class ForgotPasswordModel 6 | { 7 | [EmailAddress] 8 | [Required] 9 | public string Email { get; set; } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/ReactBoilerplate/Scripts/containers/Manage/Index/Index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | 4 | class Index extends Component { 5 | render() { 6 | return ( 7 |
8 | ); 9 | } 10 | } 11 | 12 | export default connect( 13 | (state) => state, 14 | { } 15 | )(Index); 16 | -------------------------------------------------------------------------------- /src/ReactBoilerplate/Scripts/containers/About/About.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Helmet from 'react-helmet'; 3 | 4 | export default class About extends Component { 5 | render() { 6 | return ( 7 |
8 |

About us...

9 | 10 |
11 | ); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/ReactBoilerplate/Scripts/containers/Contact/Contact.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Helmet from 'react-helmet'; 3 | 4 | export default class Contact extends Component { 5 | render() { 6 | return ( 7 |
8 |

Contact

9 | 10 |
11 | ); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/ReactBoilerplate/State/AuthState.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using ReactBoilerplate.Models.Api; 3 | 4 | namespace ReactBoilerplate.State 5 | { 6 | public class AuthState 7 | { 8 | [JsonProperty("user")] 9 | public User User { get; set; } 10 | 11 | [JsonProperty("loggedIn")] 12 | public bool LoggedIn { get; set; } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/ReactBoilerplate/Scripts/config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | app: { 3 | title: 'ASP.NET React Example', 4 | description: 'This is an example, using React with ASP.NET MVC.', 5 | head: { 6 | titleTemplate: 'React Example: %s', 7 | meta: [ 8 | { name: 'description', content: 'This is an example, using React with ASP.NET MVC.' }, 9 | ] 10 | } 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /src/ReactBoilerplate/Scripts/components/Spinner.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const fontAwesome = require('font-awesome/scss/font-awesome.scss'); 4 | 5 | const spinnerClass = fontAwesome.fa + ' ' + fontAwesome['fa-spinner'] + ' ' + fontAwesome['fa-spin'] + ' ' + fontAwesome['fa-5x']; 6 | 7 | export default () => 8 | ( 9 |

10 | 11 |

12 | ); 13 | -------------------------------------------------------------------------------- /src/ReactBoilerplate/Controllers/Manage/Models/RemoveExternalLoginModel.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace ReactBoilerplate.Controllers.Manage.Models 4 | { 5 | public class RemoveExternalLoginModel 6 | { 7 | [JsonProperty("loginProvider")] 8 | public string LoginProvider { get; set; } 9 | 10 | [JsonProperty("providerKey")] 11 | public string ProviderKey { get; set; } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/ReactBoilerplate/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "ConnectionStrings": { 3 | "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=react-dot-net-a96e017a-e5c8-494d-be7a-3d7781d2fa3a;Trusted_Connection=True;MultipleActiveResultSets=true" 4 | }, 5 | "Logging": { 6 | "IncludeScopes": false, 7 | "LogLevel": { 8 | "Default": "Error", 9 | "System": "Error", 10 | "Microsoft": "Error" 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/ReactBoilerplate/Controllers/Account/Models/SendCodeModel.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | using Newtonsoft.Json; 3 | 4 | namespace ReactBoilerplate.Controllers.Account.Models 5 | { 6 | public class SendCodeModel 7 | { 8 | [Required] 9 | [JsonProperty("provider")] 10 | public string Provider { get; set; } 11 | 12 | [JsonProperty("remember")] 13 | public bool Remember { get; set; } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/ReactBoilerplate/Scripts/redux/modules/manage/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import externalLogins from './externalLogins'; 3 | import security from './security'; 4 | import email from './email'; 5 | 6 | export default combineReducers({ 7 | externalLogins, 8 | security, 9 | email 10 | }); 11 | 12 | export * from './externalLogins'; 13 | export * from './changePassword'; 14 | export * from './security'; 15 | export * from './email'; 16 | -------------------------------------------------------------------------------- /src/ReactBoilerplate/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore; 2 | using Microsoft.AspNetCore.Hosting; 3 | 4 | namespace ReactBoilerplate 5 | { 6 | public class Program 7 | { 8 | public static void Main(string[] args) 9 | { 10 | BuildWebHost(args).Run(); 11 | } 12 | 13 | public static IWebHost BuildWebHost(string[] args) => 14 | WebHost.CreateDefaultBuilder(args) 15 | .UseStartup() 16 | .Build(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/ReactBoilerplate/State/GlobalState.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace ReactBoilerplate.State 4 | { 5 | public class GlobalState 6 | { 7 | public GlobalState() 8 | { 9 | Auth = new AuthState(); 10 | ExternalLogin = new ExternalLoginState(); 11 | } 12 | 13 | [JsonProperty("auth")] 14 | public AuthState Auth { get; set; } 15 | 16 | [JsonProperty("externalLogin")] 17 | public ExternalLoginState ExternalLogin { get; set; } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/ReactBoilerplate/Controllers/Manage/Models/ExternalLoginConfirmationModel.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | using Newtonsoft.Json; 3 | 4 | namespace ReactBoilerplate.Controllers.Manage.Models 5 | { 6 | public class ExternalLoginConfirmationModel 7 | { 8 | [JsonProperty("userName")] 9 | [Required] 10 | public string UserName { get; set; } 11 | 12 | [JsonProperty("email")] 13 | [EmailAddress] 14 | [Required] 15 | public string Email { get; set; } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/ReactBoilerplate/Scripts/redux/modules/manage/changePassword.js: -------------------------------------------------------------------------------- 1 | export const CHANGEPASSWORD_START = 'react/manage/CHANGEPASSWORD_START'; 2 | export const CHANGEPASSWORD_COMPLETE = 'react/manage/CHANGEPASSWORD_COMPLETE'; 3 | export const CHANGEPASSWORD_ERROR = 'react/manage/CHANGEPASSWORD_ERROR'; 4 | 5 | export function changePassword(body) { 6 | return { 7 | types: [CHANGEPASSWORD_START, CHANGEPASSWORD_COMPLETE, CHANGEPASSWORD_ERROR], 8 | promise: (client) => client.post('/api/manage/changepassword', { data: body }) 9 | }; 10 | } 11 | -------------------------------------------------------------------------------- /src/ReactBoilerplate/Scripts/containers/ResetPassword/ResetPassword.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { ResetPasswordForm } from 'components'; 3 | import { connect } from 'react-redux'; 4 | 5 | class ResetPassword extends Component { 6 | render() { 7 | return ( 8 |
9 |

Reset password

10 |

Reset your password.

11 |
12 | 13 |
14 | ); 15 | } 16 | } 17 | 18 | export default connect( 19 | state => ({ user: state.auth.user }), 20 | { } 21 | )(ResetPassword); 22 | -------------------------------------------------------------------------------- /src/ReactBoilerplate/Controllers/Account/Models/LoginModel.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | using Newtonsoft.Json; 3 | 4 | namespace ReactBoilerplate.Controllers.Account.Models 5 | { 6 | public class LoginModel 7 | { 8 | [JsonProperty("userName")] 9 | [Required] 10 | public string UserName { get; set; } 11 | 12 | [JsonProperty("password")] 13 | [Required] 14 | public string Password { get; set; } 15 | 16 | [JsonProperty("rememberMe")] 17 | public bool RememberMe { get; set; } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/ReactBoilerplate/Scripts/components/ExternalLoginButton.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Button } from 'react-bootstrap'; 3 | 4 | const bootstrapSocial = require('bootstrap-social'); 5 | const fontAwesome = require('font-awesome/scss/font-awesome.scss'); 6 | 7 | export default (props) => 8 | ( 9 | 15 | ); 16 | -------------------------------------------------------------------------------- /src/ReactBoilerplate/Scripts/containers/ForgotPassword/ForgotPassword.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { ForgotPasswordForm } from 'components'; 3 | import { connect } from 'react-redux'; 4 | 5 | class ForgotPassword extends Component { 6 | render() { 7 | return ( 8 |
9 |

Forgot your password?

10 |

Enter your email.

11 |
12 | 13 |
14 | ); 15 | } 16 | } 17 | 18 | export default connect( 19 | state => ({ user: state.auth.user }), 20 | { } 21 | )(ForgotPassword); 22 | -------------------------------------------------------------------------------- /src/ReactBoilerplate/Scripts/containers/Manage/ChangePassword/ChangePassword.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { ChangePasswordForm } from 'components'; 3 | import { connect } from 'react-redux'; 4 | 5 | class ChangePassword extends Component { 6 | render() { 7 | return ( 8 |
9 |

Change Password.

10 |

Change Password Form

11 |
12 | 13 |
14 | ); 15 | } 16 | } 17 | 18 | export default connect( 19 | state => ({ user: state.auth.user }), 20 | { } 21 | )(ChangePassword); 22 | -------------------------------------------------------------------------------- /src/ReactBoilerplate/Scripts/redux/reducer.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import { reducer as formReducer } from 'redux-form'; 3 | import auth from './modules/auth'; 4 | import account from './modules/account'; 5 | import externalLogin from './modules/externalLogin'; 6 | import manage from './modules/manage'; 7 | import viewBag from './modules/viewBag'; 8 | import { routerReducer } from 'react-router-redux'; 9 | 10 | export default combineReducers({ 11 | form: formReducer, 12 | routing: routerReducer, 13 | auth, 14 | account, 15 | externalLogin, 16 | manage, 17 | viewBag 18 | }); 19 | -------------------------------------------------------------------------------- /src/ReactBoilerplate/web.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/ReactBoilerplate/Services/SmsSender.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Microsoft.Extensions.Logging; 3 | 4 | namespace ReactBoilerplate.Services 5 | { 6 | public class SmsSender : ISmsSender 7 | { 8 | ILogger _logger; 9 | 10 | public SmsSender(ILoggerFactory loggerFactory) 11 | { 12 | _logger = loggerFactory.CreateLogger(); 13 | } 14 | 15 | public Task SendSmsAsync(string number, string message) 16 | { 17 | _logger.LogInformation($"Send sms:\nnumber:{number}\nmessage:{message}"); 18 | return Task.FromResult(0); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/ReactBoilerplate/Controllers/Account/Models/VerifyCodeModel.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | using Newtonsoft.Json; 3 | 4 | namespace ReactBoilerplate.Controllers.Account.Models 5 | { 6 | public class VerifyCodeModel 7 | { 8 | [Required] 9 | [JsonProperty("provider")] 10 | public string Provider { get; set; } 11 | 12 | [Required] 13 | [JsonProperty("code")] 14 | public string Code { get; set; } 15 | 16 | [JsonProperty("rememberBrowser")] 17 | public bool RememberBrowser { get; set; } 18 | 19 | [JsonProperty("rememberMe")] 20 | public bool RememberMe { get; set; } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/ReactBoilerplate/Services/EmailSender.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Microsoft.Extensions.Logging; 3 | 4 | namespace ReactBoilerplate.Services 5 | { 6 | public class EmailSender : IEmailSender 7 | { 8 | ILogger _logger; 9 | 10 | public EmailSender(ILoggerFactory loggerFactory) 11 | { 12 | _logger = loggerFactory.CreateLogger(); 13 | } 14 | 15 | public Task SendEmailAsync(string email, string subject, string message) 16 | { 17 | _logger.LogInformation($"Send email:\nemail:{email}\nsubject:{subject}\nmessage:{message}"); 18 | return Task.FromResult(0); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/ReactBoilerplate/Models/Api/User.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace ReactBoilerplate.Models.Api 4 | { 5 | public class User 6 | { 7 | [JsonProperty("id")] 8 | public string Id { get; set; } 9 | 10 | [JsonProperty("userName")] 11 | public string UserName { get; set; } 12 | 13 | [JsonProperty("email")] 14 | public string Email { get; set; } 15 | 16 | public static User From(ApplicationUser user) 17 | { 18 | return new User 19 | { 20 | Id = user.Id, 21 | UserName = user.UserName, 22 | Email = user.Email 23 | }; 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/ReactBoilerplate/Scripts/utils/isEmpty.js: -------------------------------------------------------------------------------- 1 | // Speed up calls to hasOwnProperty 2 | const hasOwnProperty = Object.prototype.hasOwnProperty; 3 | 4 | export default function isEmpty(obj) { 5 | // null and undefined are "empty" 6 | if (obj === null) return true; 7 | // Assume if it has a length property with a non-zero value 8 | // that that property is correct. 9 | if (obj.length > 0) return false; 10 | if (obj.length === 0) return true; 11 | // Otherwise, does it have any properties of its own? 12 | // Note that this doesn't handle 13 | // toString and valueOf enumeration bugs in IE < 9 14 | for (const key in obj) { 15 | if (hasOwnProperty.call(obj, key)) return false; 16 | } 17 | return true; 18 | } 19 | -------------------------------------------------------------------------------- /src/ReactBoilerplate/State/ExternalLoginState.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Newtonsoft.Json; 3 | 4 | namespace ReactBoilerplate.State 5 | { 6 | public class ExternalLoginState 7 | { 8 | public ExternalLoginState() 9 | { 10 | LoginProviders = new List(); 11 | } 12 | 13 | [JsonProperty("loginProviders")] 14 | public List LoginProviders { get; set; } 15 | 16 | public class ExternalLoginProvider 17 | { 18 | [JsonProperty("scheme")] 19 | public string Scheme { get; set; } 20 | 21 | [JsonProperty("displayName")] 22 | public string DisplayName { get; set; } 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/ReactBoilerplate/Controllers/Manage/Models/ChangeEmailModel.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | using Newtonsoft.Json; 3 | 4 | namespace ReactBoilerplate.Controllers.Manage.Models 5 | { 6 | public class ChangeEmailModel 7 | { 8 | [JsonProperty("currentPassword")] 9 | [DataType(DataType.Password)] 10 | [Required] 11 | public string CurrentPassword { get; set; } 12 | 13 | [JsonProperty("email")] 14 | [EmailAddress] 15 | [Required] 16 | public string Email { get; set; } 17 | 18 | [JsonProperty("emailConfirm")] 19 | [Compare("Email", ErrorMessage = "The email and confirmation email do not match.")] 20 | public string EmailConfirm { get; set; } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/ReactBoilerplate/Scripts/containers/App/App.scss: -------------------------------------------------------------------------------- 1 | body { 2 | padding-top: 50px; 3 | padding-bottom: 20px; 4 | } 5 | 6 | /* Wrapping element */ 7 | /* Set some basic padding to keep content from hitting the edges */ 8 | .body-content { 9 | padding-left: 15px; 10 | padding-right: 15px; 11 | } 12 | 13 | /* Set widths on the form inputs since otherwise they're 100% wide */ 14 | input, 15 | select, 16 | textarea { 17 | max-width: 280px; 18 | } 19 | 20 | /* Carousel */ 21 | .carousel-caption { 22 | z-index: 10 !important; 23 | } 24 | 25 | .carousel-caption p { 26 | font-size: 20px; 27 | line-height: 1.4; 28 | } 29 | 30 | @media (min-width: 768px) { 31 | .carousel-caption { 32 | z-index: 10 !important; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/ReactBoilerplate/Controllers/Status/ServerController.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Microsoft.AspNetCore.Identity; 3 | using Microsoft.AspNetCore.Mvc; 4 | using ReactBoilerplate.Models; 5 | 6 | namespace ReactBoilerplate.Controllers.Status 7 | { 8 | public class StatusController : BaseController 9 | { 10 | public StatusController(UserManager userManager, 11 | SignInManager signInManager) 12 | :base(userManager, signInManager) 13 | { 14 | } 15 | 16 | [Route("status/status/{statusCode}")] 17 | public async Task Status(int statusCode) 18 | { 19 | return View($"js-/statuscode{statusCode}", await BuildState()); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/ReactBoilerplate/Scripts/components/ErrorList.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Glyphicon } from 'react-bootstrap'; 3 | 4 | class ErrorList extends Component { 5 | render() { 6 | const { 7 | errors 8 | } = this.props; 9 | if (!errors) return null; 10 | if (Array.isArray(errors)) { 11 | if (errors.length === 0) return null; 12 | return ( 13 |
14 | {errors.map((err, i) => 15 | ( 16 |

17 | 18 | {' '} 19 | {err} 20 |

21 | ))} 22 |
23 | ); 24 | } 25 | return null; 26 | } 27 | } 28 | 29 | export default ErrorList; 30 | -------------------------------------------------------------------------------- /src/ReactBoilerplate/Scripts/client.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { Router, browserHistory } from 'react-router'; 4 | import getRoutes from './routes'; 5 | import { Provider } from 'react-redux'; 6 | import configureStore from './redux/configureStore'; 7 | import { syncHistoryWithStore } from 'react-router-redux'; 8 | import ApiClient from './helpers/ApiClient'; 9 | 10 | const client = new ApiClient(); 11 | const store = configureStore(window.__data, browserHistory, client); 12 | const history = syncHistoryWithStore(browserHistory, store); 13 | 14 | ReactDOM.render( 15 | 16 | 17 | {getRoutes(store)} 18 | 19 | , 20 | document.getElementById('content') 21 | ); 22 | -------------------------------------------------------------------------------- /src/ReactBoilerplate/Scripts/containers/Register/Register.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { RegisterForm } from 'components'; 3 | import { connect } from 'react-redux'; 4 | import { push } from 'react-router-redux'; 5 | 6 | class Register extends Component { 7 | componentWillReceiveProps(nextProps) { 8 | if (!this.props.user && nextProps.user) { 9 | this.props.pushState('/'); 10 | } 11 | } 12 | render() { 13 | return ( 14 |
15 |

Register

16 |

Create a new account.

17 |
18 | 19 |
20 | ); 21 | } 22 | } 23 | 24 | export default connect( 25 | state => ({ user: state.auth.user, externalLogin: state.externalLogin }), 26 | { pushState: push } 27 | )(Register); 28 | -------------------------------------------------------------------------------- /src/ReactBoilerplate/Scripts/redux/configureStore.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware, compose } from 'redux'; 2 | import thunk from 'redux-thunk'; 3 | import reducer from './reducer'; 4 | import { routerMiddleware } from 'react-router-redux'; 5 | import createMiddleware from './middleware/clientMiddleware'; 6 | 7 | let devTools = f => f; 8 | if (typeof window === 'object' 9 | && typeof window.devToolsExtension !== 'undefined') { 10 | devTools = window.devToolsExtension(); 11 | } 12 | 13 | export default function configureStore(initialState, history, client) { 14 | const enhancer = compose( 15 | applyMiddleware(thunk), 16 | applyMiddleware(routerMiddleware(history)), 17 | applyMiddleware(createMiddleware(client)), 18 | devTools 19 | )(createStore); 20 | return enhancer(reducer, initialState); 21 | } 22 | -------------------------------------------------------------------------------- /src/ReactBoilerplate/Controllers/Account/Models/ResetPasswordModel.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace ReactBoilerplate.Controllers.Account.Models 4 | { 5 | public class ResetPasswordModel 6 | { 7 | [Required] 8 | [EmailAddress] 9 | public string Email { get; set; } 10 | 11 | [Required] 12 | [StringLength(100, ErrorMessage = "The {0} must be at least {2} characters long.", MinimumLength = 6)] 13 | [DataType(DataType.Password)] 14 | public string Password { get; set; } 15 | 16 | [DataType(DataType.Password)] 17 | [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")] 18 | public string PasswordConfirm { get; set; } 19 | 20 | public string Code { get; set; } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/ReactBoilerplate/Scripts/containers/index.js: -------------------------------------------------------------------------------- 1 | export App from './App/App'; 2 | export Home from './Home/Home'; 3 | export About from './About/About'; 4 | export Contact from './Contact/Contact'; 5 | export NotFound from './NotFound/NotFound'; 6 | export Register from './Register/Register'; 7 | export Login from './Login/Login'; 8 | export ForgotPassword from './ForgotPassword/ForgotPassword'; 9 | export ResetPassword from './ResetPassword/ResetPassword'; 10 | export ConfirmEmail from './ConfirmEmail/ConfirmEmail'; 11 | export Manage from './Manage/Manage'; 12 | export ManageIndex from './Manage/Index/Index'; 13 | export ManageSecurity from './Manage/Security/Security'; 14 | export ManageChangePassword from './Manage/ChangePassword/ChangePassword'; 15 | export ManageLogins from './Manage/Logins/Logins'; 16 | export ManageEmail from './Manage/Email/Email'; 17 | -------------------------------------------------------------------------------- /src/ReactBoilerplate/Models/ApplicationDbContext.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Identity.EntityFrameworkCore; 2 | using Microsoft.EntityFrameworkCore; 3 | 4 | namespace ReactBoilerplate.Models 5 | { 6 | public class ApplicationDbContext : IdentityDbContext 7 | { 8 | public ApplicationDbContext(DbContextOptions options) 9 | : base(options) 10 | { 11 | } 12 | 13 | protected override void OnModelCreating(ModelBuilder builder) 14 | { 15 | base.OnModelCreating(builder); 16 | // Customize the ASP.NET Identity model and override the defaults if needed. 17 | // For example, you can rename the ASP.NET Identity table names and more. 18 | // Add your customizations after calling base.OnModelCreating(builder); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/ReactBoilerplate/Scripts/helpers/ApiClient.js: -------------------------------------------------------------------------------- 1 | import superagent from 'superagent'; 2 | 3 | const methods = ['get', 'post', 'put', 'patch', 'del']; 4 | 5 | class _ApiClient { 6 | constructor(req) { 7 | methods.forEach((method) => { 8 | this[method] = (path, { params, data } = {}) => new Promise((resolve, reject) => { 9 | const request = superagent[method](path); 10 | 11 | if (params) { 12 | request.query(params); 13 | } 14 | 15 | if (__SERVER__ && req.get('cookie')) { 16 | request.set('cookie', req.get('cookie')); 17 | } 18 | 19 | if (data) { 20 | request.send(data); 21 | } 22 | 23 | request.end((err, { body } = {}) => (err ? reject(body || err) : resolve(body))); 24 | }); 25 | }); 26 | } 27 | } 28 | 29 | const ApiClient = _ApiClient; 30 | 31 | export default ApiClient; 32 | -------------------------------------------------------------------------------- /src/ReactBoilerplate/Controllers/Manage/Models/ChangePasswordModel.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | using Newtonsoft.Json; 3 | 4 | namespace ReactBoilerplate.Controllers.Manage.Models 5 | { 6 | public class ChangePasswordModel 7 | { 8 | [JsonProperty("oldPassword")] 9 | [Required] 10 | [DataType(DataType.Password)] 11 | public string OldPassword { get; set; } 12 | 13 | [JsonProperty("newPassword")] 14 | [Required] 15 | [StringLength(100, ErrorMessage = "The {0} must be at least {2} characters long.", MinimumLength = 6)] 16 | public string NewPassword { get; set; } 17 | 18 | [JsonProperty("newPasswordConfirm")] 19 | [Compare("NewPassword", ErrorMessage = "The new password and confirmation password do not match.")] 20 | public string NewPasswordConfirm { get; set; } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/ReactBoilerplate/Scripts/components/TwoFactor/TwoFactor.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import SendCodeForm from './SendCodeForm'; 4 | import VerifyCodeForm from './VerifyCodeForm'; 5 | import { resetLoginState } from 'redux/modules/account'; 6 | 7 | class TwoFactor extends Component { 8 | componentWillUnmount() { 9 | this.props.resetLoginState(); 10 | } 11 | render() { 12 | const { 13 | account: { requiresTwoFactor, userFactors, sentCode } 14 | } = this.props; 15 | if (sentCode) { 16 | return ( 17 | 18 | ); 19 | } 20 | if (requiresTwoFactor) { 21 | return ( 22 | 23 | ); 24 | } 25 | return (
); 26 | } 27 | } 28 | 29 | export default connect( 30 | (state) => ({ account: state.account }), 31 | { resetLoginState } 32 | )(TwoFactor); 33 | -------------------------------------------------------------------------------- /src/ReactBoilerplate/Scripts/redux/modules/viewBag.js: -------------------------------------------------------------------------------- 1 | import { LOCATION_CHANGE } from 'react-router-redux'; 2 | 3 | export default function reducer(state = { $internal: false }, action = {}) { 4 | switch (action.type) { 5 | case '_HYDRATE_VIEWBAG': 6 | // This initial data is provided by the server. 7 | // It exists for the initial request only. 8 | return { 9 | ...state, 10 | ...action.viewBag 11 | }; 12 | case LOCATION_CHANGE: 13 | // This little snippet is a hack to prevent the viewBag 14 | // from being cleared on initial page load, but destroyed 15 | // when navigating to a different page. 16 | // https://github.com/reactjs/react-router-redux/issues/340 17 | if (state.$internal) return { $internal: true }; 18 | return { 19 | ...state, 20 | $internal: true, 21 | ...action.viewBag 22 | }; 23 | default: 24 | return state; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/ReactBoilerplate/Scripts/utils/modelState.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | 3 | /* eslint "import/prefer-default-export": 0 */ 4 | /* eslint "no-prototype-builtins": 0 */ 5 | /* eslint "no-underscore-dangle": [2, {"allow": ["_global", "_error" ]}] */ 6 | 7 | // this method will map model state returned from an API, into an object 8 | // this is valid for passing to redux-forms for validation. 9 | export function modelStateErrorToFormFields(modelState) { 10 | if (!modelState) { 11 | return null; 12 | } 13 | let updatedModelState = _.omit(modelState, '_global'); 14 | updatedModelState = _.mapValues(updatedModelState, (value) => 15 | ({ 16 | errors: value 17 | }) 18 | ); 19 | if (modelState.hasOwnProperty('_global') && modelState._global.length !== 0) { 20 | updatedModelState._error = { 21 | errors: modelState._global 22 | }; 23 | } else { 24 | updatedModelState._error = null; 25 | } 26 | return updatedModelState; 27 | } 28 | -------------------------------------------------------------------------------- /src/ReactBoilerplate/Scripts/redux/middleware/clientMiddleware.js: -------------------------------------------------------------------------------- 1 | export default function clientMiddleware(client) { 2 | return ({ dispatch, getState }) => 3 | next => action => { 4 | if (typeof action === 'function') { 5 | return action(dispatch, getState); 6 | } 7 | 8 | const { promise, types, ...rest } = action; // eslint-disable-line no-use-before-define 9 | if (!promise) { 10 | return next(action); 11 | } 12 | 13 | const [REQUEST, SUCCESS, FAILURE] = types; 14 | next({ ...rest, type: REQUEST }); 15 | 16 | const actionPromise = promise(client); 17 | actionPromise.then( 18 | (result) => next({ ...rest, result, type: SUCCESS }), 19 | (error) => next({ ...rest, error, type: FAILURE }) 20 | ).catch((error) => { 21 | console.error('MIDDLEWARE ERROR:', error); 22 | console.error('MIDDLEWARE ERROR:', error.stack); 23 | next({ ...rest, error, type: FAILURE }); 24 | }); 25 | 26 | return actionPromise; 27 | }; 28 | } 29 | -------------------------------------------------------------------------------- /src/ReactBoilerplate/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:15806/", 7 | "sslPort": 0 8 | } 9 | }, 10 | "profiles": { 11 | "Development: IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": true, 14 | "environmentVariables": { 15 | "ASPNETCORE_ENVIRONMENT": "Development" 16 | } 17 | }, 18 | "Development: .NET CLI": { 19 | "commandName": "Project", 20 | "launchBrowser": true, 21 | "environmentVariables": { 22 | "ASPNETCORE_ENVIRONMENT": "Development" 23 | }, 24 | "applicationUrl": "http://localhost:15806/" 25 | }, 26 | "Production: Kestrel": { 27 | "commandName": "Kestrel", 28 | "launchBrowser": true, 29 | "environmentVariables": { 30 | "ASPNETCORE_ENVIRONMENT": "Production" 31 | }, 32 | "applicationUrl": "http://localhost:15806/" 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /src/ReactBoilerplate/Controllers/Account/Models/RegisterModel.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | using Newtonsoft.Json; 3 | 4 | namespace ReactBoilerplate.Controllers.Account.Models 5 | { 6 | public class RegisterModel 7 | { 8 | [JsonProperty("userName")] 9 | [Required] 10 | public string UserName { get; set; } 11 | 12 | [JsonProperty("email")] 13 | [EmailAddress] 14 | [Required] 15 | public string Email { get; set; } 16 | 17 | [JsonProperty("password")] 18 | [Required] 19 | [StringLength(100, ErrorMessage = "The {0} must be at least {2} characters long.", MinimumLength = 6)] 20 | public string Password { get; set; } 21 | 22 | [JsonProperty("passwordConfirm")] 23 | [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")] 24 | public string PasswordConfirm { get; set; } 25 | 26 | [JsonProperty("linkExternalLogin")] 27 | public bool LinkExternalLogin { get; set; } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/ReactBoilerplate/Scripts/components/index.js: -------------------------------------------------------------------------------- 1 | export LoginForm from './LoginForm/LoginForm'; 2 | export RegisterForm from './RegisterForm/RegisterForm'; 3 | export ForgotPasswordForm from './ForgotPasswordForm/ForgotPasswordForm'; 4 | export ResetPasswordForm from './ResetPasswordForm/ResetPasswordForm'; 5 | export ChangePasswordForm from './ChangePasswordForm/ChangePasswordForm'; 6 | export Input from './Input'; 7 | export ExternalLogin from './ExternalLogin/ExternalLogin'; 8 | export ExternalLoginButton from './ExternalLoginButton'; 9 | export Spinner from './Spinner'; 10 | export ErrorList from './ErrorList'; 11 | export TwoFactor from './TwoFactor/TwoFactor'; 12 | export ChangeEmailForm from './ChangeEmailForm/ChangeEmailForm'; 13 | // There is a bug in babel. When exporting types that will be inherited, 14 | // you must import them directly from the component. You can't proxy 15 | // them like this index.js does. 16 | // http://stackoverflow.com/questions/28551582/traceur-runtime-super-expression-must-either-be-null-or-a-function-not-undefin 17 | // export Form from './Form'; 18 | -------------------------------------------------------------------------------- /src/ReactBoilerplate/Controllers/Home/HomeController.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Microsoft.AspNetCore.Identity; 3 | using Microsoft.AspNetCore.Mvc; 4 | using ReactBoilerplate.Models; 5 | 6 | namespace ReactBoilerplate.Controllers.Home 7 | { 8 | public class HomeController : BaseController 9 | { 10 | public HomeController(UserManager userManager, 11 | SignInManager signInManager) 12 | :base(userManager, 13 | signInManager) 14 | { 15 | } 16 | 17 | public async Task Index(string greeting = "Hello!") 18 | { 19 | return View("js-{auto}", await BuildState()); 20 | } 21 | 22 | [Route("about")] 23 | public async Task About() 24 | { 25 | return View("js-{auto}", await BuildState()); 26 | } 27 | 28 | [Route("contact")] 29 | public async Task Contact() 30 | { 31 | return View("js-{auto}", await BuildState()); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/ReactBoilerplate/State/Manage/ExternalLoginsState.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Newtonsoft.Json; 3 | 4 | namespace ReactBoilerplate.State.Manage 5 | { 6 | public class ExternalLoginsState 7 | { 8 | public ExternalLoginsState() 9 | { 10 | CurrentLogins = new List(); 11 | OtherLogins = new List(); 12 | } 13 | 14 | [JsonProperty("currentLogins")] 15 | public List CurrentLogins { get; set; } 16 | 17 | [JsonProperty("otherLogins")] 18 | public List OtherLogins { get; set; } 19 | 20 | public class ExternalLogin 21 | { 22 | [JsonProperty("loginProvider")] 23 | public string LoginProvider { get; set; } 24 | 25 | [JsonProperty("loginProviderDisplayName")] 26 | public string LoginProviderDisplayName { get; set; } 27 | 28 | [JsonProperty("providerKey")] 29 | public string ProviderKey { get; set; } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Paul Knopf 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 | -------------------------------------------------------------------------------- /src/ReactBoilerplate/Scripts/containers/App/Modals/TwoFactorModal.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { Modal, Button } from 'react-bootstrap'; 4 | import { resetLoginState } from 'redux/modules/account'; 5 | import { TwoFactor } from 'components'; 6 | 7 | class TwoFactorModal extends Component { 8 | constructor(props) { 9 | super(props); 10 | this.close = this.close.bind(this); 11 | } 12 | close() { 13 | this.props.resetLoginState(); 14 | } 15 | render() { 16 | const { 17 | requiresTwoFactor 18 | } = this.props.account; 19 | return ( 20 | 21 | 22 | Security 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | ); 32 | } 33 | } 34 | 35 | export default connect( 36 | state => ({ account: state.account }), 37 | { resetLoginState } 38 | )(TwoFactorModal); 39 | -------------------------------------------------------------------------------- /src/ReactBoilerplate/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "eslint-config-airbnb", 3 | "env": { 4 | "browser": true, 5 | "node": true 6 | }, 7 | "parser": "babel-eslint", 8 | "plugins": [ 9 | "react", 10 | "import" 11 | ], 12 | "rules": { 13 | "comma-dangle": 0, // not sure why airbnb turned this on. gross! 14 | "indent": [2, 2, {"SwitchCase": 1}], 15 | "react/prefer-stateless-function": 0, 16 | "react/prop-types": 0, 17 | "react/jsx-closing-bracket-location": 0, 18 | "no-console": 0, 19 | "prefer-template": 0, 20 | "max-len": 0, 21 | "no-underscore-dangle": [2, {"allow": ["__data"]}], 22 | "global-require": 0, 23 | "no-restricted-syntax": 0, 24 | "linebreak-style": 0, 25 | "react/jsx-filename-extension": 0, 26 | "import/imports-first": 0, 27 | "no-class-assign": 0 28 | }, 29 | "settings": { 30 | "import/parser": "babel-eslint", 31 | "import/resolver": { 32 | "node": { 33 | "moduleDirectory": ["node_modules", "Scripts"] 34 | } 35 | }, 36 | "no-underscore-dangle": { 37 | "allow": ["__data"] 38 | } 39 | }, 40 | "globals": { 41 | "__CLIENT__": true, 42 | "__SERVER__": true 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/ReactBoilerplate/Scripts/components/TwoFactor/SendCodeForm.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Form from 'components/Form'; 3 | import { reduxForm } from 'redux-form'; 4 | import { Input } from 'components'; 5 | import { sendCode } from 'redux/modules/account'; 6 | 7 | class SendCodeForm extends Form { 8 | render() { 9 | const { 10 | fields: { provider }, 11 | userFactors 12 | } = this.props; 13 | return ( 14 |
15 | {this.renderGlobalErrorList()} 16 | ({ value: userFactor, display: userFactor }))} /> 17 |
18 |
19 | 20 |
21 |
22 |
23 | ); 24 | } 25 | } 26 | 27 | SendCodeForm = reduxForm({ 28 | form: 'sendCode', 29 | fields: ['provider'] 30 | }, 31 | (state) => ({ 32 | userFactors: state.account.userFactors, 33 | initialValues: { provider: (state.account.userFactors.length > 0 ? state.account.userFactors[0] : '') } 34 | }), 35 | { } 36 | )(SendCodeForm); 37 | 38 | export default SendCodeForm; 39 | -------------------------------------------------------------------------------- /src/ReactBoilerplate/Scripts/containers/Manage/Manage.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { Row, Col, Nav, NavItem } from 'react-bootstrap'; 4 | import { LinkContainer } from 'react-router-bootstrap'; 5 | 6 | class Manage extends Component { 7 | static propTypes = { 8 | children: PropTypes.object.isRequired 9 | }; 10 | render() { 11 | return ( 12 | 13 | 14 | 28 | 29 | 30 | {this.props.children} 31 | 32 | 33 | ); 34 | } 35 | } 36 | 37 | export default connect( 38 | state => ({ user: state.auth.user }), 39 | { } 40 | )(Manage); 41 | -------------------------------------------------------------------------------- /src/ReactBoilerplate/Scripts/components/ExternalLogin/ExternalLogin.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { authenticate } from 'redux/modules/externalLogin'; 4 | import { ExternalLoginButton } from 'components'; 5 | 6 | class ExternalLogin extends Component { 7 | constructor(props) { 8 | super(props); 9 | this.loginClick = this.loginClick.bind(this); 10 | } 11 | loginClick(scheme) { 12 | return (event) => { 13 | event.preventDefault(); 14 | this.props.authenticate(scheme); 15 | }; 16 | } 17 | render() { 18 | const { 19 | loginProviders 20 | } = this.props; 21 | return ( 22 |

23 | {loginProviders.map((loginProvider, i) => 24 | ( 25 | 26 | 30 | {' '} 31 | 32 | ))} 33 |

34 | ); 35 | } 36 | } 37 | 38 | export default connect( 39 | (state) => ({ loginProviders: state.externalLogin.loginProviders, location: state.routing.locationBeforeTransitions }), 40 | { authenticate } 41 | )(ExternalLogin); 42 | -------------------------------------------------------------------------------- /src/ReactBoilerplate/ReactBoilerplate.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp2.0 5 | true 6 | ReactBoilerplate 7 | Exe 8 | ReactBoilerplate 9 | 89e1d495-c355-4990-bb58-6384b3b2e6df 10 | $(AssetTargetFallback);dotnet5.6;dnxcore50;portable-net45+win8 11 | 12 | 13 | 14 | 15 | PreserveNewest 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/ReactBoilerplate/Scripts/redux/modules/auth.js: -------------------------------------------------------------------------------- 1 | import { REGISTER_COMPLETE, LOGIN_COMPLETE, LOGOFF_COMPLETE, VERIFYCODE_COMPLETE } from './account'; 2 | import { EXTERNALAUTHENTICATE_COMPLETE } from './externalLogin'; 3 | 4 | const initialState = { 5 | loggedIn: false, 6 | user: null 7 | }; 8 | 9 | export default function reducer(state = initialState, action = {}) { 10 | switch (action.type) { 11 | case REGISTER_COMPLETE: 12 | case LOGIN_COMPLETE: 13 | if (!action.result.success) { 14 | return state; 15 | } 16 | return { 17 | ...state, 18 | user: action.result.user, 19 | loggedIn: true 20 | }; 21 | case LOGOFF_COMPLETE: 22 | if (!action.result.success) { 23 | return state; 24 | } 25 | return { 26 | ...state, 27 | user: null, 28 | loggedIn: false 29 | }; 30 | case EXTERNALAUTHENTICATE_COMPLETE: 31 | if (action.result.signedIn) { 32 | return { 33 | ...state, 34 | user: action.result.user, 35 | loggedIn: true 36 | }; 37 | } 38 | return state; 39 | case VERIFYCODE_COMPLETE: 40 | if (action.result.success) { 41 | return { 42 | ...state, 43 | user: action.result.user, 44 | loggedIn: true 45 | }; 46 | } 47 | return state; 48 | default: 49 | return state; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/ReactBoilerplate/Scripts/containers/ConfirmEmail/ConfirmEmail.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { Link } from 'react-router'; 4 | 5 | class ConfirmEmail extends Component { 6 | render() { 7 | const { 8 | success, 9 | change // was this a result of a change of an email 10 | } = this.props; 11 | return ( 12 |
13 | {!success && 14 |
15 |

Error.

16 |

An error occurred while processing your request.

17 |
18 | } 19 | {success && 20 |
21 | {change && 22 |
23 |

Change email

24 |

25 | Your email was succesfully changed. 26 |

27 |
28 | } 29 | {!change && 30 |
31 |

Confirm email

32 |

33 | Thank you for confirming your email. 34 | Please Click here to Log in. 35 |

36 |
37 | } 38 |
39 | } 40 |
41 | ); 42 | } 43 | } 44 | 45 | export default connect( 46 | (state) => ({ success: state.viewBag.success, change: state.viewBag.change }), 47 | { } 48 | )(ConfirmEmail); 49 | -------------------------------------------------------------------------------- /src/ReactBoilerplate/Controllers/Manage/ServerController.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Microsoft.AspNetCore.Identity; 3 | using Microsoft.AspNetCore.Mvc; 4 | using ReactBoilerplate.Models; 5 | 6 | namespace ReactBoilerplate.Controllers.Manage 7 | { 8 | [Route("manage")] 9 | public class ServerController : BaseController 10 | { 11 | public ServerController(UserManager userManager, 12 | SignInManager signInManager) 13 | :base(userManager, 14 | signInManager) 15 | { 16 | } 17 | 18 | public async Task Index() 19 | { 20 | return View("js-{auto}", await BuildState()); 21 | } 22 | 23 | [Route("security")] 24 | public async Task Security() 25 | { 26 | return View("js-{auto}", await BuildState()); 27 | } 28 | 29 | [Route("email")] 30 | public async Task Email() 31 | { 32 | return View("js-{auto}", await BuildState()); 33 | } 34 | 35 | [Route("changepassword")] 36 | public async Task ChangePassword() 37 | { 38 | return View("js-{auto}", await BuildState()); 39 | } 40 | 41 | [Route("logins")] 42 | public async Task Logins() 43 | { 44 | return View("js-{auto}", await BuildState()); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/ReactBoilerplate/Scripts/components/Form.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { modelStateErrorToFormFields } from '../utils/modelState'; 3 | import { ErrorList } from 'components'; 4 | 5 | class Form extends Component { 6 | modifyValues(values) { 7 | return values; 8 | } 9 | handleApiSubmit(action, success, error) { 10 | const { 11 | handleSubmit 12 | } = this.props; 13 | return handleSubmit((values, dispatch) => 14 | new Promise((resolve, reject) => { 15 | dispatch(action(this.modifyValues(values))) 16 | .then( 17 | (result) => { 18 | if (result.success) { 19 | resolve(); 20 | if (success) { 21 | success(result); 22 | } 23 | } else { 24 | reject(modelStateErrorToFormFields(result.errors)); 25 | if (error) { 26 | error(result); 27 | } 28 | } 29 | }, 30 | (result) => { 31 | reject(modelStateErrorToFormFields(result.errors)); 32 | if (error) { 33 | error(result); 34 | } 35 | }); 36 | }) 37 | ); 38 | } 39 | renderGlobalErrorList() { 40 | const { 41 | error 42 | } = this.props; 43 | if (!error) { 44 | return null; 45 | } 46 | if (!error.errors) { 47 | return null; 48 | } 49 | return (); 50 | } 51 | } 52 | 53 | export default Form; 54 | -------------------------------------------------------------------------------- /src/ReactBoilerplate/Scripts/components/LoginForm/LoginForm.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Form from 'components/Form'; 3 | import { reduxForm } from 'redux-form'; 4 | import { Input, ExternalLogin } from 'components'; 5 | import { login } from 'redux/modules/account'; 6 | import { Row, Col } from 'react-bootstrap'; 7 | 8 | class LoginForm extends Form { 9 | render() { 10 | const { 11 | fields: { userName, password }, 12 | loginProviders 13 | } = this.props; 14 | return ( 15 |
16 | {this.renderGlobalErrorList()} 17 | {(loginProviders.length > 0) && 18 | 19 | 20 | 21 | 22 |

Or...

23 | 24 |
25 | } 26 | 27 | 28 |
29 |
30 | 31 |
32 |
33 |
34 | ); 35 | } 36 | } 37 | 38 | LoginForm = reduxForm({ 39 | form: 'login', 40 | fields: ['userName', 'password', 'rememberMe'] 41 | }, 42 | (state) => ({ loginProviders: state.externalLogin.loginProviders }), 43 | { } 44 | )(LoginForm); 45 | 46 | export default LoginForm; 47 | -------------------------------------------------------------------------------- /src/ReactBoilerplate/Scripts/components/TwoFactor/VerifyCodeForm.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Form from 'components/Form'; 3 | import { reduxForm } from 'redux-form'; 4 | import { Input } from 'components'; 5 | import { verifyCode } from 'redux/modules/account'; 6 | 7 | class VerifyCodeForm extends Form { 8 | modifyValues(values) { 9 | return { 10 | ...values, 11 | provider: this.props.sentCodeWithProvider 12 | }; 13 | } 14 | render() { 15 | const { 16 | fields: { code, rememberMe, rememberBrowser } 17 | } = this.props; 18 | return ( 19 |
20 | {this.renderGlobalErrorList()} 21 | 22 | 23 | 24 |
25 |
26 | 27 |
28 |
29 |
30 | ); 31 | } 32 | } 33 | 34 | VerifyCodeForm = reduxForm({ 35 | form: 'verifyCode', 36 | fields: ['code', 'rememberMe', 'rememberBrowser'] 37 | }, 38 | (state) => ({ 39 | sentCodeWithProvider: state.account.sentCodeWithProvider, 40 | initialValues: { 41 | rememberMe: true, 42 | rememberBrowser: true 43 | } 44 | }), 45 | { } 46 | )(VerifyCodeForm); 47 | 48 | export default VerifyCodeForm; 49 | -------------------------------------------------------------------------------- /src/ReactBoilerplate/Scripts/helpers/Html.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import ReactDOM from 'react-dom/server'; 3 | import Helmet from 'react-helmet'; 4 | import serialize from 'serialize-javascript'; 5 | 6 | export default class Html extends Component { 7 | static propTypes = { 8 | component: PropTypes.node, 9 | store: PropTypes.object 10 | }; 11 | 12 | render() { 13 | const { component, store } = this.props; 14 | const content = component ? ReactDOM.renderToString(component) : ''; 15 | const head = Helmet.rewind(); 16 | 17 | return ( 18 | 19 | 20 | {head.base.toComponent()} 21 | {head.title.toComponent()} 22 | {head.meta.toComponent()} 23 | {head.link.toComponent()} 24 | {head.script.toComponent()} 25 | 26 | 27 | 34 | 35 | 36 |
37 | 106 | 107 | 108 | "; 109 | }); 110 | 111 | dynamic data = new ExpandoObject(); 112 | data.externalAuthenticated = false; 113 | data.loginProvider = null; 114 | data.user = null; 115 | data.requiresTwoFactor = false; 116 | data.lockedOut = false; 117 | data.signedIn = false; 118 | data.signInError = false; 119 | data.proposedEmail = ""; 120 | data.proposedUserName = ""; 121 | 122 | var info = await _signInManager.GetExternalLoginInfoAsync(); 123 | if (info == null) 124 | // unable to authenticate with an external login 125 | return Content(callbackTemplate(data), "text/html"); 126 | 127 | var schemes = await _signInManager.GetExternalAuthenticationSchemesAsync(); 128 | if (string.IsNullOrEmpty(info.ProviderDisplayName)) 129 | { 130 | info.ProviderDisplayName = 131 | schemes 132 | .SingleOrDefault(x => x.Name.Equals(info.LoginProvider))? 133 | .DisplayName; 134 | if (string.IsNullOrEmpty(info.ProviderDisplayName)) 135 | { 136 | info.ProviderDisplayName = info.LoginProvider; 137 | } 138 | } 139 | 140 | data.loginProvider = new 141 | { 142 | scheme = info.LoginProvider, 143 | displayName = info.ProviderDisplayName 144 | }; 145 | 146 | data.externalAuthenticated = true; 147 | 148 | var email = info.Principal.FindFirstValue(ClaimTypes.Email); 149 | var userName = info.Principal.FindFirstValue(ClaimTypes.Name); 150 | if (!string.IsNullOrEmpty(userName)) 151 | userName = userName.Replace(" ", "_"); 152 | 153 | data.proposedEmail = email; 154 | data.proposedUserName = userName; 155 | 156 | // sign in the user with this external login provider if the user already has a login. 157 | if (autoLogin) 158 | { 159 | var user = await _userManager.FindByLoginAsync(info.LoginProvider, info.ProviderKey); 160 | if (user != null) 161 | { 162 | var result = 163 | await _signInManager.ExternalLoginSignInAsync(info.LoginProvider, info.ProviderKey, false); 164 | 165 | if (result.Succeeded) 166 | { 167 | data.signedIn = true; 168 | data.user = ReactBoilerplate.Models.Api.User.From(user); 169 | return Content(callbackTemplate(data), "text/html"); 170 | } 171 | 172 | data.signInError = true; 173 | 174 | if (result.RequiresTwoFactor) 175 | { 176 | data.requiresTwoFactor = true; 177 | data.userFactors = await _userManager.GetValidTwoFactorProvidersAsync(user); 178 | } 179 | if (result.IsLockedOut) 180 | data.lockedOut = true; 181 | 182 | return Content(callbackTemplate(data), "text/html"); 183 | } 184 | } 185 | 186 | return Content(callbackTemplate(data), "text/html"); 187 | } 188 | 189 | [Route("externalloginredirect")] 190 | public IActionResult ExternalLoginRedirect(string provider, bool autoLogin = true) 191 | { 192 | var properties = _signInManager.ConfigureExternalAuthenticationProperties(provider, "/externallogincallback?autoLogin=" + autoLogin); 193 | return new ChallengeResult(provider, properties); 194 | } 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /src/ReactBoilerplate/Controllers/Manage/ApiController.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using System.Threading.Tasks; 4 | using Microsoft.AspNetCore.Authorization; 5 | using Microsoft.AspNetCore.Identity; 6 | using Microsoft.AspNetCore.Mvc; 7 | using ReactBoilerplate.Controllers.Manage.Models; 8 | using ReactBoilerplate.Models; 9 | using ReactBoilerplate.Services; 10 | using ReactBoilerplate.State; 11 | using ReactBoilerplate.State.Manage; 12 | 13 | namespace ReactBoilerplate.Controllers.Manage 14 | { 15 | [Authorize] 16 | [Route("api/manage")] 17 | public class ApiController : BaseController 18 | { 19 | private readonly UserManager _userManager; 20 | private readonly SignInManager _signInManager; 21 | private readonly IEmailSender _emailSender; 22 | 23 | public ApiController(UserManager userManager, 24 | SignInManager signInManager, 25 | IEmailSender emailSender) 26 | : base(userManager, signInManager) 27 | { 28 | _userManager = userManager; 29 | _signInManager = signInManager; 30 | _emailSender = emailSender; 31 | } 32 | 33 | [Route("security")] 34 | public async Task Security() 35 | { 36 | var user = await GetCurrentUserAsync(); 37 | 38 | return new 39 | { 40 | twoFactorEnabled = await _userManager.GetTwoFactorEnabledAsync(user), 41 | validTwoFactorProviders = await _userManager.GetValidTwoFactorProvidersAsync(user), 42 | emailConfirmed = await _userManager.IsEmailConfirmedAsync(user) 43 | }; 44 | } 45 | 46 | [Route("settwofactor")] 47 | public async Task SetTwoFactor([FromBody]SetTwoFactorModel model) 48 | { 49 | var user = await GetCurrentUserAsync(); 50 | 51 | if (await _userManager.GetTwoFactorEnabledAsync(user) == model.Enabled) 52 | { 53 | // already set 54 | return new 55 | { 56 | success = true 57 | }; 58 | } 59 | 60 | await _userManager.SetTwoFactorEnabledAsync(user, model.Enabled); 61 | 62 | return new 63 | { 64 | twoFactorEnabled = await _userManager.GetTwoFactorEnabledAsync(user), 65 | success = true 66 | }; 67 | } 68 | 69 | [Route("email")] 70 | public async Task Email() 71 | { 72 | var user = await GetCurrentUserAsync(); 73 | 74 | return new 75 | { 76 | email = await _userManager.GetEmailAsync(user), 77 | emailConfirmed = await _userManager.IsEmailConfirmedAsync(user) 78 | }; 79 | } 80 | 81 | [Route("changeemail")] 82 | public async Task ChangeEmail([FromBody]ChangeEmailModel model) 83 | { 84 | if (!ModelState.IsValid) 85 | { 86 | return new 87 | { 88 | success = false, 89 | errors = GetModelState() 90 | }; 91 | } 92 | 93 | var user = await GetCurrentUserAsync(); 94 | 95 | if (!await _userManager.CheckPasswordAsync(user, model.CurrentPassword)) 96 | { 97 | ModelState.AddModelError("currentPassword", "Invalid password."); 98 | return new 99 | { 100 | success = false, 101 | errors = GetModelState() 102 | }; 103 | } 104 | 105 | // send an email to the user asking them to finish the change of email. 106 | var code = await _userManager.GenerateChangeEmailTokenAsync(user, model.Email); 107 | var callbackUrl = Url.RouteUrl("confirmemail", new { userId = user.Id, newEmail = model.Email, code = code }, protocol: HttpContext.Request.Scheme); 108 | await _emailSender.SendEmailAsync(model.Email, "Confirm your email change", "Please confirm your new email by clicking this link: link"); 109 | 110 | return new 111 | { 112 | success = true 113 | }; 114 | } 115 | 116 | [Route("verifyemail")] 117 | public async Task VerifyEmail() 118 | { 119 | var user = await GetCurrentUserAsync(); 120 | 121 | // send an email to the user asking them to finish the change of email. 122 | var code = await _userManager.GenerateEmailConfirmationTokenAsync(user); 123 | var callbackUrl = Url.RouteUrl("confirmemail", new { userId = user.Id, code = code }, protocol: HttpContext.Request.Scheme); 124 | await _emailSender.SendEmailAsync(user.Email, "Confirm your email change", "Please confirm your new email by clicking this link: link"); 125 | 126 | return new 127 | { 128 | success = true 129 | }; 130 | } 131 | 132 | [Route("changepassword")] 133 | public async Task ChangePassword([FromBody]ChangePasswordModel model) 134 | { 135 | if (!ModelState.IsValid) 136 | { 137 | return new 138 | { 139 | success = false, 140 | errors = GetModelState() 141 | }; 142 | } 143 | var user = await GetCurrentUserAsync(); 144 | var result = await _userManager.ChangePasswordAsync(user, model.OldPassword, model.NewPassword); 145 | if (result.Succeeded) 146 | { 147 | await _signInManager.SignInAsync(user, isPersistent: false); 148 | return new 149 | { 150 | success = true 151 | }; 152 | } 153 | foreach (var error in result.Errors) 154 | { 155 | ModelState.AddModelError(string.Empty, error.Description); 156 | } 157 | return new 158 | { 159 | success = false, 160 | errors = GetModelState() 161 | }; 162 | } 163 | 164 | [Route("externallogins")] 165 | public async Task ExternalLogins() 166 | { 167 | return new 168 | { 169 | success = true, 170 | externalLogins = await GetExternalLoginsState() 171 | }; 172 | } 173 | 174 | [Route("addexternallogin")] 175 | public async Task AddExternalLogin() 176 | { 177 | var user = await GetCurrentUserAsync(); 178 | var info = await _signInManager.GetExternalLoginInfoAsync(); 179 | if (info == null) 180 | { 181 | return new 182 | { 183 | success = false, 184 | externalLogins = await GetExternalLoginsState(), 185 | errors = new List { "Enable to authenticate with service." } 186 | }; 187 | } 188 | var result = await _userManager.AddLoginAsync(user, info); 189 | 190 | if (result.Succeeded) 191 | { 192 | return new 193 | { 194 | success = true, 195 | externalLogins = await GetExternalLoginsState() 196 | }; 197 | } 198 | 199 | return new 200 | { 201 | success = false, 202 | externalLogins = await GetExternalLoginsState(), 203 | errors = result.Errors.Select(x => x.Description) 204 | }; 205 | } 206 | 207 | [Route("removeexternallogin")] 208 | public async Task RemoveExternalLogin([FromBody]RemoveExternalLoginModel model) 209 | { 210 | var user = await GetCurrentUserAsync(); 211 | var result = await _userManager.RemoveLoginAsync(user, model.LoginProvider, model.ProviderKey); 212 | if (result.Succeeded) 213 | { 214 | await _signInManager.SignInAsync(user, isPersistent: false); 215 | return new 216 | { 217 | success = true, 218 | externalLogins = await GetExternalLoginsState() 219 | }; 220 | } 221 | return new 222 | { 223 | success = false, 224 | error = result.Errors.Select(x => x.Description) 225 | }; 226 | } 227 | 228 | private async Task GetExternalLoginsState() 229 | { 230 | var user = await GetCurrentUserAsync(); 231 | var userLogins = await _userManager.GetLoginsAsync(user); 232 | var schemes = await _signInManager.GetExternalAuthenticationSchemesAsync(); 233 | foreach (var userLogin in userLogins.Where(userLogin => string.IsNullOrEmpty(userLogin.ProviderDisplayName))) 234 | { 235 | userLogin.ProviderDisplayName = 236 | schemes 237 | .SingleOrDefault(x => x.Name.Equals(userLogin.LoginProvider))? 238 | .DisplayName; 239 | if (string.IsNullOrEmpty(userLogin.ProviderDisplayName)) 240 | { 241 | userLogin.ProviderDisplayName = userLogin.LoginProvider; 242 | } 243 | } 244 | var otherLogins = schemes.Where(auth => userLogins.All(ul => auth.Name != ul.LoginProvider)).ToList(); 245 | 246 | return new ExternalLoginsState 247 | { 248 | CurrentLogins = userLogins.Select(x => new ExternalLoginsState.ExternalLogin 249 | { 250 | ProviderKey = x.ProviderKey, 251 | LoginProvider = x.LoginProvider, 252 | LoginProviderDisplayName = x.ProviderDisplayName 253 | }).ToList(), 254 | OtherLogins = otherLogins.Select(x => new ExternalLoginState.ExternalLoginProvider 255 | { 256 | DisplayName = x.DisplayName, 257 | Scheme = x.Name 258 | }).ToList() 259 | }; 260 | } 261 | } 262 | } 263 | -------------------------------------------------------------------------------- /src/ReactBoilerplate/Controllers/Account/ApiController.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Microsoft.AspNetCore.Identity; 3 | using Microsoft.AspNetCore.Mvc; 4 | using ReactBoilerplate.Controllers.Account.Models; 5 | using ReactBoilerplate.Models; 6 | using ReactBoilerplate.Services; 7 | 8 | namespace ReactBoilerplate.Controllers.Account 9 | { 10 | [Route("api/account")] 11 | public class ApiController : BaseController 12 | { 13 | UserManager _userManager; 14 | SignInManager _signInManager; 15 | IEmailSender _emailSender; 16 | private readonly ISmsSender _smsSender; 17 | 18 | public ApiController(UserManager userManager, 19 | SignInManager signInManager, 20 | IEmailSender emailSender, 21 | ISmsSender smsSender 22 | ) 23 | :base(userManager, signInManager) 24 | { 25 | _userManager = userManager; 26 | _signInManager = signInManager; 27 | _emailSender = emailSender; 28 | _smsSender = smsSender; 29 | } 30 | 31 | [Route("register")] 32 | [HttpPost] 33 | public async Task Register([FromBody]RegisterModel model) 34 | { 35 | ExternalLoginInfo externalLoginInfo = null; 36 | if (model.LinkExternalLogin) 37 | { 38 | externalLoginInfo = await _signInManager.GetExternalLoginInfoAsync(); 39 | if (externalLoginInfo == null) 40 | { 41 | ModelState.AddModelError(string.Empty, "Unsuccessful login with service"); 42 | } 43 | else 44 | { 45 | var existingLogin = await _userManager.FindByLoginAsync(externalLoginInfo.LoginProvider, externalLoginInfo.ProviderKey); 46 | if (existingLogin != null) 47 | { 48 | ModelState.AddModelError(string.Empty, "An account is already associated with this login server."); 49 | } 50 | } 51 | } 52 | 53 | if (ModelState.IsValid) 54 | { 55 | var user = new ApplicationUser { UserName = model.UserName, Email = model.Email }; 56 | var result = await _userManager.CreateAsync(user, model.Password); 57 | 58 | if (result.Succeeded) 59 | { 60 | var code = await _userManager.GenerateEmailConfirmationTokenAsync(user); 61 | var callbackUrl = Url.RouteUrl("confirmemail", new { userId = user.Id, code = code }, protocol: HttpContext.Request.Scheme); 62 | await _emailSender.SendEmailAsync(model.Email, "Confirm your account", "Please confirm your account by clicking this link: link"); 63 | 64 | // add the external login to the account 65 | if (externalLoginInfo != null) 66 | { 67 | var addLoginResult = await _userManager.AddLoginAsync(user, externalLoginInfo); 68 | if (!addLoginResult.Succeeded) 69 | { 70 | foreach (var error in addLoginResult.Errors) 71 | { 72 | // TODO: log 73 | } 74 | } 75 | } 76 | 77 | await _signInManager.SignInAsync(user, false); 78 | return new 79 | { 80 | success = true, 81 | user = ReactBoilerplate.Models.Api.User.From(user) 82 | }; 83 | } 84 | foreach (var error in result.Errors) 85 | { 86 | ModelState.AddModelError(string.Empty, error.Description); 87 | } 88 | } 89 | 90 | return new 91 | { 92 | success = false, 93 | errors = GetModelState() 94 | }; 95 | } 96 | 97 | [Route("login")] 98 | public async Task Login([FromBody]LoginModel model) 99 | { 100 | if (ModelState.IsValid) 101 | { 102 | var user = await _userManager.FindByNameAsync(model.UserName); 103 | if (user == null) 104 | { 105 | ModelState.AddModelError("UserName", "No user found with the given user name."); 106 | return new 107 | { 108 | success = false, 109 | errors = GetModelState() 110 | }; 111 | } 112 | 113 | var result = await _signInManager.PasswordSignInAsync(user, model.Password, model.RememberMe, lockoutOnFailure: false); 114 | if (result.Succeeded) 115 | { 116 | return new 117 | { 118 | success = true, 119 | user = ReactBoilerplate.Models.Api.User.From(user) 120 | }; 121 | } 122 | if (result.RequiresTwoFactor) 123 | { 124 | var userFactors = await _userManager.GetValidTwoFactorProvidersAsync(user); 125 | return new 126 | { 127 | success = false, 128 | requiresTwoFactor = true, 129 | userFactors 130 | }; 131 | } 132 | if (result.IsLockedOut) 133 | { 134 | ModelState.AddModelError(string.Empty, "You are currently locked out."); 135 | return new 136 | { 137 | success = false, 138 | errors = GetModelState() 139 | }; 140 | } 141 | 142 | ModelState.AddModelError(string.Empty, "Invalid login attempt."); 143 | return new 144 | { 145 | success = false, 146 | errors = GetModelState() 147 | }; 148 | } 149 | 150 | return new 151 | { 152 | success = false, 153 | errors = GetModelState() 154 | }; 155 | } 156 | 157 | [Route("logoff")] 158 | [HttpPost] 159 | public async Task LogOff() 160 | { 161 | await _signInManager.SignOutAsync(); 162 | return new 163 | { 164 | success = true 165 | }; 166 | } 167 | 168 | [Route("forgotpassword")] 169 | [HttpPost] 170 | public async Task ForgotPassword([FromBody]ForgotPasswordModel model) 171 | { 172 | if (ModelState.IsValid) 173 | { 174 | var user = await _userManager.FindByEmailAsync(model.Email); 175 | if (user == null || !(await _userManager.IsEmailConfirmedAsync(user))) 176 | { 177 | // Don't reveal that the user does not exist or is not confirmed 178 | return new 179 | { 180 | success = true 181 | }; 182 | } 183 | 184 | var code = await _userManager.GeneratePasswordResetTokenAsync(user); 185 | var callbackUrl = Url.RouteUrl("resetpassword", new { userId = user.Id, code = code }, protocol: HttpContext.Request.Scheme); 186 | await _emailSender.SendEmailAsync(model.Email, "Reset Password", 187 | "Please reset your password by clicking here: link"); 188 | 189 | return new 190 | { 191 | success = true 192 | }; 193 | } 194 | 195 | return new 196 | { 197 | success = false, 198 | errors = GetModelState() 199 | }; 200 | } 201 | 202 | [Route("resetpassword")] 203 | [HttpPost] 204 | public async Task ResetPassword([FromBody]ResetPasswordModel model) 205 | { 206 | if (!ModelState.IsValid) 207 | { 208 | return new 209 | { 210 | success = false, 211 | errors = GetModelState() 212 | }; 213 | } 214 | var user = await _userManager.FindByEmailAsync(model.Email); 215 | if (user == null) 216 | { 217 | // Don't reveal that the user does not exist 218 | return new 219 | { 220 | success = true 221 | }; 222 | } 223 | var result = await _userManager.ResetPasswordAsync(user, model.Code, model.Password); 224 | if (result.Succeeded) 225 | { 226 | return new 227 | { 228 | success = true 229 | }; 230 | } 231 | 232 | foreach (var error in result.Errors) 233 | { 234 | ModelState.AddModelError(string.Empty, error.Description); 235 | } 236 | 237 | return new 238 | { 239 | success = false, 240 | errors = GetModelState() 241 | }; 242 | } 243 | 244 | [Route("sendcode")] 245 | [HttpPost] 246 | public async Task SendCode([FromBody]SendCodeModel model) 247 | { 248 | if (!ModelState.IsValid) 249 | { 250 | return new 251 | { 252 | success = false, 253 | errors = GetModelState() 254 | }; 255 | } 256 | 257 | var user = await _signInManager.GetTwoFactorAuthenticationUserAsync(); 258 | if (user == null) 259 | { 260 | ModelState.AddModelError(string.Empty, "Invalid user."); 261 | return new 262 | { 263 | success = false, 264 | errors = GetModelState() 265 | }; 266 | } 267 | 268 | var code = await _userManager.GenerateTwoFactorTokenAsync(user, model.Provider); 269 | if (string.IsNullOrEmpty(code)) 270 | { 271 | ModelState.AddModelError(string.Empty, "Unknown error."); 272 | return new 273 | { 274 | success = false, 275 | errors = GetModelState() 276 | }; 277 | } 278 | 279 | var message = "Your security code is: " + code; 280 | if (model.Provider == "Email") 281 | { 282 | await _emailSender.SendEmailAsync(await _userManager.GetEmailAsync(user), "Security Code", message); 283 | } 284 | else if (model.Provider == "Phone") 285 | { 286 | await _smsSender.SendSmsAsync(await _userManager.GetPhoneNumberAsync(user), message); 287 | } 288 | 289 | return new 290 | { 291 | provider = model.Provider, 292 | success = true 293 | }; 294 | } 295 | 296 | [Route("verifycode")] 297 | public async Task VerifyCode([FromBody]VerifyCodeModel model) 298 | { 299 | if (!ModelState.IsValid) 300 | { 301 | return new 302 | { 303 | success = false, 304 | errors = GetModelState() 305 | }; 306 | } 307 | 308 | var user = await _signInManager.GetTwoFactorAuthenticationUserAsync(); 309 | if (user == null) 310 | { 311 | ModelState.AddModelError(string.Empty, "Invalid user."); 312 | return new 313 | { 314 | success = false, 315 | errors = GetModelState() 316 | }; 317 | } 318 | 319 | var result = await _signInManager.TwoFactorSignInAsync(model.Provider, model.Code, model.RememberMe, model.RememberBrowser); 320 | 321 | if (result.Succeeded) 322 | { 323 | return new 324 | { 325 | success = true, 326 | user = ReactBoilerplate.Models.Api.User.From(user) 327 | }; 328 | } 329 | 330 | ModelState.AddModelError(string.Empty, result.IsLockedOut ? "User account locked out." : "Invalid code."); 331 | 332 | return new 333 | { 334 | success = false, 335 | errors = GetModelState() 336 | }; 337 | } 338 | } 339 | } 340 | --------------------------------------------------------------------------------