├── .gitignore ├── LICENSE ├── README.md ├── ReactBoilerplate.sln ├── resources ├── nutshell.gif └── preview-thumbnail.jpg └── src └── ReactBoilerplate ├── .babelrc ├── .bootstraprc ├── .editorconfig ├── .eslintrc ├── Controllers ├── Account │ ├── ApiController.cs │ ├── Models │ │ ├── ForgotPasswordModel.cs │ │ ├── LoginModel.cs │ │ ├── RegisterModel.cs │ │ ├── ResetPasswordModel.cs │ │ ├── SendCodeModel.cs │ │ └── VerifyCodeModel.cs │ └── ServerController.cs ├── BaseController.cs ├── Home │ └── HomeController.cs ├── Manage │ ├── ApiController.cs │ ├── Models │ │ ├── ChangeEmailModel.cs │ │ ├── ChangePasswordModel.cs │ │ ├── ExternalLoginConfirmationModel.cs │ │ ├── RemoveExternalLoginModel.cs │ │ └── SetTwoFactorModel.cs │ └── ServerController.cs └── Status │ └── ServerController.cs ├── Models ├── Api │ └── User.cs ├── ApplicationDbContext.cs └── ApplicationUser.cs ├── Program.cs ├── Properties └── launchSettings.json ├── React.sublime-project ├── ReactBoilerplate.csproj ├── Scripts ├── client.js ├── components │ ├── ChangeEmailForm │ │ └── ChangeEmailForm.js │ ├── ChangePasswordForm │ │ └── ChangePasswordForm.js │ ├── ErrorList.js │ ├── ExternalLogin │ │ └── ExternalLogin.js │ ├── ExternalLoginButton.js │ ├── ForgotPasswordForm │ │ └── ForgotPasswordForm.js │ ├── Form.js │ ├── Input.js │ ├── LoginForm │ │ └── LoginForm.js │ ├── RegisterForm │ │ └── RegisterForm.js │ ├── ResetPasswordForm │ │ └── ResetPasswordForm.js │ ├── Spinner.js │ ├── TwoFactor │ │ ├── SendCodeForm.js │ │ ├── TwoFactor.js │ │ └── VerifyCodeForm.js │ └── index.js ├── config.js ├── containers │ ├── About │ │ └── About.js │ ├── App │ │ ├── App.js │ │ ├── App.scss │ │ └── Modals │ │ │ └── TwoFactorModal.js │ ├── ConfirmEmail │ │ └── ConfirmEmail.js │ ├── Contact │ │ └── Contact.js │ ├── ForgotPassword │ │ └── ForgotPassword.js │ ├── Home │ │ ├── ASP-NET-Banners-01.png │ │ ├── ASP-NET-Banners-02.png │ │ ├── Banner-01-Azure.png │ │ ├── Banner-02-VS.png │ │ ├── Home.js │ │ └── logo.png │ ├── Login │ │ └── Login.js │ ├── Manage │ │ ├── ChangePassword │ │ │ └── ChangePassword.js │ │ ├── Email │ │ │ └── Email.js │ │ ├── Index │ │ │ └── Index.js │ │ ├── Logins │ │ │ └── Logins.js │ │ ├── Manage.js │ │ └── Security │ │ │ └── Security.js │ ├── NotFound │ │ └── NotFound.js │ ├── Register │ │ └── Register.js │ ├── ResetPassword │ │ └── ResetPassword.js │ ├── SignIn │ │ └── SignIn.js │ └── index.js ├── helpers │ ├── ApiClient.js │ └── Html.js ├── redux │ ├── configureStore.js │ ├── middleware │ │ └── clientMiddleware.js │ ├── modules │ │ ├── account.js │ │ ├── auth.js │ │ ├── externalLogin.js │ │ ├── manage │ │ │ ├── changePassword.js │ │ │ ├── email.js │ │ │ ├── externalLogins.js │ │ │ ├── index.js │ │ │ └── security.js │ │ └── viewBag.js │ └── reducer.js ├── routes.js ├── server.js ├── utils │ ├── isEmpty.js │ ├── modelState.js │ ├── promise-window-server.js │ └── superagent-server.js └── webpack │ ├── dev.config.js │ └── prod.config.js ├── Services ├── EmailSender.cs ├── IEmailSender.cs ├── ISmsSender.cs └── SmsSender.cs ├── Startup.cs ├── State ├── AuthState.cs ├── ExternalLoginState.cs ├── GlobalState.cs └── Manage │ └── ExternalLoginsState.cs ├── appsettings.json ├── gulpfile.js ├── package-lock.json ├── package.json ├── web.config └── wwwroot └── favicon.ico /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | x64/ 19 | x86/ 20 | bld/ 21 | [Bb]in/ 22 | [Oo]bj/ 23 | 24 | # Visual Studio 2015 cache/options directory 25 | .vs/ 26 | # Uncomment if you have tasks that create the project's static files in wwwroot 27 | #wwwroot/ 28 | 29 | # MSTest test Results 30 | [Tt]est[Rr]esult*/ 31 | [Bb]uild[Ll]og.* 32 | 33 | # NUNIT 34 | *.VisualState.xml 35 | TestResult.xml 36 | 37 | # Build Results of an ATL Project 38 | [Dd]ebugPS/ 39 | [Rr]eleasePS/ 40 | dlldata.c 41 | 42 | # DNX 43 | project.lock.json 44 | artifacts/ 45 | 46 | *_i.c 47 | *_p.c 48 | *_i.h 49 | *.ilk 50 | *.meta 51 | *.obj 52 | *.pch 53 | *.pdb 54 | *.pgc 55 | *.pgd 56 | *.rsp 57 | *.sbr 58 | *.tlb 59 | *.tli 60 | *.tlh 61 | *.tmp 62 | *.tmp_proj 63 | *.log 64 | *.vspscc 65 | *.vssscc 66 | .builds 67 | *.pidb 68 | *.svclog 69 | *.scc 70 | 71 | # Chutzpah Test files 72 | _Chutzpah* 73 | 74 | # Visual C++ cache files 75 | ipch/ 76 | *.aps 77 | *.ncb 78 | *.opendb 79 | *.opensdf 80 | *.sdf 81 | *.cachefile 82 | 83 | # Visual Studio profiler 84 | *.psess 85 | *.vsp 86 | *.vspx 87 | *.sap 88 | 89 | # TFS 2012 Local Workspace 90 | $tf/ 91 | 92 | # Guidance Automation Toolkit 93 | *.gpState 94 | 95 | # ReSharper is a .NET coding add-in 96 | _ReSharper*/ 97 | *.[Rr]e[Ss]harper 98 | *.DotSettings.user 99 | 100 | # JustCode is a .NET coding add-in 101 | .JustCode 102 | 103 | # TeamCity is a build add-in 104 | _TeamCity* 105 | 106 | # DotCover is a Code Coverage Tool 107 | *.dotCover 108 | 109 | # NCrunch 110 | _NCrunch_* 111 | .*crunch*.local.xml 112 | nCrunchTemp_* 113 | 114 | # MightyMoose 115 | *.mm.* 116 | AutoTest.Net/ 117 | 118 | # Web workbench (sass) 119 | .sass-cache/ 120 | 121 | # Installshield output folder 122 | [Ee]xpress/ 123 | 124 | # DocProject is a documentation generator add-in 125 | DocProject/buildhelp/ 126 | DocProject/Help/*.HxT 127 | DocProject/Help/*.HxC 128 | DocProject/Help/*.hhc 129 | DocProject/Help/*.hhk 130 | DocProject/Help/*.hhp 131 | DocProject/Help/Html2 132 | DocProject/Help/html 133 | 134 | # Click-Once directory 135 | publish/ 136 | 137 | # Publish Web Output 138 | *.[Pp]ublish.xml 139 | *.azurePubxml 140 | # TODO: Comment the next line if you want to checkin your web deploy settings 141 | # but database connection strings (with potential passwords) will be unencrypted 142 | *.pubxml 143 | *.publishproj 144 | 145 | # NuGet Packages 146 | *.nupkg 147 | # The packages folder can be ignored because of Package Restore 148 | **/packages/* 149 | # except build/, which is used as an MSBuild target. 150 | !**/packages/build/ 151 | # Uncomment if necessary however generally it will be regenerated when needed 152 | #!**/packages/repositories.config 153 | # NuGet v3's project.json files produces more ignoreable files 154 | *.nuget.props 155 | *.nuget.targets 156 | 157 | # Microsoft Azure Build Output 158 | csx/ 159 | *.build.csdef 160 | 161 | # Microsoft Azure Emulator 162 | ecf/ 163 | rcf/ 164 | 165 | # Microsoft Azure ApplicationInsights config file 166 | ApplicationInsights.config 167 | 168 | # Windows Store app package directory 169 | AppPackages/ 170 | BundleArtifacts/ 171 | 172 | # Visual Studio cache files 173 | # files ending in .cache can be ignored 174 | *.[Cc]ache 175 | # but keep track of directories ending in .cache 176 | !*.[Cc]ache/ 177 | 178 | # Others 179 | ClientBin/ 180 | ~$* 181 | *~ 182 | *.dbmdl 183 | *.dbproj.schemaview 184 | *.pfx 185 | *.publishsettings 186 | node_modules/ 187 | orleans.codegen.cs 188 | 189 | # RIA/Silverlight projects 190 | Generated_Code/ 191 | 192 | # Backup & report files from converting an old project file 193 | # to a newer Visual Studio version. Backup files are not needed, 194 | # because we have git ;-) 195 | _UpgradeReport_Files/ 196 | Backup*/ 197 | UpgradeLog*.XML 198 | UpgradeLog*.htm 199 | 200 | # SQL Server files 201 | *.mdf 202 | *.ldf 203 | 204 | # Business Intelligence projects 205 | *.rdl.data 206 | *.bim.layout 207 | *.bim_*.settings 208 | 209 | # Microsoft Fakes 210 | FakesAssemblies/ 211 | 212 | # GhostDoc plugin setting file 213 | *.GhostDoc.xml 214 | 215 | # Node.js Tools for Visual Studio 216 | .ntvs_analysis.dat 217 | 218 | # Visual Studio 6 build log 219 | *.plg 220 | 221 | # Visual Studio 6 workspace options file 222 | *.opt 223 | 224 | # Visual Studio LightSwitch build output 225 | **/*.HTMLClient/GeneratedArtifacts 226 | **/*.DesktopClient/GeneratedArtifacts 227 | **/*.DesktopClient/ModelManifest.xml 228 | **/*.Server/GeneratedArtifacts 229 | **/*.Server/ModelManifest.xml 230 | _Pvt_Extensions 231 | 232 | # Paket dependency manager 233 | .paket/paket.exe 234 | 235 | # FAKE - F# Make 236 | .fake/ 237 | src/ReactBoilerplate/bower_components/ 238 | src/ReactBoilerplate/wwwroot/ 239 | src/ReactBoilerplate/React.sublime-workspace 240 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-aspnet-boilerplate 2 |

3 | 4 |

5 | 6 | A starting point for building universal/isomorphic React applications with ASP.NET Core 1, leveraging existing front-end approaches. Uses the [JavaScriptViewEngine](https://github.com/pauldotknopf/javascriptviewengine). 7 | 8 | ## Goals 9 | 10 | 1. **Minimize .NET's usage** - It's only usage should be for building REST endpoints (WebApi) and providing the initial state (pure POCO). No razor syntax *anywhere*. 11 | 2. **Isomorphic/universal rendering** 12 | 3. **Client and server should render using the same source files (javascript)** 13 | 4. **Out-of-the-box login/register/manage functionality** - Use the branch ```empty-template``` if you wish to have a vanilla React application. 14 | 15 | This approach is great for front-end developers because it gives them complete control to build their app as they like. No .NET crutches (bundling/razor). No opinions. No gotchas. Just another typical React client-side application, but with the initial state provided by ASP.NET for each URL. 16 | 17 | ## Getting started 18 | 19 | The best way to get started with this project is to use the Yeoman generator. 20 | 21 | ```bash 22 | npm install -g yo 23 | npm install -g generator-react-aspnet-boilerplate 24 | ``` 25 | 26 | Then generate your new project: 27 | 28 | ``` 29 | yo react-aspnet-boilerplate 30 | ``` 31 | 32 | You can also generate a clean template (no authentication/account management) with another generator: 33 | 34 | ```bash 35 | yo react-aspnet-boilerplate:empty-template 36 | ``` 37 | 38 | After you have your new project generated, let's run that app! 39 | 40 | ```bash 41 | cd src/ReactBoilerplate 42 | npm install 43 | gulp 44 | dotnet restore 45 | # The following two lines are only for the 'master' branch, which has a database backend (user management). 46 | # They are not needed when using 'empty-template'. 47 | dotnet ef migrations add initial 48 | dotnet ef database update 49 | dotnet run 50 | ``` 51 | 52 | 53 | Some of the branches in this repo that are maintained: 54 | * [```master```](https://github.com/pauldotknopf/react-aspnet-boilerplate/tree/master) - This is the main branch. It has all the stuff required to get you started, including membership, external logins (OAuth) and account management. This is the default branch used with the Yeoman generator. 55 | * [```empty-template```](https://github.com/pauldotknopf/react-aspnet-boilerplate/tree/empty-template) - This branch for people that want an empty template with the absolute minimum recommend boilerplate for any ASP.NET React application. 56 | 57 | ## The interesting parts 58 | 59 | - [client.js](https://github.com/pauldotknopf/react-dot-net/blob/master/src/ReactBoilerplate/Scripts/client.js) and [server.js](https://github.com/pauldotknopf/react-dot-net/blob/master/src/ReactBoilerplate/Scripts/server.js) - The entry point for the client-side/server-side applications. 60 | - [Html.js](https://github.com/pauldotknopf/react-dot-net/blob/master/src/ReactBoilerplate/Scripts/helpers/Html.js) and [App.js](https://github.com/pauldotknopf/react-dot-net/blob/master/src/ReactBoilerplate/Scripts/containers/App/App.js) - These files essentially represent the "React" version of MVC Razor's "_Layout.cshtml". 61 | - [Controllers](https://github.com/pauldotknopf/react-aspnet-boilerplate/tree/master/src/ReactBoilerplate/Controllers) - The endpoints for a each initial GET request, and each client-side network request. 62 | 63 | ## What is next? 64 | 65 | I will be adding features to this project as time goes on to help me get started with new React projects in .NET. So, expect some more things. I am also open to contributions or recommendations. 66 | 67 | I took a lot of things from [react-redux-universal-hot-example](https://github.com/erikras/react-redux-universal-hot-example), but not everything. As time goes on, expect to see more of the same patterns/technologies/techniques copied over. 68 | -------------------------------------------------------------------------------- /ReactBoilerplate.sln: -------------------------------------------------------------------------------- 1 | Microsoft Visual Studio Solution File, Format Version 12.00 2 | # Visual Studio 15 3 | VisualStudioVersion = 15.0.27428.2005 4 | MinimumVisualStudioVersion = 10.0.40219.1 5 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{C23ABA50-98BF-4132-B6F6-43605AD05B0F}" 6 | EndProject 7 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{BCDEFAE7-02D5-49F6-9054-511C7B472A27}" 8 | EndProject 9 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReactBoilerplate", "src\ReactBoilerplate\ReactBoilerplate.csproj", "{A9424E07-13A1-49AF-BE65-EBCE6B4F343A}" 10 | EndProject 11 | Global 12 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 13 | Debug|Any CPU = Debug|Any CPU 14 | Release|Any CPU = Release|Any CPU 15 | EndGlobalSection 16 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 17 | {A9424E07-13A1-49AF-BE65-EBCE6B4F343A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 18 | {A9424E07-13A1-49AF-BE65-EBCE6B4F343A}.Debug|Any CPU.Build.0 = Debug|Any CPU 19 | {A9424E07-13A1-49AF-BE65-EBCE6B4F343A}.Release|Any CPU.ActiveCfg = Release|Any CPU 20 | {A9424E07-13A1-49AF-BE65-EBCE6B4F343A}.Release|Any CPU.Build.0 = Release|Any CPU 21 | EndGlobalSection 22 | GlobalSection(SolutionProperties) = preSolution 23 | HideSolutionNode = FALSE 24 | EndGlobalSection 25 | GlobalSection(NestedProjects) = preSolution 26 | {A9424E07-13A1-49AF-BE65-EBCE6B4F343A} = {C23ABA50-98BF-4132-B6F6-43605AD05B0F} 27 | EndGlobalSection 28 | GlobalSection(ExtensibilityGlobals) = postSolution 29 | SolutionGuid = {ABA2BABF-36F7-43D0-9820-1BA0F96428C7} 30 | EndGlobalSection 31 | EndGlobal 32 | -------------------------------------------------------------------------------- /resources/nutshell.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pauldotknopf/react-aspnet-boilerplate/a605a4040d3efb8dc07f5cfda5462d398e6d4c7b/resources/nutshell.gif -------------------------------------------------------------------------------- /resources/preview-thumbnail.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pauldotknopf/react-aspnet-boilerplate/a605a4040d3efb8dc07f5cfda5462d398e6d4c7b/resources/preview-thumbnail.jpg -------------------------------------------------------------------------------- /src/ReactBoilerplate/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["react", "es2015", "stage-0"], 3 | "plugins": [] 4 | } -------------------------------------------------------------------------------- /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/.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/.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/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 | -------------------------------------------------------------------------------- /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/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/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/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/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/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/Controllers/Account/ServerController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Dynamic; 3 | using System.Linq; 4 | using System.Security.Claims; 5 | using System.Threading.Tasks; 6 | using Microsoft.AspNetCore.Identity; 7 | using Microsoft.AspNetCore.Mvc; 8 | using Microsoft.Extensions.Options; 9 | using Newtonsoft.Json; 10 | using ReactBoilerplate.Models; 11 | using Microsoft.Extensions.DependencyInjection; 12 | 13 | namespace ReactBoilerplate.Controllers.Account 14 | { 15 | public class ServerController : BaseController 16 | { 17 | UserManager _userManager; 18 | SignInManager _signInManager; 19 | 20 | public ServerController(UserManager userManager, 21 | SignInManager signInManager) 22 | : base(userManager, 23 | signInManager) 24 | { 25 | _userManager = userManager; 26 | _signInManager = signInManager; 27 | } 28 | 29 | [Route("register")] 30 | public async Task Register() 31 | { 32 | return View("js-{auto}", await BuildState()); 33 | } 34 | 35 | [Route("login")] 36 | public async Task Login() 37 | { 38 | return View("js-{auto}", await BuildState()); 39 | } 40 | 41 | [Route("forgotpassword")] 42 | public async Task ForgotPassword() 43 | { 44 | return View("js-{auto}", await BuildState()); 45 | } 46 | 47 | [Route("resetpassword", Name="resetpassword")] 48 | public async Task ResetPassword() 49 | { 50 | return View("js-{auto}", await BuildState()); 51 | } 52 | 53 | [Route("confirmemail", Name="confirmemail")] 54 | public async Task ConfirmEmail(string userId, string code, string newEmail /*for email changed*/) 55 | { 56 | ViewBag.change = !string.IsNullOrEmpty(newEmail); 57 | 58 | var state = await BuildState(); 59 | 60 | if (userId == null || code == null) 61 | { 62 | ViewBag.success = false; 63 | return View("js-{auto}", state); 64 | } 65 | 66 | var user = await _userManager.FindByIdAsync(userId); 67 | 68 | if (user == null) 69 | { 70 | ViewBag.success = false; 71 | return View("js-{auto}", state); 72 | } 73 | 74 | IdentityResult result; 75 | if (!string.IsNullOrEmpty(newEmail)) 76 | { 77 | result = await _userManager.ChangeEmailAsync(user, newEmail, code); 78 | } 79 | else 80 | { 81 | result = await _userManager.ConfirmEmailAsync(user, code); 82 | } 83 | 84 | ViewBag.success = result.Succeeded; 85 | 86 | return View("js-{auto}", state); 87 | } 88 | 89 | [Route("externallogincallback")] 90 | public async Task ExternalLoginCallback(bool autoLogin = true) 91 | { 92 | var callbackTemplate = new Func(x => 93 | { 94 | var serializerSettings = HttpContext 95 | .RequestServices 96 | .GetRequiredService>() 97 | .Value 98 | .SerializerSettings; 99 | var serialized = JsonConvert.SerializeObject(x, serializerSettings); 100 | return 101 | $@" 102 | 103 | 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/BaseController.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.Threading.Tasks; 3 | using Microsoft.AspNetCore.Identity; 4 | using Microsoft.AspNetCore.Mvc; 5 | using ReactBoilerplate.Models; 6 | using ReactBoilerplate.State; 7 | 8 | namespace ReactBoilerplate.Controllers 9 | { 10 | public class BaseController : Controller 11 | { 12 | UserManager _userManager; 13 | SignInManager _signInManager; 14 | 15 | public BaseController(UserManager userManager, 16 | SignInManager signInManager) 17 | { 18 | _userManager = userManager; 19 | _signInManager = signInManager; 20 | } 21 | 22 | protected async Task BuildState() 23 | { 24 | var state = new GlobalState(); 25 | 26 | var user = await GetCurrentUserAsync(); 27 | 28 | if (user != null) 29 | { 30 | state.Auth.User = ReactBoilerplate.Models.Api.User.From(user); 31 | state.Auth.LoggedIn = true; 32 | } 33 | 34 | var schemes = await _signInManager.GetExternalAuthenticationSchemesAsync(); 35 | state.ExternalLogin.LoginProviders 36 | .AddRange(schemes 37 | .Select(x => new ExternalLoginState.ExternalLoginProvider 38 | { 39 | Scheme = x.Name, 40 | DisplayName = x.DisplayName 41 | })); 42 | 43 | return state; 44 | } 45 | 46 | protected async Task GetCurrentUserAsync() 47 | { 48 | if (!User.Identity.IsAuthenticated) 49 | return null; 50 | 51 | var user = await _userManager.GetUserAsync(HttpContext.User); 52 | 53 | if (user == null) 54 | { 55 | await _signInManager.SignOutAsync(); 56 | return null; 57 | } 58 | 59 | return user; 60 | } 61 | 62 | protected IActionResult RedirectToLocal(string returnUrl) 63 | { 64 | return Redirect(Url.IsLocalUrl(returnUrl) ? returnUrl : "/"); 65 | } 66 | 67 | protected object GetModelState() 68 | { 69 | return ModelState.ToDictionary(x => string.IsNullOrEmpty(x.Key) ? "_global" : ToCamelCase(x.Key), x => x.Value.Errors.Select(y => y.ErrorMessage)); 70 | } 71 | 72 | protected string ToCamelCase(string s) 73 | { 74 | if (string.IsNullOrEmpty(s)) 75 | return s; 76 | 77 | if (!char.IsUpper(s[0])) 78 | return s; 79 | 80 | string camelCase = char.ToLower(s[0]).ToString(); 81 | if (s.Length > 1) 82 | camelCase += s.Substring(1); 83 | 84 | return camelCase; 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/components/ChangeEmailForm/ChangeEmailForm.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 { changeEmail } from 'redux/modules/manage'; 6 | 7 | class ChangeEmailForm extends Form { 8 | constructor(props) { 9 | super(props); 10 | this.success = this.success.bind(this); 11 | this.state = { success: false }; 12 | } 13 | success() { 14 | this.setState({ success: true }); 15 | } 16 | render() { 17 | const { 18 | fields: { currentPassword, email, emailConfirm } 19 | } = this.props; 20 | const { 21 | success 22 | } = this.state; 23 | return ( 24 |
25 | {success && 26 |

27 | An email has been sent to your email to confirm the change. 28 |

29 | } 30 | {!success && 31 |
34 | {this.renderGlobalErrorList()} 35 | 36 | 37 | 38 |
39 |
40 | 41 |
42 |
43 |
44 | } 45 |
46 | ); 47 | } 48 | } 49 | 50 | ChangeEmailForm = reduxForm({ 51 | form: 'changeEmail', 52 | fields: ['currentPassword', 'email', 'emailConfirm'] 53 | }, 54 | (state) => state, 55 | { } 56 | )(ChangeEmailForm); 57 | 58 | export default ChangeEmailForm; 59 | -------------------------------------------------------------------------------- /src/ReactBoilerplate/Scripts/components/ChangePasswordForm/ChangePasswordForm.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 { changePassword } from 'redux/modules/manage'; 6 | 7 | class ChangePasswordForm extends Form { 8 | constructor(props) { 9 | super(props); 10 | this.success = this.success.bind(this); 11 | this.state = { success: false }; 12 | } 13 | success() { 14 | this.setState({ success: true }); 15 | } 16 | render() { 17 | const { 18 | fields: { oldPassword, newPassword, newPasswordConfirm } 19 | } = this.props; 20 | const { 21 | success 22 | } = this.state; 23 | return ( 24 |
25 | {success && 26 |

27 | Your password has been changed. 28 |

29 | } 30 | {!success && 31 |
34 | {this.renderGlobalErrorList()} 35 | 36 | 37 | 38 |
39 |
40 | 41 |
42 |
43 |
44 | } 45 |
46 | ); 47 | } 48 | } 49 | 50 | ChangePasswordForm = reduxForm({ 51 | form: 'changePassword', 52 | fields: ['oldPassword', 'newPassword', 'newPasswordConfirm'] 53 | }, 54 | (state) => state, 55 | { } 56 | )(ChangePasswordForm); 57 | 58 | export default ChangePasswordForm; 59 | -------------------------------------------------------------------------------- /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/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/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/components/ForgotPasswordForm/ForgotPasswordForm.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 { forgotPassword } from 'redux/modules/account'; 6 | 7 | class ForgotPasswordForm extends Form { 8 | constructor(props) { 9 | super(props); 10 | this.success = this.success.bind(this); 11 | this.state = { success: false }; 12 | } 13 | success() { 14 | this.setState({ success: true }); 15 | } 16 | render() { 17 | const { 18 | fields: { email } 19 | } = this.props; 20 | const { 21 | success 22 | } = this.state; 23 | return ( 24 |
25 | {success && 26 |

27 | Please check your email to reset your password. 28 |

29 | } 30 | {!success && 31 |
34 | {this.renderGlobalErrorList()} 35 | 36 |
37 |
38 | 39 |
40 |
41 |
42 | } 43 |
44 | ); 45 | } 46 | } 47 | 48 | ForgotPasswordForm = reduxForm({ 49 | form: 'forgotPassword', 50 | fields: ['email'] 51 | }, 52 | (state) => state, 53 | { } 54 | )(ForgotPasswordForm); 55 | 56 | export default ForgotPasswordForm; 57 | -------------------------------------------------------------------------------- /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/Input.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import classNames from 'classnames'; 3 | import { Glyphicon } from 'react-bootstrap'; 4 | 5 | class Input extends Component { 6 | static propTypes = { 7 | field: PropTypes.object.isRequired, 8 | type: React.PropTypes.oneOf([ 9 | 'password', 10 | 'text', 11 | 'option', 12 | 'checkbox' 13 | ]), 14 | }; 15 | buildFieldProps() { 16 | const { 17 | defaultChecked, 18 | defaultValue, 19 | name, 20 | onBlur, 21 | onChange, 22 | onDragStart, 23 | onDrop, 24 | onFocus 25 | } = this.props.field; 26 | return { 27 | defaultChecked, 28 | defaultValue, 29 | name, 30 | onBlur, 31 | onChange, 32 | onDragStart, 33 | onDrop, 34 | onFocus 35 | }; 36 | } 37 | renderErrorList(errors) { 38 | if (!errors) { 39 | return null; 40 | } 41 | return ( 42 |
43 | {errors.map((err, i) => 44 | ( 45 |

48 | 49 | {' '} 50 | {err} 51 |

52 | ))} 53 |
54 | ); 55 | } 56 | renderInput() { 57 | return ( 58 | 65 | ); 66 | } 67 | renderOption() { 68 | const { 69 | options 70 | } = this.props; 71 | return ( 72 | 83 | ); 84 | } 85 | renderCheckBox() { 86 | return ( 87 |
88 | 89 | 90 |
91 | ); 92 | } 93 | render() { 94 | let hasError = false; 95 | let errors; 96 | if (this.props.field.touched && this.props.field.invalid) { 97 | hasError = true; 98 | errors = this.props.field.error.errors; 99 | if (!Array.isArray(errors)) { 100 | console.error('The errors object does not seem to be an array of errors.'); // eslint-disable-line max-len 101 | errors = null; 102 | } 103 | if (errors.length === 0) { 104 | console.error('The errors array is empty. If it is empty, no array should be provided, the field is valid.'); // eslint-disable-line max-len 105 | } 106 | } 107 | const rowClass = classNames({ 108 | 'form-group': true, 109 | 'has-error': hasError, 110 | }); 111 | let input; 112 | switch (this.props.type) { 113 | case 'password': 114 | case 'text': 115 | input = this.renderInput(); 116 | break; 117 | case 'option': 118 | input = this.renderOption(); 119 | break; 120 | case 'checkbox': 121 | input = this.renderCheckBox(); 122 | break; 123 | default: 124 | throw new Error('unknown type'); 125 | } 126 | return ( 127 |
128 | {(this.props.type !== 'checkbox') && 129 | 130 | } 131 |
132 | {input} 133 | {this.renderErrorList(errors)} 134 |
135 |
136 | ); 137 | } 138 | } 139 | 140 | Input.defaultProps = { 141 | type: 'text' 142 | }; 143 | 144 | export default Input; 145 | -------------------------------------------------------------------------------- /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/RegisterForm/RegisterForm.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Form from 'components/Form'; 3 | import { reduxForm } from 'redux-form'; 4 | import { Input, ExternalLoginButton, ExternalLogin } from 'components'; 5 | import { register } from 'redux/modules/account'; 6 | import { clearAuthentication as clearExternalAuthentication } from 'redux/modules/externalLogin'; 7 | import { Button, Row, Col } from 'react-bootstrap'; 8 | 9 | class RegisterForm extends Form { 10 | modifyValues(values) { 11 | return { 12 | ...values, 13 | linkExternalLogin: this.props.externalLogin.externalAuthenticated 14 | }; 15 | } 16 | onRemoveExternalAuthClick(action) { 17 | return (event) => { 18 | event.preventDefault(); 19 | action(); 20 | }; 21 | } 22 | render() { 23 | const { 24 | fields: { userName, email, password, passwordConfirm }, 25 | externalLogin: { externalAuthenticated, externalAuthenticatedProvider, loginProviders } 26 | } = this.props; 27 | return ( 28 |
29 | {this.renderGlobalErrorList()} 30 | {externalAuthenticated && 31 |
32 | 33 | 34 | 38 | {' '} 39 | 42 | 43 |
44 | } 45 | {(!externalAuthenticated && loginProviders.length > 0) && 46 | 47 | 48 | 49 | 50 |

Or...

51 | 52 |
53 | } 54 | 55 | 56 | 57 | 58 |
59 |
60 | 61 |
62 |
63 |
64 | ); 65 | } 66 | } 67 | 68 | RegisterForm = reduxForm({ 69 | form: 'register', 70 | fields: ['userName', 'email', 'password', 'passwordConfirm'] 71 | }, 72 | (state) => ({ 73 | externalLogin: state.externalLogin, 74 | initialValues: { userName: (state.externalLogin.proposedUserName ? state.externalLogin.proposedUserName : ''), email: (state.externalLogin.proposedEmail ? state.externalLogin.proposedEmail : '') } 75 | }), 76 | { clearExternalAuthentication } 77 | )(RegisterForm); 78 | 79 | export default RegisterForm; 80 | -------------------------------------------------------------------------------- /src/ReactBoilerplate/Scripts/components/ResetPasswordForm/ResetPasswordForm.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 { resetPassword } from 'redux/modules/account'; 6 | import { Link } from 'react-router'; 7 | 8 | class ResetPasswordForm extends Form { 9 | constructor(props) { 10 | super(props); 11 | this.success = this.success.bind(this); 12 | this.state = { success: false }; 13 | } 14 | modifyValues(values) { 15 | return { 16 | ...values, 17 | code: this.props.code 18 | }; 19 | } 20 | success() { 21 | this.setState({ success: true }); 22 | } 23 | render() { 24 | const { 25 | fields: { email, password, passwordConfirm } 26 | } = this.props; 27 | const { 28 | success 29 | } = this.state; 30 | return ( 31 |
32 | {success && 33 |

34 | Your password has been reset. Please Click here to log in. 35 |

36 | } 37 | {!success && 38 |
41 | {this.renderGlobalErrorList()} 42 | 43 | 44 | 45 |
46 |
47 | 48 |
49 |
50 |
51 | } 52 |
53 | ); 54 | } 55 | } 56 | 57 | ResetPasswordForm = reduxForm({ 58 | form: 'resetPassword', 59 | fields: ['email', 'password', 'passwordConfirm'] 60 | }, 61 | (state) => { 62 | let code = null; 63 | if (state.routing.locationBeforeTransitions) { 64 | if (state.routing.locationBeforeTransitions.query) { 65 | code = state.routing.locationBeforeTransitions.query.code; 66 | } 67 | } 68 | return { 69 | code 70 | }; 71 | }, 72 | { } 73 | )(ResetPasswordForm); 74 | 75 | export default ResetPasswordForm; 76 | -------------------------------------------------------------------------------- /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/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/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/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/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/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/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/App/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import Helmet from 'react-helmet'; 4 | import config from '../../config'; 5 | import { IndexLink } from 'react-router'; 6 | import { LinkContainer, IndexLinkContainer } from 'react-router-bootstrap'; 7 | import Navbar from 'react-bootstrap/lib/Navbar'; 8 | import Nav from 'react-bootstrap/lib/Nav'; 9 | import NavItem from 'react-bootstrap/lib/NavItem'; 10 | import { logoff } from '../../redux/modules/account'; 11 | import { push } from 'react-router-redux'; 12 | import TwoFactorModal from './Modals/TwoFactorModal'; 13 | 14 | require('./App.scss'); 15 | 16 | class App extends Component { 17 | static propTypes = { 18 | children: PropTypes.object.isRequired 19 | }; 20 | constructor(props) { 21 | super(props); 22 | this.logoffClick = this.logoffClick.bind(this); 23 | } 24 | logoffClick() { 25 | this.props.logoff(); 26 | this.props.pushState('/'); 27 | } 28 | renderLoggedInLinks(user) { 29 | return ( 30 | 38 | ); 39 | } 40 | renderAnonymousLinks() { 41 | return ( 42 | 50 | ); 51 | } 52 | render() { 53 | const { 54 | user 55 | } = this.props; 56 | let loginLinks; 57 | if (user) { 58 | loginLinks = this.renderLoggedInLinks(user); 59 | } else { 60 | loginLinks = this.renderAnonymousLinks(); 61 | } 62 | return ( 63 |
64 | 65 | 66 | 67 | 68 | 69 | {config.app.title} 70 | 71 | 72 | 73 | 74 | 75 | 86 | {loginLinks} 87 | 88 | 89 |
90 | {this.props.children} 91 |
92 |
93 |

© 2016 - {config.app.title}

94 |
95 |
96 | 97 |
98 | ); 99 | } 100 | } 101 | 102 | export default connect( 103 | state => ({ user: state.auth.user }), 104 | { logoff, pushState: push } 105 | )(App); 106 | -------------------------------------------------------------------------------- /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/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/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/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/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/Home/ASP-NET-Banners-01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pauldotknopf/react-aspnet-boilerplate/a605a4040d3efb8dc07f5cfda5462d398e6d4c7b/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/a605a4040d3efb8dc07f5cfda5462d398e6d4c7b/src/ReactBoilerplate/Scripts/containers/Home/ASP-NET-Banners-02.png -------------------------------------------------------------------------------- /src/ReactBoilerplate/Scripts/containers/Home/Banner-01-Azure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pauldotknopf/react-aspnet-boilerplate/a605a4040d3efb8dc07f5cfda5462d398e6d4c7b/src/ReactBoilerplate/Scripts/containers/Home/Banner-01-Azure.png -------------------------------------------------------------------------------- /src/ReactBoilerplate/Scripts/containers/Home/Banner-02-VS.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pauldotknopf/react-aspnet-boilerplate/a605a4040d3efb8dc07f5cfda5462d398e6d4c7b/src/ReactBoilerplate/Scripts/containers/Home/Banner-02-VS.png -------------------------------------------------------------------------------- /src/ReactBoilerplate/Scripts/containers/Home/Home.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Helmet from 'react-helmet'; 3 | import { connect } from 'react-redux'; 4 | import { Carousel, CarouselItem } from 'react-bootstrap'; 5 | 6 | class Home extends Component { 7 | render() { 8 | return ( 9 |
10 | 11 | 12 | 13 | ASP.NET 18 |
19 |

20 | Learn how to build ASP.NET apps that can run anywhere. 21 | 22 | Learn More 23 | 24 |

25 |
26 |
27 | 28 | Visual Studio 33 |
34 |

35 | There are powerful new features in Visual Studio for building modern web apps. 36 | 37 | Learn More 38 | 39 |

40 |
41 |
42 | 43 | Package Management 48 |
49 |

50 | Bring in libraries from NuGet, Bower, and npm, and automate 51 | tasks using Grunt or Gulp. 52 | 53 | Learn More 54 | 55 |

56 |
57 |
58 | 59 | Microsoft Azure 64 |
65 |

66 | Learn how Microsoft's Azure cloud platform allows you to build, 67 | deploy, and scale web apps. 68 | 69 | Learn More 70 | 71 |

72 |
73 |
74 |
75 |
76 | ); 77 | } 78 | } 79 | 80 | function mapStateToProps(state) { 81 | return state; 82 | } 83 | 84 | function mapDispatchToProps() { 85 | return {}; 86 | } 87 | 88 | export default connect( 89 | mapStateToProps, 90 | mapDispatchToProps 91 | )(Home); 92 | -------------------------------------------------------------------------------- /src/ReactBoilerplate/Scripts/containers/Home/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pauldotknopf/react-aspnet-boilerplate/a605a4040d3efb8dc07f5cfda5462d398e6d4c7b/src/ReactBoilerplate/Scripts/containers/Home/logo.png -------------------------------------------------------------------------------- /src/ReactBoilerplate/Scripts/containers/Login/Login.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { LoginForm } from 'components'; 3 | import { connect } from 'react-redux'; 4 | import { push } from 'react-router-redux'; 5 | import { Link } from 'react-router'; 6 | import { rehydrateLogin } from 'redux/modules/externalLogin'; 7 | 8 | class Login extends Component { 9 | componentWillReceiveProps(nextProps) { 10 | // if the user logged in, redirect the user. 11 | if (!this.props.user && nextProps.user) { 12 | if (this.props.location.query.returnUrl) { 13 | this.props.pushState(this.props.location.query.returnUrl); 14 | } else { 15 | this.props.pushState('/'); 16 | } 17 | return; 18 | } 19 | // if the user was externally authenticated, but wasn't registered, 20 | // redirect the user to the register. 21 | if (!this.props.externalLogin.externalAuthenticated && nextProps.externalLogin.externalAuthenticated) { 22 | if (nextProps.externalLogin.signInError) { 23 | // The user requires two-factor login or is locked out. 24 | // This means the user is already registered, so no need 25 | // to redirect to register page. 26 | return; 27 | } 28 | 29 | let registerUrl = '/register'; 30 | if (this.props.location.query.returnUrl) { 31 | registerUrl += '?returnUrl=' + this.props.location.query.returnUrl; 32 | } 33 | this.props.pushState(registerUrl); 34 | // whenever we navigate to a new page, the external login info is cleared. 35 | // however, when we navigate to the register page, we want to this info 36 | // so that the register page can associte the external login to the new 37 | // account. 38 | // So, every the `pushState` call clears out `externalLogin`, we will 39 | // need to put it back in 40 | this.props.rehydrateLogin(nextProps.externalLogin); 41 | return; 42 | } 43 | } 44 | render() { 45 | return ( 46 |
47 |

Login

48 |
49 | 50 |

51 | Register as a new user? 52 |

53 |

54 | Forgot your password? 55 |

56 |
57 | ); 58 | } 59 | } 60 | 61 | export default connect( 62 | state => ({ user: state.auth.user, externalLogin: state.externalLogin }), 63 | { pushState: push, rehydrateLogin } 64 | )(Login); 65 | -------------------------------------------------------------------------------- /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/containers/Manage/Email/Email.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { loadEmail, destroyEmail, verifyEmail } from 'redux/modules/manage'; 4 | import { ChangeEmailForm, Spinner } from 'components'; 5 | import { Alert, Button } from 'react-bootstrap'; 6 | 7 | class Email extends Component { 8 | constructor(props) { 9 | super(props); 10 | this.verifyClick = this.verifyClick.bind(this); 11 | this.state = { sendingEmailVerification: false }; 12 | } 13 | componentDidMount() { 14 | this.props.loadEmail(); 15 | } 16 | componentWillUnmount() { 17 | this.props.destroyEmail(); 18 | } 19 | verifyClick(event) { 20 | event.preventDefault(); 21 | this.setState({ sendingEmailVerification: true }); 22 | this.props.verifyEmail() 23 | .then(() => { 24 | this.setState({ sendingEmailVerification: false }); 25 | }, () => { 26 | this.setState({ sendingEmailVerification: false }); 27 | }); 28 | } 29 | render() { 30 | const { 31 | email, 32 | emailConfirmed 33 | } = this.props.email; 34 | const { 35 | sendingEmailVerification 36 | } = this.state; 37 | console.log(email); 38 | if (typeof email === 'undefined') { 39 | return (); 40 | } 41 | return ( 42 |
43 |

Email

44 |
45 |
46 | 47 |
48 |

{email}

49 |
50 |
51 |
52 | {!emailConfirmed && 53 | 54 | Your email is not verified. 55 |
56 | 61 |
62 | } 63 |

Change your email

64 | 65 |
66 | ); 67 | } 68 | } 69 | 70 | export default connect( 71 | (state) => ({ email: state.manage.email }), 72 | { loadEmail, destroyEmail, verifyEmail } 73 | )(Email); 74 | -------------------------------------------------------------------------------- /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/Manage/Logins/Logins.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { loadExternalLogins, destroyExternalLogins, addExternalLogin, removeExternalLogin } from 'redux/modules/manage'; 4 | import { authenticate as externalAuthenticate, clearAuthentication as clearExternalAuthentication } from 'redux/modules/externalLogin'; 5 | import { Button } from 'react-bootstrap'; 6 | import { ExternalLoginButton, Spinner, ErrorList } from 'components'; 7 | 8 | class Logins extends Component { 9 | constructor(props) { 10 | super(props); 11 | this.addButtonClick = this.addButtonClick.bind(this); 12 | this.removeButtonClick = this.removeButtonClick.bind(this); 13 | } 14 | componentDidMount() { 15 | this.props.loadExternalLogins(); 16 | } 17 | componentWillUnmount() { 18 | this.props.destroyExternalLogins(); 19 | } 20 | addButtonClick(scheme) { 21 | return (event) => { 22 | event.preventDefault(); 23 | this.props.externalAuthenticate(scheme, false /* don't auto sign-in */) 24 | .then((result) => { 25 | this.props.clearExternalAuthentication(); 26 | if (result.externalAuthenticated) { 27 | // the user succesfully authenticated with the service. 28 | // add the login to this account. 29 | this.props.addExternalLogin(); 30 | } 31 | }, () => { 32 | this.props.clearExternalAuthentication(); 33 | }); 34 | }; 35 | } 36 | removeButtonClick(scheme) { 37 | return (event) => { 38 | event.preventDefault(); 39 | this.props.removeExternalLogin(scheme); 40 | }; 41 | } 42 | render() { 43 | if (this.props.externalLogins.loading) { 44 | return (); 45 | } 46 | if (!this.props.externalLogins.currentLogins) { 47 | return (); 48 | } 49 | const { 50 | currentLogins, 51 | otherLogins, 52 | errors 53 | } = this.props.externalLogins; 54 | return ( 55 |
56 |

Manage your external logins

57 | 58 | {(currentLogins.length > 0) && 59 |
60 |

Current logins

61 | 62 | 63 | {currentLogins.map((currentLogin, i) => 64 | ( 65 | 66 | 76 | 77 | ))} 78 | 79 |
67 | 70 | {' '} 71 | 75 |
80 |
81 | } 82 | {(otherLogins.length > 0) && 83 |
84 |

Add another service to log in.

85 | 86 | 87 | {otherLogins.map((otherLogin, i) => 88 | ( 89 | 90 | 100 | 101 | ))} 102 | 103 |
91 | 94 | {' '} 95 | 99 |
104 |
105 | } 106 |
107 | ); 108 | } 109 | } 110 | 111 | export default connect( 112 | state => ({ externalLogins: state.manage.externalLogins }), 113 | { loadExternalLogins, destroyExternalLogins, externalAuthenticate, clearExternalAuthentication, addExternalLogin, removeExternalLogin } 114 | )(Logins); 115 | -------------------------------------------------------------------------------- /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/containers/Manage/Security/Security.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { loadSecurity, destroySecurity, setTwoFactor } from 'redux/modules/manage'; 4 | import { Spinner } from 'components'; 5 | import { Alert, Button } from 'react-bootstrap'; 6 | 7 | class Security extends Component { 8 | constructor(props) { 9 | super(props); 10 | this.toggleTwoFactorClick = this.toggleTwoFactorClick.bind(this); 11 | } 12 | componentDidMount() { 13 | this.props.loadSecurity(); 14 | } 15 | componentWillUnmount() { 16 | this.props.destroySecurity(); 17 | } 18 | toggleTwoFactorClick(event) { 19 | event.preventDefault(); 20 | const { 21 | twoFactorEnabled 22 | } = this.props.security; 23 | this.props.setTwoFactor(!twoFactorEnabled); 24 | } 25 | render() { 26 | const { 27 | twoFactorEnabled, 28 | validTwoFactorProviders, 29 | settingTwoFactor 30 | } = this.props.security; 31 | if (typeof twoFactorEnabled === 'undefined') { 32 | return (); 33 | } 34 | return ( 35 |
36 | 37 | Two-factor authentication is {twoFactorEnabled ? 'enabled' : 'disabled'}. 38 |
39 | 44 |
45 | {(twoFactorEnabled && validTwoFactorProviders.length === 0) && 46 | 47 | Although you have two-factor authentication enabled, you have no valid providers to authenticate with. 48 | 49 | } 50 |
51 | ); 52 | } 53 | } 54 | 55 | export default connect( 56 | (state) => ({ security: state.manage.security }), 57 | { loadSecurity, destroySecurity, setTwoFactor } 58 | )(Security); 59 | -------------------------------------------------------------------------------- /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/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/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/Scripts/containers/SignIn/SignIn.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pauldotknopf/react-aspnet-boilerplate/a605a4040d3efb8dc07f5cfda5462d398e6d4c7b/src/ReactBoilerplate/Scripts/containers/SignIn/SignIn.js -------------------------------------------------------------------------------- /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/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/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 |