├── src
└── ReactBoilerplate
│ ├── Scripts
│ ├── containers
│ │ ├── SignIn
│ │ │ └── SignIn.js
│ │ ├── Home
│ │ │ ├── logo.png
│ │ │ ├── Banner-02-VS.png
│ │ │ ├── Banner-01-Azure.png
│ │ │ ├── ASP-NET-Banners-01.png
│ │ │ ├── ASP-NET-Banners-02.png
│ │ │ └── Home.js
│ │ ├── NotFound
│ │ │ └── NotFound.js
│ │ ├── Manage
│ │ │ ├── Index
│ │ │ │ └── Index.js
│ │ │ ├── ChangePassword
│ │ │ │ └── ChangePassword.js
│ │ │ ├── Manage.js
│ │ │ ├── Security
│ │ │ │ └── Security.js
│ │ │ ├── Email
│ │ │ │ └── Email.js
│ │ │ └── Logins
│ │ │ │ └── Logins.js
│ │ ├── About
│ │ │ └── About.js
│ │ ├── Contact
│ │ │ └── Contact.js
│ │ ├── ResetPassword
│ │ │ └── ResetPassword.js
│ │ ├── ForgotPassword
│ │ │ └── ForgotPassword.js
│ │ ├── App
│ │ │ ├── App.scss
│ │ │ ├── Modals
│ │ │ │ └── TwoFactorModal.js
│ │ │ └── App.js
│ │ ├── Register
│ │ │ └── Register.js
│ │ ├── index.js
│ │ ├── ConfirmEmail
│ │ │ └── ConfirmEmail.js
│ │ └── Login
│ │ │ └── Login.js
│ ├── utils
│ │ ├── promise-window-server.js
│ │ ├── superagent-server.js
│ │ ├── isEmpty.js
│ │ └── modelState.js
│ ├── config.js
│ ├── components
│ │ ├── Spinner.js
│ │ ├── ExternalLoginButton.js
│ │ ├── ErrorList.js
│ │ ├── TwoFactor
│ │ │ ├── TwoFactor.js
│ │ │ ├── SendCodeForm.js
│ │ │ └── VerifyCodeForm.js
│ │ ├── index.js
│ │ ├── ExternalLogin
│ │ │ └── ExternalLogin.js
│ │ ├── Form.js
│ │ ├── LoginForm
│ │ │ └── LoginForm.js
│ │ ├── ForgotPasswordForm
│ │ │ └── ForgotPasswordForm.js
│ │ ├── ChangeEmailForm
│ │ │ └── ChangeEmailForm.js
│ │ ├── ChangePasswordForm
│ │ │ └── ChangePasswordForm.js
│ │ ├── ResetPasswordForm
│ │ │ └── ResetPasswordForm.js
│ │ ├── RegisterForm
│ │ │ └── RegisterForm.js
│ │ └── Input.js
│ ├── redux
│ │ ├── modules
│ │ │ ├── manage
│ │ │ │ ├── index.js
│ │ │ │ ├── changePassword.js
│ │ │ │ ├── email.js
│ │ │ │ ├── security.js
│ │ │ │ └── externalLogins.js
│ │ │ ├── viewBag.js
│ │ │ ├── auth.js
│ │ │ ├── externalLogin.js
│ │ │ └── account.js
│ │ ├── reducer.js
│ │ ├── configureStore.js
│ │ └── middleware
│ │ │ └── clientMiddleware.js
│ ├── client.js
│ ├── helpers
│ │ ├── ApiClient.js
│ │ └── Html.js
│ ├── routes.js
│ ├── server.js
│ └── webpack
│ │ ├── dev.config.js
│ │ └── prod.config.js
│ ├── .babelrc
│ ├── wwwroot
│ └── favicon.ico
│ ├── .editorconfig
│ ├── .bootstraprc
│ ├── Services
│ ├── ISmsSender.cs
│ ├── IEmailSender.cs
│ ├── SmsSender.cs
│ └── EmailSender.cs
│ ├── React.sublime-project
│ ├── Models
│ ├── ApplicationUser.cs
│ ├── Api
│ │ └── User.cs
│ └── ApplicationDbContext.cs
│ ├── Controllers
│ ├── Manage
│ │ ├── Models
│ │ │ ├── SetTwoFactorModel.cs
│ │ │ ├── RemoveExternalLoginModel.cs
│ │ │ ├── ExternalLoginConfirmationModel.cs
│ │ │ ├── ChangeEmailModel.cs
│ │ │ └── ChangePasswordModel.cs
│ │ ├── ServerController.cs
│ │ └── ApiController.cs
│ ├── Account
│ │ ├── Models
│ │ │ ├── ForgotPasswordModel.cs
│ │ │ ├── SendCodeModel.cs
│ │ │ ├── LoginModel.cs
│ │ │ ├── VerifyCodeModel.cs
│ │ │ ├── ResetPasswordModel.cs
│ │ │ └── RegisterModel.cs
│ │ ├── ServerController.cs
│ │ └── ApiController.cs
│ ├── Status
│ │ └── ServerController.cs
│ ├── Home
│ │ └── HomeController.cs
│ └── BaseController.cs
│ ├── State
│ ├── AuthState.cs
│ ├── GlobalState.cs
│ ├── ExternalLoginState.cs
│ └── Manage
│ │ └── ExternalLoginsState.cs
│ ├── appsettings.json
│ ├── Program.cs
│ ├── web.config
│ ├── Properties
│ └── launchSettings.json
│ ├── .eslintrc
│ ├── ReactBoilerplate.csproj
│ ├── package.json
│ ├── gulpfile.js
│ └── Startup.cs
├── resources
├── nutshell.gif
└── preview-thumbnail.jpg
├── LICENSE
├── ReactBoilerplate.sln
├── README.md
└── .gitignore
/src/ReactBoilerplate/Scripts/containers/SignIn/SignIn.js:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/ReactBoilerplate/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["react", "es2015", "stage-0"],
3 | "plugins": []
4 | }
--------------------------------------------------------------------------------
/resources/nutshell.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pauldotknopf/react-aspnet-boilerplate/HEAD/resources/nutshell.gif
--------------------------------------------------------------------------------
/resources/preview-thumbnail.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pauldotknopf/react-aspnet-boilerplate/HEAD/resources/preview-thumbnail.jpg
--------------------------------------------------------------------------------
/src/ReactBoilerplate/wwwroot/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pauldotknopf/react-aspnet-boilerplate/HEAD/src/ReactBoilerplate/wwwroot/favicon.ico
--------------------------------------------------------------------------------
/src/ReactBoilerplate/Scripts/containers/Home/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pauldotknopf/react-aspnet-boilerplate/HEAD/src/ReactBoilerplate/Scripts/containers/Home/logo.png
--------------------------------------------------------------------------------
/src/ReactBoilerplate/.editorconfig:
--------------------------------------------------------------------------------
1 | # editorconfig.org
2 | root = true
3 |
4 | [*]
5 | indent_style = space
6 | indent_size = 2
7 |
8 | [*.md]
9 | trim_trailing_whitespace = false
10 |
--------------------------------------------------------------------------------
/src/ReactBoilerplate/Scripts/containers/Home/Banner-02-VS.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pauldotknopf/react-aspnet-boilerplate/HEAD/src/ReactBoilerplate/Scripts/containers/Home/Banner-02-VS.png
--------------------------------------------------------------------------------
/src/ReactBoilerplate/Scripts/containers/Home/Banner-01-Azure.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pauldotknopf/react-aspnet-boilerplate/HEAD/src/ReactBoilerplate/Scripts/containers/Home/Banner-01-Azure.png
--------------------------------------------------------------------------------
/src/ReactBoilerplate/Scripts/containers/Home/ASP-NET-Banners-01.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pauldotknopf/react-aspnet-boilerplate/HEAD/src/ReactBoilerplate/Scripts/containers/Home/ASP-NET-Banners-01.png
--------------------------------------------------------------------------------
/src/ReactBoilerplate/Scripts/containers/Home/ASP-NET-Banners-02.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pauldotknopf/react-aspnet-boilerplate/HEAD/src/ReactBoilerplate/Scripts/containers/Home/ASP-NET-Banners-02.png
--------------------------------------------------------------------------------
/src/ReactBoilerplate/Scripts/utils/promise-window-server.js:
--------------------------------------------------------------------------------
1 | export default class PromiseWindow {
2 | open() {
3 | throw new Error('you can not run this on the server, ding-dong.');
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/src/ReactBoilerplate/.bootstraprc:
--------------------------------------------------------------------------------
1 | {
2 | "bootstrapVersion": 3,
3 |
4 | "useFlexbox": true,
5 | "extractStyles": true,
6 | "styleLoaders": ["style", "css", "sass"],
7 |
8 | "styles": true,
9 |
10 | "scripts": false
11 | }
--------------------------------------------------------------------------------
/src/ReactBoilerplate/Services/ISmsSender.cs:
--------------------------------------------------------------------------------
1 | using System.Threading.Tasks;
2 |
3 | namespace ReactBoilerplate.Services
4 | {
5 | public interface ISmsSender
6 | {
7 | Task SendSmsAsync(string number, string message);
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/ReactBoilerplate/React.sublime-project:
--------------------------------------------------------------------------------
1 | {
2 | "folders":
3 | [
4 | {
5 | "path": ".",
6 | "folder_exclude_patterns": ["bower_components", "node_modules"],
7 | "file_exclude_patterns": ["React.xproj*", "project.lock.json"]
8 | }
9 | ]
10 | }
11 |
--------------------------------------------------------------------------------
/src/ReactBoilerplate/Services/IEmailSender.cs:
--------------------------------------------------------------------------------
1 | using System.Threading.Tasks;
2 |
3 | namespace ReactBoilerplate.Services
4 | {
5 | public interface IEmailSender
6 | {
7 | Task SendEmailAsync(string email, string subject, string message);
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/ReactBoilerplate/Models/ApplicationUser.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Identity;
2 | using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
3 |
4 | namespace ReactBoilerplate.Models
5 | {
6 | public class ApplicationUser : IdentityUser
7 | {
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/ReactBoilerplate/Scripts/containers/NotFound/NotFound.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export default function NotFound() {
4 | return (
5 |
6 |
Doh! 404!
7 |
These are not the droids you are looking for!
8 |
9 | );
10 | }
11 |
--------------------------------------------------------------------------------
/src/ReactBoilerplate/Controllers/Manage/Models/SetTwoFactorModel.cs:
--------------------------------------------------------------------------------
1 | using Newtonsoft.Json;
2 |
3 | namespace ReactBoilerplate.Controllers.Manage.Models
4 | {
5 | public class SetTwoFactorModel
6 | {
7 | [JsonProperty("enabled")]
8 | public bool Enabled { get; set; }
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/src/ReactBoilerplate/Scripts/utils/superagent-server.js:
--------------------------------------------------------------------------------
1 | // this replaces superagent on the server.
2 | // TODO: If the server attempts to use this, throw an exception.
3 | // This will ensure that not server-side-rendering depends on network requests.
4 | // The entire state should have been provided for the page already.
5 |
--------------------------------------------------------------------------------
/src/ReactBoilerplate/Controllers/Account/Models/ForgotPasswordModel.cs:
--------------------------------------------------------------------------------
1 | using System.ComponentModel.DataAnnotations;
2 |
3 | namespace ReactBoilerplate.Controllers.Account.Models
4 | {
5 | public class ForgotPasswordModel
6 | {
7 | [EmailAddress]
8 | [Required]
9 | public string Email { get; set; }
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/ReactBoilerplate/Scripts/containers/Manage/Index/Index.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { connect } from 'react-redux';
3 |
4 | class Index extends Component {
5 | render() {
6 | return (
7 |
8 | );
9 | }
10 | }
11 |
12 | export default connect(
13 | (state) => state,
14 | { }
15 | )(Index);
16 |
--------------------------------------------------------------------------------
/src/ReactBoilerplate/Scripts/containers/About/About.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import Helmet from 'react-helmet';
3 |
4 | export default class About extends Component {
5 | render() {
6 | return (
7 |
8 |
About us...
9 |
10 |
11 | );
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/ReactBoilerplate/Scripts/containers/Contact/Contact.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import Helmet from 'react-helmet';
3 |
4 | export default class Contact extends Component {
5 | render() {
6 | return (
7 |
8 |
Contact
9 |
10 |
11 | );
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/ReactBoilerplate/State/AuthState.cs:
--------------------------------------------------------------------------------
1 | using Newtonsoft.Json;
2 | using ReactBoilerplate.Models.Api;
3 |
4 | namespace ReactBoilerplate.State
5 | {
6 | public class AuthState
7 | {
8 | [JsonProperty("user")]
9 | public User User { get; set; }
10 |
11 | [JsonProperty("loggedIn")]
12 | public bool LoggedIn { get; set; }
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/ReactBoilerplate/Scripts/config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | app: {
3 | title: 'ASP.NET React Example',
4 | description: 'This is an example, using React with ASP.NET MVC.',
5 | head: {
6 | titleTemplate: 'React Example: %s',
7 | meta: [
8 | { name: 'description', content: 'This is an example, using React with ASP.NET MVC.' },
9 | ]
10 | }
11 | }
12 | };
13 |
--------------------------------------------------------------------------------
/src/ReactBoilerplate/Scripts/components/Spinner.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const fontAwesome = require('font-awesome/scss/font-awesome.scss');
4 |
5 | const spinnerClass = fontAwesome.fa + ' ' + fontAwesome['fa-spinner'] + ' ' + fontAwesome['fa-spin'] + ' ' + fontAwesome['fa-5x'];
6 |
7 | export default () =>
8 | (
9 |
10 |
11 |
12 | );
13 |
--------------------------------------------------------------------------------
/src/ReactBoilerplate/Controllers/Manage/Models/RemoveExternalLoginModel.cs:
--------------------------------------------------------------------------------
1 | using Newtonsoft.Json;
2 |
3 | namespace ReactBoilerplate.Controllers.Manage.Models
4 | {
5 | public class RemoveExternalLoginModel
6 | {
7 | [JsonProperty("loginProvider")]
8 | public string LoginProvider { get; set; }
9 |
10 | [JsonProperty("providerKey")]
11 | public string ProviderKey { get; set; }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/ReactBoilerplate/appsettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "ConnectionStrings": {
3 | "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=react-dot-net-a96e017a-e5c8-494d-be7a-3d7781d2fa3a;Trusted_Connection=True;MultipleActiveResultSets=true"
4 | },
5 | "Logging": {
6 | "IncludeScopes": false,
7 | "LogLevel": {
8 | "Default": "Error",
9 | "System": "Error",
10 | "Microsoft": "Error"
11 | }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/ReactBoilerplate/Controllers/Account/Models/SendCodeModel.cs:
--------------------------------------------------------------------------------
1 | using System.ComponentModel.DataAnnotations;
2 | using Newtonsoft.Json;
3 |
4 | namespace ReactBoilerplate.Controllers.Account.Models
5 | {
6 | public class SendCodeModel
7 | {
8 | [Required]
9 | [JsonProperty("provider")]
10 | public string Provider { get; set; }
11 |
12 | [JsonProperty("remember")]
13 | public bool Remember { get; set; }
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/ReactBoilerplate/Scripts/redux/modules/manage/index.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux';
2 | import externalLogins from './externalLogins';
3 | import security from './security';
4 | import email from './email';
5 |
6 | export default combineReducers({
7 | externalLogins,
8 | security,
9 | email
10 | });
11 |
12 | export * from './externalLogins';
13 | export * from './changePassword';
14 | export * from './security';
15 | export * from './email';
16 |
--------------------------------------------------------------------------------
/src/ReactBoilerplate/Program.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore;
2 | using Microsoft.AspNetCore.Hosting;
3 |
4 | namespace ReactBoilerplate
5 | {
6 | public class Program
7 | {
8 | public static void Main(string[] args)
9 | {
10 | BuildWebHost(args).Run();
11 | }
12 |
13 | public static IWebHost BuildWebHost(string[] args) =>
14 | WebHost.CreateDefaultBuilder(args)
15 | .UseStartup()
16 | .Build();
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/ReactBoilerplate/State/GlobalState.cs:
--------------------------------------------------------------------------------
1 | using Newtonsoft.Json;
2 |
3 | namespace ReactBoilerplate.State
4 | {
5 | public class GlobalState
6 | {
7 | public GlobalState()
8 | {
9 | Auth = new AuthState();
10 | ExternalLogin = new ExternalLoginState();
11 | }
12 |
13 | [JsonProperty("auth")]
14 | public AuthState Auth { get; set; }
15 |
16 | [JsonProperty("externalLogin")]
17 | public ExternalLoginState ExternalLogin { get; set; }
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/ReactBoilerplate/Controllers/Manage/Models/ExternalLoginConfirmationModel.cs:
--------------------------------------------------------------------------------
1 | using System.ComponentModel.DataAnnotations;
2 | using Newtonsoft.Json;
3 |
4 | namespace ReactBoilerplate.Controllers.Manage.Models
5 | {
6 | public class ExternalLoginConfirmationModel
7 | {
8 | [JsonProperty("userName")]
9 | [Required]
10 | public string UserName { get; set; }
11 |
12 | [JsonProperty("email")]
13 | [EmailAddress]
14 | [Required]
15 | public string Email { get; set; }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/ReactBoilerplate/Scripts/redux/modules/manage/changePassword.js:
--------------------------------------------------------------------------------
1 | export const CHANGEPASSWORD_START = 'react/manage/CHANGEPASSWORD_START';
2 | export const CHANGEPASSWORD_COMPLETE = 'react/manage/CHANGEPASSWORD_COMPLETE';
3 | export const CHANGEPASSWORD_ERROR = 'react/manage/CHANGEPASSWORD_ERROR';
4 |
5 | export function changePassword(body) {
6 | return {
7 | types: [CHANGEPASSWORD_START, CHANGEPASSWORD_COMPLETE, CHANGEPASSWORD_ERROR],
8 | promise: (client) => client.post('/api/manage/changepassword', { data: body })
9 | };
10 | }
11 |
--------------------------------------------------------------------------------
/src/ReactBoilerplate/Scripts/containers/ResetPassword/ResetPassword.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { ResetPasswordForm } from 'components';
3 | import { connect } from 'react-redux';
4 |
5 | class ResetPassword extends Component {
6 | render() {
7 | return (
8 |
9 |
Reset password
10 | Reset your password.
11 |
12 |
13 |
14 | );
15 | }
16 | }
17 |
18 | export default connect(
19 | state => ({ user: state.auth.user }),
20 | { }
21 | )(ResetPassword);
22 |
--------------------------------------------------------------------------------
/src/ReactBoilerplate/Controllers/Account/Models/LoginModel.cs:
--------------------------------------------------------------------------------
1 | using System.ComponentModel.DataAnnotations;
2 | using Newtonsoft.Json;
3 |
4 | namespace ReactBoilerplate.Controllers.Account.Models
5 | {
6 | public class LoginModel
7 | {
8 | [JsonProperty("userName")]
9 | [Required]
10 | public string UserName { get; set; }
11 |
12 | [JsonProperty("password")]
13 | [Required]
14 | public string Password { get; set; }
15 |
16 | [JsonProperty("rememberMe")]
17 | public bool RememberMe { get; set; }
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/ReactBoilerplate/Scripts/components/ExternalLoginButton.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Button } from 'react-bootstrap';
3 |
4 | const bootstrapSocial = require('bootstrap-social');
5 | const fontAwesome = require('font-awesome/scss/font-awesome.scss');
6 |
7 | export default (props) =>
8 | (
9 |
12 |
13 | {props.text}
14 |
15 | );
16 |
--------------------------------------------------------------------------------
/src/ReactBoilerplate/Scripts/containers/ForgotPassword/ForgotPassword.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { ForgotPasswordForm } from 'components';
3 | import { connect } from 'react-redux';
4 |
5 | class ForgotPassword extends Component {
6 | render() {
7 | return (
8 |
9 |
Forgot your password?
10 | Enter your email.
11 |
12 |
13 |
14 | );
15 | }
16 | }
17 |
18 | export default connect(
19 | state => ({ user: state.auth.user }),
20 | { }
21 | )(ForgotPassword);
22 |
--------------------------------------------------------------------------------
/src/ReactBoilerplate/Scripts/containers/Manage/ChangePassword/ChangePassword.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { ChangePasswordForm } from 'components';
3 | import { connect } from 'react-redux';
4 |
5 | class ChangePassword extends Component {
6 | render() {
7 | return (
8 |
9 |
Change Password.
10 | Change Password Form
11 |
12 |
13 |
14 | );
15 | }
16 | }
17 |
18 | export default connect(
19 | state => ({ user: state.auth.user }),
20 | { }
21 | )(ChangePassword);
22 |
--------------------------------------------------------------------------------
/src/ReactBoilerplate/Scripts/redux/reducer.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux';
2 | import { reducer as formReducer } from 'redux-form';
3 | import auth from './modules/auth';
4 | import account from './modules/account';
5 | import externalLogin from './modules/externalLogin';
6 | import manage from './modules/manage';
7 | import viewBag from './modules/viewBag';
8 | import { routerReducer } from 'react-router-redux';
9 |
10 | export default combineReducers({
11 | form: formReducer,
12 | routing: routerReducer,
13 | auth,
14 | account,
15 | externalLogin,
16 | manage,
17 | viewBag
18 | });
19 |
--------------------------------------------------------------------------------
/src/ReactBoilerplate/web.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/src/ReactBoilerplate/Services/SmsSender.cs:
--------------------------------------------------------------------------------
1 | using System.Threading.Tasks;
2 | using Microsoft.Extensions.Logging;
3 |
4 | namespace ReactBoilerplate.Services
5 | {
6 | public class SmsSender : ISmsSender
7 | {
8 | ILogger _logger;
9 |
10 | public SmsSender(ILoggerFactory loggerFactory)
11 | {
12 | _logger = loggerFactory.CreateLogger();
13 | }
14 |
15 | public Task SendSmsAsync(string number, string message)
16 | {
17 | _logger.LogInformation($"Send sms:\nnumber:{number}\nmessage:{message}");
18 | return Task.FromResult(0);
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/ReactBoilerplate/Controllers/Account/Models/VerifyCodeModel.cs:
--------------------------------------------------------------------------------
1 | using System.ComponentModel.DataAnnotations;
2 | using Newtonsoft.Json;
3 |
4 | namespace ReactBoilerplate.Controllers.Account.Models
5 | {
6 | public class VerifyCodeModel
7 | {
8 | [Required]
9 | [JsonProperty("provider")]
10 | public string Provider { get; set; }
11 |
12 | [Required]
13 | [JsonProperty("code")]
14 | public string Code { get; set; }
15 |
16 | [JsonProperty("rememberBrowser")]
17 | public bool RememberBrowser { get; set; }
18 |
19 | [JsonProperty("rememberMe")]
20 | public bool RememberMe { get; set; }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/ReactBoilerplate/Services/EmailSender.cs:
--------------------------------------------------------------------------------
1 | using System.Threading.Tasks;
2 | using Microsoft.Extensions.Logging;
3 |
4 | namespace ReactBoilerplate.Services
5 | {
6 | public class EmailSender : IEmailSender
7 | {
8 | ILogger _logger;
9 |
10 | public EmailSender(ILoggerFactory loggerFactory)
11 | {
12 | _logger = loggerFactory.CreateLogger();
13 | }
14 |
15 | public Task SendEmailAsync(string email, string subject, string message)
16 | {
17 | _logger.LogInformation($"Send email:\nemail:{email}\nsubject:{subject}\nmessage:{message}");
18 | return Task.FromResult(0);
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/ReactBoilerplate/Models/Api/User.cs:
--------------------------------------------------------------------------------
1 | using Newtonsoft.Json;
2 |
3 | namespace ReactBoilerplate.Models.Api
4 | {
5 | public class User
6 | {
7 | [JsonProperty("id")]
8 | public string Id { get; set; }
9 |
10 | [JsonProperty("userName")]
11 | public string UserName { get; set; }
12 |
13 | [JsonProperty("email")]
14 | public string Email { get; set; }
15 |
16 | public static User From(ApplicationUser user)
17 | {
18 | return new User
19 | {
20 | Id = user.Id,
21 | UserName = user.UserName,
22 | Email = user.Email
23 | };
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/ReactBoilerplate/Scripts/utils/isEmpty.js:
--------------------------------------------------------------------------------
1 | // Speed up calls to hasOwnProperty
2 | const hasOwnProperty = Object.prototype.hasOwnProperty;
3 |
4 | export default function isEmpty(obj) {
5 | // null and undefined are "empty"
6 | if (obj === null) return true;
7 | // Assume if it has a length property with a non-zero value
8 | // that that property is correct.
9 | if (obj.length > 0) return false;
10 | if (obj.length === 0) return true;
11 | // Otherwise, does it have any properties of its own?
12 | // Note that this doesn't handle
13 | // toString and valueOf enumeration bugs in IE < 9
14 | for (const key in obj) {
15 | if (hasOwnProperty.call(obj, key)) return false;
16 | }
17 | return true;
18 | }
19 |
--------------------------------------------------------------------------------
/src/ReactBoilerplate/State/ExternalLoginState.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using Newtonsoft.Json;
3 |
4 | namespace ReactBoilerplate.State
5 | {
6 | public class ExternalLoginState
7 | {
8 | public ExternalLoginState()
9 | {
10 | LoginProviders = new List();
11 | }
12 |
13 | [JsonProperty("loginProviders")]
14 | public List LoginProviders { get; set; }
15 |
16 | public class ExternalLoginProvider
17 | {
18 | [JsonProperty("scheme")]
19 | public string Scheme { get; set; }
20 |
21 | [JsonProperty("displayName")]
22 | public string DisplayName { get; set; }
23 | }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/ReactBoilerplate/Controllers/Manage/Models/ChangeEmailModel.cs:
--------------------------------------------------------------------------------
1 | using System.ComponentModel.DataAnnotations;
2 | using Newtonsoft.Json;
3 |
4 | namespace ReactBoilerplate.Controllers.Manage.Models
5 | {
6 | public class ChangeEmailModel
7 | {
8 | [JsonProperty("currentPassword")]
9 | [DataType(DataType.Password)]
10 | [Required]
11 | public string CurrentPassword { get; set; }
12 |
13 | [JsonProperty("email")]
14 | [EmailAddress]
15 | [Required]
16 | public string Email { get; set; }
17 |
18 | [JsonProperty("emailConfirm")]
19 | [Compare("Email", ErrorMessage = "The email and confirmation email do not match.")]
20 | public string EmailConfirm { get; set; }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/ReactBoilerplate/Scripts/containers/App/App.scss:
--------------------------------------------------------------------------------
1 | body {
2 | padding-top: 50px;
3 | padding-bottom: 20px;
4 | }
5 |
6 | /* Wrapping element */
7 | /* Set some basic padding to keep content from hitting the edges */
8 | .body-content {
9 | padding-left: 15px;
10 | padding-right: 15px;
11 | }
12 |
13 | /* Set widths on the form inputs since otherwise they're 100% wide */
14 | input,
15 | select,
16 | textarea {
17 | max-width: 280px;
18 | }
19 |
20 | /* Carousel */
21 | .carousel-caption {
22 | z-index: 10 !important;
23 | }
24 |
25 | .carousel-caption p {
26 | font-size: 20px;
27 | line-height: 1.4;
28 | }
29 |
30 | @media (min-width: 768px) {
31 | .carousel-caption {
32 | z-index: 10 !important;
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/ReactBoilerplate/Controllers/Status/ServerController.cs:
--------------------------------------------------------------------------------
1 | using System.Threading.Tasks;
2 | using Microsoft.AspNetCore.Identity;
3 | using Microsoft.AspNetCore.Mvc;
4 | using ReactBoilerplate.Models;
5 |
6 | namespace ReactBoilerplate.Controllers.Status
7 | {
8 | public class StatusController : BaseController
9 | {
10 | public StatusController(UserManager userManager,
11 | SignInManager signInManager)
12 | :base(userManager, signInManager)
13 | {
14 | }
15 |
16 | [Route("status/status/{statusCode}")]
17 | public async Task Status(int statusCode)
18 | {
19 | return View($"js-/statuscode{statusCode}", await BuildState());
20 | }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/ReactBoilerplate/Scripts/components/ErrorList.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { Glyphicon } from 'react-bootstrap';
3 |
4 | class ErrorList extends Component {
5 | render() {
6 | const {
7 | errors
8 | } = this.props;
9 | if (!errors) return null;
10 | if (Array.isArray(errors)) {
11 | if (errors.length === 0) return null;
12 | return (
13 |
14 | {errors.map((err, i) =>
15 | (
16 |
17 |
18 | {' '}
19 | {err}
20 |
21 | ))}
22 |
23 | );
24 | }
25 | return null;
26 | }
27 | }
28 |
29 | export default ErrorList;
30 |
--------------------------------------------------------------------------------
/src/ReactBoilerplate/Scripts/client.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import { Router, browserHistory } from 'react-router';
4 | import getRoutes from './routes';
5 | import { Provider } from 'react-redux';
6 | import configureStore from './redux/configureStore';
7 | import { syncHistoryWithStore } from 'react-router-redux';
8 | import ApiClient from './helpers/ApiClient';
9 |
10 | const client = new ApiClient();
11 | const store = configureStore(window.__data, browserHistory, client);
12 | const history = syncHistoryWithStore(browserHistory, store);
13 |
14 | ReactDOM.render(
15 |
16 |
17 | {getRoutes(store)}
18 |
19 | ,
20 | document.getElementById('content')
21 | );
22 |
--------------------------------------------------------------------------------
/src/ReactBoilerplate/Scripts/containers/Register/Register.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { RegisterForm } from 'components';
3 | import { connect } from 'react-redux';
4 | import { push } from 'react-router-redux';
5 |
6 | class Register extends Component {
7 | componentWillReceiveProps(nextProps) {
8 | if (!this.props.user && nextProps.user) {
9 | this.props.pushState('/');
10 | }
11 | }
12 | render() {
13 | return (
14 |
15 |
Register
16 | Create a new account.
17 |
18 |
19 |
20 | );
21 | }
22 | }
23 |
24 | export default connect(
25 | state => ({ user: state.auth.user, externalLogin: state.externalLogin }),
26 | { pushState: push }
27 | )(Register);
28 |
--------------------------------------------------------------------------------
/src/ReactBoilerplate/Scripts/redux/configureStore.js:
--------------------------------------------------------------------------------
1 | import { createStore, applyMiddleware, compose } from 'redux';
2 | import thunk from 'redux-thunk';
3 | import reducer from './reducer';
4 | import { routerMiddleware } from 'react-router-redux';
5 | import createMiddleware from './middleware/clientMiddleware';
6 |
7 | let devTools = f => f;
8 | if (typeof window === 'object'
9 | && typeof window.devToolsExtension !== 'undefined') {
10 | devTools = window.devToolsExtension();
11 | }
12 |
13 | export default function configureStore(initialState, history, client) {
14 | const enhancer = compose(
15 | applyMiddleware(thunk),
16 | applyMiddleware(routerMiddleware(history)),
17 | applyMiddleware(createMiddleware(client)),
18 | devTools
19 | )(createStore);
20 | return enhancer(reducer, initialState);
21 | }
22 |
--------------------------------------------------------------------------------
/src/ReactBoilerplate/Controllers/Account/Models/ResetPasswordModel.cs:
--------------------------------------------------------------------------------
1 | using System.ComponentModel.DataAnnotations;
2 |
3 | namespace ReactBoilerplate.Controllers.Account.Models
4 | {
5 | public class ResetPasswordModel
6 | {
7 | [Required]
8 | [EmailAddress]
9 | public string Email { get; set; }
10 |
11 | [Required]
12 | [StringLength(100, ErrorMessage = "The {0} must be at least {2} characters long.", MinimumLength = 6)]
13 | [DataType(DataType.Password)]
14 | public string Password { get; set; }
15 |
16 | [DataType(DataType.Password)]
17 | [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")]
18 | public string PasswordConfirm { get; set; }
19 |
20 | public string Code { get; set; }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/ReactBoilerplate/Scripts/containers/index.js:
--------------------------------------------------------------------------------
1 | export App from './App/App';
2 | export Home from './Home/Home';
3 | export About from './About/About';
4 | export Contact from './Contact/Contact';
5 | export NotFound from './NotFound/NotFound';
6 | export Register from './Register/Register';
7 | export Login from './Login/Login';
8 | export ForgotPassword from './ForgotPassword/ForgotPassword';
9 | export ResetPassword from './ResetPassword/ResetPassword';
10 | export ConfirmEmail from './ConfirmEmail/ConfirmEmail';
11 | export Manage from './Manage/Manage';
12 | export ManageIndex from './Manage/Index/Index';
13 | export ManageSecurity from './Manage/Security/Security';
14 | export ManageChangePassword from './Manage/ChangePassword/ChangePassword';
15 | export ManageLogins from './Manage/Logins/Logins';
16 | export ManageEmail from './Manage/Email/Email';
17 |
--------------------------------------------------------------------------------
/src/ReactBoilerplate/Models/ApplicationDbContext.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
2 | using Microsoft.EntityFrameworkCore;
3 |
4 | namespace ReactBoilerplate.Models
5 | {
6 | public class ApplicationDbContext : IdentityDbContext
7 | {
8 | public ApplicationDbContext(DbContextOptions options)
9 | : base(options)
10 | {
11 | }
12 |
13 | protected override void OnModelCreating(ModelBuilder builder)
14 | {
15 | base.OnModelCreating(builder);
16 | // Customize the ASP.NET Identity model and override the defaults if needed.
17 | // For example, you can rename the ASP.NET Identity table names and more.
18 | // Add your customizations after calling base.OnModelCreating(builder);
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/ReactBoilerplate/Scripts/helpers/ApiClient.js:
--------------------------------------------------------------------------------
1 | import superagent from 'superagent';
2 |
3 | const methods = ['get', 'post', 'put', 'patch', 'del'];
4 |
5 | class _ApiClient {
6 | constructor(req) {
7 | methods.forEach((method) => {
8 | this[method] = (path, { params, data } = {}) => new Promise((resolve, reject) => {
9 | const request = superagent[method](path);
10 |
11 | if (params) {
12 | request.query(params);
13 | }
14 |
15 | if (__SERVER__ && req.get('cookie')) {
16 | request.set('cookie', req.get('cookie'));
17 | }
18 |
19 | if (data) {
20 | request.send(data);
21 | }
22 |
23 | request.end((err, { body } = {}) => (err ? reject(body || err) : resolve(body)));
24 | });
25 | });
26 | }
27 | }
28 |
29 | const ApiClient = _ApiClient;
30 |
31 | export default ApiClient;
32 |
--------------------------------------------------------------------------------
/src/ReactBoilerplate/Controllers/Manage/Models/ChangePasswordModel.cs:
--------------------------------------------------------------------------------
1 | using System.ComponentModel.DataAnnotations;
2 | using Newtonsoft.Json;
3 |
4 | namespace ReactBoilerplate.Controllers.Manage.Models
5 | {
6 | public class ChangePasswordModel
7 | {
8 | [JsonProperty("oldPassword")]
9 | [Required]
10 | [DataType(DataType.Password)]
11 | public string OldPassword { get; set; }
12 |
13 | [JsonProperty("newPassword")]
14 | [Required]
15 | [StringLength(100, ErrorMessage = "The {0} must be at least {2} characters long.", MinimumLength = 6)]
16 | public string NewPassword { get; set; }
17 |
18 | [JsonProperty("newPasswordConfirm")]
19 | [Compare("NewPassword", ErrorMessage = "The new password and confirmation password do not match.")]
20 | public string NewPasswordConfirm { get; set; }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/ReactBoilerplate/Scripts/components/TwoFactor/TwoFactor.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { connect } from 'react-redux';
3 | import SendCodeForm from './SendCodeForm';
4 | import VerifyCodeForm from './VerifyCodeForm';
5 | import { resetLoginState } from 'redux/modules/account';
6 |
7 | class TwoFactor extends Component {
8 | componentWillUnmount() {
9 | this.props.resetLoginState();
10 | }
11 | render() {
12 | const {
13 | account: { requiresTwoFactor, userFactors, sentCode }
14 | } = this.props;
15 | if (sentCode) {
16 | return (
17 |
18 | );
19 | }
20 | if (requiresTwoFactor) {
21 | return (
22 |
23 | );
24 | }
25 | return (
);
26 | }
27 | }
28 |
29 | export default connect(
30 | (state) => ({ account: state.account }),
31 | { resetLoginState }
32 | )(TwoFactor);
33 |
--------------------------------------------------------------------------------
/src/ReactBoilerplate/Scripts/redux/modules/viewBag.js:
--------------------------------------------------------------------------------
1 | import { LOCATION_CHANGE } from 'react-router-redux';
2 |
3 | export default function reducer(state = { $internal: false }, action = {}) {
4 | switch (action.type) {
5 | case '_HYDRATE_VIEWBAG':
6 | // This initial data is provided by the server.
7 | // It exists for the initial request only.
8 | return {
9 | ...state,
10 | ...action.viewBag
11 | };
12 | case LOCATION_CHANGE:
13 | // This little snippet is a hack to prevent the viewBag
14 | // from being cleared on initial page load, but destroyed
15 | // when navigating to a different page.
16 | // https://github.com/reactjs/react-router-redux/issues/340
17 | if (state.$internal) return { $internal: true };
18 | return {
19 | ...state,
20 | $internal: true,
21 | ...action.viewBag
22 | };
23 | default:
24 | return state;
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/ReactBoilerplate/Scripts/utils/modelState.js:
--------------------------------------------------------------------------------
1 | import _ from 'lodash';
2 |
3 | /* eslint "import/prefer-default-export": 0 */
4 | /* eslint "no-prototype-builtins": 0 */
5 | /* eslint "no-underscore-dangle": [2, {"allow": ["_global", "_error" ]}] */
6 |
7 | // this method will map model state returned from an API, into an object
8 | // this is valid for passing to redux-forms for validation.
9 | export function modelStateErrorToFormFields(modelState) {
10 | if (!modelState) {
11 | return null;
12 | }
13 | let updatedModelState = _.omit(modelState, '_global');
14 | updatedModelState = _.mapValues(updatedModelState, (value) =>
15 | ({
16 | errors: value
17 | })
18 | );
19 | if (modelState.hasOwnProperty('_global') && modelState._global.length !== 0) {
20 | updatedModelState._error = {
21 | errors: modelState._global
22 | };
23 | } else {
24 | updatedModelState._error = null;
25 | }
26 | return updatedModelState;
27 | }
28 |
--------------------------------------------------------------------------------
/src/ReactBoilerplate/Scripts/redux/middleware/clientMiddleware.js:
--------------------------------------------------------------------------------
1 | export default function clientMiddleware(client) {
2 | return ({ dispatch, getState }) =>
3 | next => action => {
4 | if (typeof action === 'function') {
5 | return action(dispatch, getState);
6 | }
7 |
8 | const { promise, types, ...rest } = action; // eslint-disable-line no-use-before-define
9 | if (!promise) {
10 | return next(action);
11 | }
12 |
13 | const [REQUEST, SUCCESS, FAILURE] = types;
14 | next({ ...rest, type: REQUEST });
15 |
16 | const actionPromise = promise(client);
17 | actionPromise.then(
18 | (result) => next({ ...rest, result, type: SUCCESS }),
19 | (error) => next({ ...rest, error, type: FAILURE })
20 | ).catch((error) => {
21 | console.error('MIDDLEWARE ERROR:', error);
22 | console.error('MIDDLEWARE ERROR:', error.stack);
23 | next({ ...rest, error, type: FAILURE });
24 | });
25 |
26 | return actionPromise;
27 | };
28 | }
29 |
--------------------------------------------------------------------------------
/src/ReactBoilerplate/Properties/launchSettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "iisSettings": {
3 | "windowsAuthentication": false,
4 | "anonymousAuthentication": true,
5 | "iisExpress": {
6 | "applicationUrl": "http://localhost:15806/",
7 | "sslPort": 0
8 | }
9 | },
10 | "profiles": {
11 | "Development: IIS Express": {
12 | "commandName": "IISExpress",
13 | "launchBrowser": true,
14 | "environmentVariables": {
15 | "ASPNETCORE_ENVIRONMENT": "Development"
16 | }
17 | },
18 | "Development: .NET CLI": {
19 | "commandName": "Project",
20 | "launchBrowser": true,
21 | "environmentVariables": {
22 | "ASPNETCORE_ENVIRONMENT": "Development"
23 | },
24 | "applicationUrl": "http://localhost:15806/"
25 | },
26 | "Production: Kestrel": {
27 | "commandName": "Kestrel",
28 | "launchBrowser": true,
29 | "environmentVariables": {
30 | "ASPNETCORE_ENVIRONMENT": "Production"
31 | },
32 | "applicationUrl": "http://localhost:15806/"
33 | }
34 | }
35 | }
--------------------------------------------------------------------------------
/src/ReactBoilerplate/Controllers/Account/Models/RegisterModel.cs:
--------------------------------------------------------------------------------
1 | using System.ComponentModel.DataAnnotations;
2 | using Newtonsoft.Json;
3 |
4 | namespace ReactBoilerplate.Controllers.Account.Models
5 | {
6 | public class RegisterModel
7 | {
8 | [JsonProperty("userName")]
9 | [Required]
10 | public string UserName { get; set; }
11 |
12 | [JsonProperty("email")]
13 | [EmailAddress]
14 | [Required]
15 | public string Email { get; set; }
16 |
17 | [JsonProperty("password")]
18 | [Required]
19 | [StringLength(100, ErrorMessage = "The {0} must be at least {2} characters long.", MinimumLength = 6)]
20 | public string Password { get; set; }
21 |
22 | [JsonProperty("passwordConfirm")]
23 | [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")]
24 | public string PasswordConfirm { get; set; }
25 |
26 | [JsonProperty("linkExternalLogin")]
27 | public bool LinkExternalLogin { get; set; }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/ReactBoilerplate/Scripts/components/index.js:
--------------------------------------------------------------------------------
1 | export LoginForm from './LoginForm/LoginForm';
2 | export RegisterForm from './RegisterForm/RegisterForm';
3 | export ForgotPasswordForm from './ForgotPasswordForm/ForgotPasswordForm';
4 | export ResetPasswordForm from './ResetPasswordForm/ResetPasswordForm';
5 | export ChangePasswordForm from './ChangePasswordForm/ChangePasswordForm';
6 | export Input from './Input';
7 | export ExternalLogin from './ExternalLogin/ExternalLogin';
8 | export ExternalLoginButton from './ExternalLoginButton';
9 | export Spinner from './Spinner';
10 | export ErrorList from './ErrorList';
11 | export TwoFactor from './TwoFactor/TwoFactor';
12 | export ChangeEmailForm from './ChangeEmailForm/ChangeEmailForm';
13 | // There is a bug in babel. When exporting types that will be inherited,
14 | // you must import them directly from the component. You can't proxy
15 | // them like this index.js does.
16 | // http://stackoverflow.com/questions/28551582/traceur-runtime-super-expression-must-either-be-null-or-a-function-not-undefin
17 | // export Form from './Form';
18 |
--------------------------------------------------------------------------------
/src/ReactBoilerplate/Controllers/Home/HomeController.cs:
--------------------------------------------------------------------------------
1 | using System.Threading.Tasks;
2 | using Microsoft.AspNetCore.Identity;
3 | using Microsoft.AspNetCore.Mvc;
4 | using ReactBoilerplate.Models;
5 |
6 | namespace ReactBoilerplate.Controllers.Home
7 | {
8 | public class HomeController : BaseController
9 | {
10 | public HomeController(UserManager userManager,
11 | SignInManager signInManager)
12 | :base(userManager,
13 | signInManager)
14 | {
15 | }
16 |
17 | public async Task Index(string greeting = "Hello!")
18 | {
19 | return View("js-{auto}", await BuildState());
20 | }
21 |
22 | [Route("about")]
23 | public async Task About()
24 | {
25 | return View("js-{auto}", await BuildState());
26 | }
27 |
28 | [Route("contact")]
29 | public async Task Contact()
30 | {
31 | return View("js-{auto}", await BuildState());
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/ReactBoilerplate/State/Manage/ExternalLoginsState.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using Newtonsoft.Json;
3 |
4 | namespace ReactBoilerplate.State.Manage
5 | {
6 | public class ExternalLoginsState
7 | {
8 | public ExternalLoginsState()
9 | {
10 | CurrentLogins = new List();
11 | OtherLogins = new List();
12 | }
13 |
14 | [JsonProperty("currentLogins")]
15 | public List CurrentLogins { get; set; }
16 |
17 | [JsonProperty("otherLogins")]
18 | public List OtherLogins { get; set; }
19 |
20 | public class ExternalLogin
21 | {
22 | [JsonProperty("loginProvider")]
23 | public string LoginProvider { get; set; }
24 |
25 | [JsonProperty("loginProviderDisplayName")]
26 | public string LoginProviderDisplayName { get; set; }
27 |
28 | [JsonProperty("providerKey")]
29 | public string ProviderKey { get; set; }
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016 Paul Knopf
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/ReactBoilerplate/Scripts/containers/App/Modals/TwoFactorModal.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { connect } from 'react-redux';
3 | import { Modal, Button } from 'react-bootstrap';
4 | import { resetLoginState } from 'redux/modules/account';
5 | import { TwoFactor } from 'components';
6 |
7 | class TwoFactorModal extends Component {
8 | constructor(props) {
9 | super(props);
10 | this.close = this.close.bind(this);
11 | }
12 | close() {
13 | this.props.resetLoginState();
14 | }
15 | render() {
16 | const {
17 | requiresTwoFactor
18 | } = this.props.account;
19 | return (
20 |
21 |
22 | Security
23 |
24 |
25 |
26 |
27 |
28 | Close
29 |
30 |
31 | );
32 | }
33 | }
34 |
35 | export default connect(
36 | state => ({ account: state.account }),
37 | { resetLoginState }
38 | )(TwoFactorModal);
39 |
--------------------------------------------------------------------------------
/src/ReactBoilerplate/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "eslint-config-airbnb",
3 | "env": {
4 | "browser": true,
5 | "node": true
6 | },
7 | "parser": "babel-eslint",
8 | "plugins": [
9 | "react",
10 | "import"
11 | ],
12 | "rules": {
13 | "comma-dangle": 0, // not sure why airbnb turned this on. gross!
14 | "indent": [2, 2, {"SwitchCase": 1}],
15 | "react/prefer-stateless-function": 0,
16 | "react/prop-types": 0,
17 | "react/jsx-closing-bracket-location": 0,
18 | "no-console": 0,
19 | "prefer-template": 0,
20 | "max-len": 0,
21 | "no-underscore-dangle": [2, {"allow": ["__data"]}],
22 | "global-require": 0,
23 | "no-restricted-syntax": 0,
24 | "linebreak-style": 0,
25 | "react/jsx-filename-extension": 0,
26 | "import/imports-first": 0,
27 | "no-class-assign": 0
28 | },
29 | "settings": {
30 | "import/parser": "babel-eslint",
31 | "import/resolver": {
32 | "node": {
33 | "moduleDirectory": ["node_modules", "Scripts"]
34 | }
35 | },
36 | "no-underscore-dangle": {
37 | "allow": ["__data"]
38 | }
39 | },
40 | "globals": {
41 | "__CLIENT__": true,
42 | "__SERVER__": true
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/ReactBoilerplate/Scripts/components/TwoFactor/SendCodeForm.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Form from 'components/Form';
3 | import { reduxForm } from 'redux-form';
4 | import { Input } from 'components';
5 | import { sendCode } from 'redux/modules/account';
6 |
7 | class SendCodeForm extends Form {
8 | render() {
9 | const {
10 | fields: { provider },
11 | userFactors
12 | } = this.props;
13 | return (
14 |
23 | );
24 | }
25 | }
26 |
27 | SendCodeForm = reduxForm({
28 | form: 'sendCode',
29 | fields: ['provider']
30 | },
31 | (state) => ({
32 | userFactors: state.account.userFactors,
33 | initialValues: { provider: (state.account.userFactors.length > 0 ? state.account.userFactors[0] : '') }
34 | }),
35 | { }
36 | )(SendCodeForm);
37 |
38 | export default SendCodeForm;
39 |
--------------------------------------------------------------------------------
/src/ReactBoilerplate/Scripts/containers/Manage/Manage.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 | import { connect } from 'react-redux';
3 | import { Row, Col, Nav, NavItem } from 'react-bootstrap';
4 | import { LinkContainer } from 'react-router-bootstrap';
5 |
6 | class Manage extends Component {
7 | static propTypes = {
8 | children: PropTypes.object.isRequired
9 | };
10 | render() {
11 | return (
12 |
13 |
14 |
15 |
16 | Security
17 |
18 |
19 | Email
20 |
21 |
22 | Change password
23 |
24 |
25 | Manage logins
26 |
27 |
28 |
29 |
30 | {this.props.children}
31 |
32 |
33 | );
34 | }
35 | }
36 |
37 | export default connect(
38 | state => ({ user: state.auth.user }),
39 | { }
40 | )(Manage);
41 |
--------------------------------------------------------------------------------
/src/ReactBoilerplate/Scripts/components/ExternalLogin/ExternalLogin.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { connect } from 'react-redux';
3 | import { authenticate } from 'redux/modules/externalLogin';
4 | import { ExternalLoginButton } from 'components';
5 |
6 | class ExternalLogin extends Component {
7 | constructor(props) {
8 | super(props);
9 | this.loginClick = this.loginClick.bind(this);
10 | }
11 | loginClick(scheme) {
12 | return (event) => {
13 | event.preventDefault();
14 | this.props.authenticate(scheme);
15 | };
16 | }
17 | render() {
18 | const {
19 | loginProviders
20 | } = this.props;
21 | return (
22 |
23 | {loginProviders.map((loginProvider, i) =>
24 | (
25 |
26 |
30 | {' '}
31 |
32 | ))}
33 |
34 | );
35 | }
36 | }
37 |
38 | export default connect(
39 | (state) => ({ loginProviders: state.externalLogin.loginProviders, location: state.routing.locationBeforeTransitions }),
40 | { authenticate }
41 | )(ExternalLogin);
42 |
--------------------------------------------------------------------------------
/src/ReactBoilerplate/ReactBoilerplate.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netcoreapp2.0
5 | true
6 | ReactBoilerplate
7 | Exe
8 | ReactBoilerplate
9 | 89e1d495-c355-4990-bb58-6384b3b2e6df
10 | $(AssetTargetFallback);dotnet5.6;dnxcore50;portable-net45+win8
11 |
12 |
13 |
14 |
15 | PreserveNewest
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/src/ReactBoilerplate/Scripts/redux/modules/auth.js:
--------------------------------------------------------------------------------
1 | import { REGISTER_COMPLETE, LOGIN_COMPLETE, LOGOFF_COMPLETE, VERIFYCODE_COMPLETE } from './account';
2 | import { EXTERNALAUTHENTICATE_COMPLETE } from './externalLogin';
3 |
4 | const initialState = {
5 | loggedIn: false,
6 | user: null
7 | };
8 |
9 | export default function reducer(state = initialState, action = {}) {
10 | switch (action.type) {
11 | case REGISTER_COMPLETE:
12 | case LOGIN_COMPLETE:
13 | if (!action.result.success) {
14 | return state;
15 | }
16 | return {
17 | ...state,
18 | user: action.result.user,
19 | loggedIn: true
20 | };
21 | case LOGOFF_COMPLETE:
22 | if (!action.result.success) {
23 | return state;
24 | }
25 | return {
26 | ...state,
27 | user: null,
28 | loggedIn: false
29 | };
30 | case EXTERNALAUTHENTICATE_COMPLETE:
31 | if (action.result.signedIn) {
32 | return {
33 | ...state,
34 | user: action.result.user,
35 | loggedIn: true
36 | };
37 | }
38 | return state;
39 | case VERIFYCODE_COMPLETE:
40 | if (action.result.success) {
41 | return {
42 | ...state,
43 | user: action.result.user,
44 | loggedIn: true
45 | };
46 | }
47 | return state;
48 | default:
49 | return state;
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/ReactBoilerplate/Scripts/containers/ConfirmEmail/ConfirmEmail.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { connect } from 'react-redux';
3 | import { Link } from 'react-router';
4 |
5 | class ConfirmEmail extends Component {
6 | render() {
7 | const {
8 | success,
9 | change // was this a result of a change of an email
10 | } = this.props;
11 | return (
12 |
13 | {!success &&
14 |
15 |
Error.
16 | An error occurred while processing your request.
17 |
18 | }
19 | {success &&
20 |
21 | {change &&
22 |
23 |
Change email
24 |
25 | Your email was succesfully changed.
26 |
27 |
28 | }
29 | {!change &&
30 |
31 |
Confirm email
32 |
33 | Thank you for confirming your email.
34 | Please Click here to Log in.
35 |
36 |
37 | }
38 |
39 | }
40 |
41 | );
42 | }
43 | }
44 |
45 | export default connect(
46 | (state) => ({ success: state.viewBag.success, change: state.viewBag.change }),
47 | { }
48 | )(ConfirmEmail);
49 |
--------------------------------------------------------------------------------
/src/ReactBoilerplate/Controllers/Manage/ServerController.cs:
--------------------------------------------------------------------------------
1 | using System.Threading.Tasks;
2 | using Microsoft.AspNetCore.Identity;
3 | using Microsoft.AspNetCore.Mvc;
4 | using ReactBoilerplate.Models;
5 |
6 | namespace ReactBoilerplate.Controllers.Manage
7 | {
8 | [Route("manage")]
9 | public class ServerController : BaseController
10 | {
11 | public ServerController(UserManager userManager,
12 | SignInManager signInManager)
13 | :base(userManager,
14 | signInManager)
15 | {
16 | }
17 |
18 | public async Task Index()
19 | {
20 | return View("js-{auto}", await BuildState());
21 | }
22 |
23 | [Route("security")]
24 | public async Task Security()
25 | {
26 | return View("js-{auto}", await BuildState());
27 | }
28 |
29 | [Route("email")]
30 | public async Task Email()
31 | {
32 | return View("js-{auto}", await BuildState());
33 | }
34 |
35 | [Route("changepassword")]
36 | public async Task ChangePassword()
37 | {
38 | return View("js-{auto}", await BuildState());
39 | }
40 |
41 | [Route("logins")]
42 | public async Task Logins()
43 | {
44 | return View("js-{auto}", await BuildState());
45 | }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/ReactBoilerplate/Scripts/components/Form.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { modelStateErrorToFormFields } from '../utils/modelState';
3 | import { ErrorList } from 'components';
4 |
5 | class Form extends Component {
6 | modifyValues(values) {
7 | return values;
8 | }
9 | handleApiSubmit(action, success, error) {
10 | const {
11 | handleSubmit
12 | } = this.props;
13 | return handleSubmit((values, dispatch) =>
14 | new Promise((resolve, reject) => {
15 | dispatch(action(this.modifyValues(values)))
16 | .then(
17 | (result) => {
18 | if (result.success) {
19 | resolve();
20 | if (success) {
21 | success(result);
22 | }
23 | } else {
24 | reject(modelStateErrorToFormFields(result.errors));
25 | if (error) {
26 | error(result);
27 | }
28 | }
29 | },
30 | (result) => {
31 | reject(modelStateErrorToFormFields(result.errors));
32 | if (error) {
33 | error(result);
34 | }
35 | });
36 | })
37 | );
38 | }
39 | renderGlobalErrorList() {
40 | const {
41 | error
42 | } = this.props;
43 | if (!error) {
44 | return null;
45 | }
46 | if (!error.errors) {
47 | return null;
48 | }
49 | return ( );
50 | }
51 | }
52 |
53 | export default Form;
54 |
--------------------------------------------------------------------------------
/src/ReactBoilerplate/Scripts/components/LoginForm/LoginForm.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Form from 'components/Form';
3 | import { reduxForm } from 'redux-form';
4 | import { Input, ExternalLogin } from 'components';
5 | import { login } from 'redux/modules/account';
6 | import { Row, Col } from 'react-bootstrap';
7 |
8 | class LoginForm extends Form {
9 | render() {
10 | const {
11 | fields: { userName, password },
12 | loginProviders
13 | } = this.props;
14 | return (
15 |
34 | );
35 | }
36 | }
37 |
38 | LoginForm = reduxForm({
39 | form: 'login',
40 | fields: ['userName', 'password', 'rememberMe']
41 | },
42 | (state) => ({ loginProviders: state.externalLogin.loginProviders }),
43 | { }
44 | )(LoginForm);
45 |
46 | export default LoginForm;
47 |
--------------------------------------------------------------------------------
/src/ReactBoilerplate/Scripts/components/TwoFactor/VerifyCodeForm.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Form from 'components/Form';
3 | import { reduxForm } from 'redux-form';
4 | import { Input } from 'components';
5 | import { verifyCode } from 'redux/modules/account';
6 |
7 | class VerifyCodeForm extends Form {
8 | modifyValues(values) {
9 | return {
10 | ...values,
11 | provider: this.props.sentCodeWithProvider
12 | };
13 | }
14 | render() {
15 | const {
16 | fields: { code, rememberMe, rememberBrowser }
17 | } = this.props;
18 | return (
19 |
30 | );
31 | }
32 | }
33 |
34 | VerifyCodeForm = reduxForm({
35 | form: 'verifyCode',
36 | fields: ['code', 'rememberMe', 'rememberBrowser']
37 | },
38 | (state) => ({
39 | sentCodeWithProvider: state.account.sentCodeWithProvider,
40 | initialValues: {
41 | rememberMe: true,
42 | rememberBrowser: true
43 | }
44 | }),
45 | { }
46 | )(VerifyCodeForm);
47 |
48 | export default VerifyCodeForm;
49 |
--------------------------------------------------------------------------------
/src/ReactBoilerplate/Scripts/helpers/Html.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 | import ReactDOM from 'react-dom/server';
3 | import Helmet from 'react-helmet';
4 | import serialize from 'serialize-javascript';
5 |
6 | export default class Html extends Component {
7 | static propTypes = {
8 | component: PropTypes.node,
9 | store: PropTypes.object
10 | };
11 |
12 | render() {
13 | const { component, store } = this.props;
14 | const content = component ? ReactDOM.renderToString(component) : '';
15 | const head = Helmet.rewind();
16 |
17 | return (
18 |
19 |
20 | {head.base.toComponent()}
21 | {head.title.toComponent()}
22 | {head.meta.toComponent()}
23 | {head.link.toComponent()}
24 | {head.script.toComponent()}
25 |
26 |
27 |
34 |
35 |
36 |
37 |
41 |
42 |
43 |
44 | );
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
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/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ASP.NET",
3 | "version": "0.0.0",
4 | "devDependencies": {},
5 | "dependencies": {
6 | "babel-core": "^6.11.4",
7 | "babel-eslint": "^6.1.2",
8 | "babel-loader": "^6.2.4",
9 | "babel-preset-es2015": "^6.9.0",
10 | "babel-preset-react": "^6.11.1",
11 | "babel-preset-stage-0": "^6.5.0",
12 | "bootstrap-loader": "^1.1.0",
13 | "bootstrap-sass": "^3.3.7",
14 | "bootstrap-social": "^4.11.0",
15 | "classnames": "^2.2.3",
16 | "css-loader": "^0.23.1",
17 | "eslint": "^3.2.2",
18 | "eslint-config-airbnb": "^10.0.0",
19 | "eslint-loader": "^1.5.0",
20 | "eslint-plugin-import": "^1.12.0",
21 | "eslint-plugin-jsx-a11y": "^2.0.1",
22 | "eslint-plugin-react": "^6.0.0",
23 | "extract-text-webpack-plugin": "^1.0.1",
24 | "file-loader": "^0.9.0",
25 | "font-awesome": "^4.5.0",
26 | "gulp": "^3.9.1",
27 | "gulp-util": "^3.0.7",
28 | "lodash": "^4.14.1",
29 | "node-sass": "^4.9.0",
30 | "promise-window": "^1.0.0",
31 | "react": "^15.3.0",
32 | "react-bootstrap": "^0.30.1",
33 | "react-dom": "^15.3.0",
34 | "react-helmet": "^3.1.0",
35 | "react-redux": "^4.4.5",
36 | "react-router": "^2.6.1",
37 | "react-router-bootstrap": "^0.23.1",
38 | "react-router-redux": "^4.0.5",
39 | "redux": "^3.5.2",
40 | "redux-form": "^4.2.2",
41 | "redux-thunk": "^2.1.0",
42 | "resolve-url-loader": "^1.6.0",
43 | "sass-loader": "^4.0.0",
44 | "serialize-javascript": "^1.3.0",
45 | "style-loader": "^0.13.1",
46 | "superagent": "^2.1.0",
47 | "url-loader": "^0.5.7",
48 | "webpack": "^1.13.1"
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/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 |
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/redux/modules/manage/email.js:
--------------------------------------------------------------------------------
1 | export const LOADEMAIL_START = 'react/manage/LOADEMAIL_START';
2 | export const LOADEMAIL_COMPLETE = 'react/manage/LOADEMAIL_COMPLETE';
3 | export const LOADEMAIL_ERROR = 'react/manage/LOADEMAIL_ERROR';
4 |
5 | export const CHANGEEMAIL_START = 'react/manage/CHANGEEMAIL_START';
6 | export const CHANGEEMAIL_COMPLETE = 'react/manage/CHANGEEMAIL_COMPLETE';
7 | export const CHANGEEMAIL_ERROR = 'react/manage/CHANGEEMAIL_ERROR';
8 |
9 | export const VERIFYEMAIL_START = 'react/manage/VERIFYEMAIL_START';
10 | export const VERIFYEMAIL_COMPLETE = 'react/manage/VERIFYEMAIL_COMPLETE';
11 | export const VERIFYEMAIL_ERROR = 'react/manage/VERIFYEMAIL_ERROR';
12 |
13 | export const EMAIL_DESTROY = 'react/manage/EMAIL_DESTROY';
14 |
15 | const initialState = {
16 |
17 | };
18 |
19 | export default function (state = initialState, action = {}) {
20 | switch (action.type) {
21 | case LOADEMAIL_COMPLETE:
22 | return {
23 | ...state,
24 | ...action.result
25 | };
26 | case EMAIL_DESTROY:
27 | return initialState;
28 | default:
29 | return state;
30 | }
31 | }
32 |
33 | export function loadEmail() {
34 | return {
35 | types: [LOADEMAIL_START, LOADEMAIL_COMPLETE, LOADEMAIL_ERROR],
36 | promise: (client) => client.post('/api/manage/email')
37 | };
38 | }
39 |
40 | export function destroyEmail() {
41 | return { type: EMAIL_DESTROY };
42 | }
43 |
44 | export function changeEmail(body) {
45 | return {
46 | types: [CHANGEEMAIL_START, CHANGEEMAIL_COMPLETE, CHANGEEMAIL_ERROR],
47 | promise: (client) => client.post('/api/manage/changeemail', { data: body })
48 | };
49 | }
50 |
51 | export function verifyEmail(body) {
52 | return {
53 | types: [VERIFYEMAIL_START, VERIFYEMAIL_COMPLETE, VERIFYEMAIL_ERROR],
54 | promise: (client) => client.post('/api/manage/verifyemail', { data: body })
55 | };
56 | }
57 |
--------------------------------------------------------------------------------
/src/ReactBoilerplate/gulpfile.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | var isProduction = process.env.NODE_ENV == 'production';
4 | console.log('isProduction=' + isProduction);
5 |
6 | var gulp = require('gulp');
7 | var gutil = require('gulp-util');
8 | var extend = require('lodash').extend;
9 | var webpack = require('webpack');
10 | var webpackConfig = {
11 | config: require("./Scripts/webpack/" + (isProduction ? "prod" : "dev") + ".config.js")
12 | };
13 |
14 | gulp.task('default', ['build']);
15 | gulp.task('build', ['build-client', 'build-server']);
16 | gulp.task('watch', ['watch-client', 'watch-server'])
17 |
18 | // client
19 | gulp.task('watch-client', ['build-client'], function(){
20 | gulp.watch(["Scripts/**/*"], ["build-client"]);
21 | })
22 | gulp.task('build-client-compiler', function() {
23 | if(!webpackConfig.clientCompiler)
24 | webpackConfig.clientCompiler = webpack(webpackConfig.config.client);
25 | });
26 | gulp.task('build-client', ['build-client-compiler'], function(cb) {
27 | webpackConfig.clientCompiler.run(function(err, stats) {
28 | if(err) throw new gutil.PluginError("build-client", err);
29 | gutil.log("[build-client]", stats.toString({
30 | colors: true,
31 | chunks: false
32 | }));
33 | cb();
34 | });
35 | });
36 |
37 | // server
38 | gulp.task('watch-server', ['build-server'], function(){
39 | gulp.watch(["Scripts/**/*"], ["build-server"]);
40 | })
41 | gulp.task('build-server-compiler', function() {
42 | if(!webpackConfig.serverCompiler)
43 | webpackConfig.serverCompiler = webpack(webpackConfig.config.server);
44 | });
45 | gulp.task('build-server', ['build-server-compiler'], function(cb) {
46 | webpackConfig.serverCompiler.run(function(err, stats) {
47 | if(err) throw new gutil.PluginError("build-server", err);
48 | gutil.log("[build-server]", stats.toString({
49 | colors: true,
50 | chunks: false
51 | }));
52 | cb();
53 | });
54 | });
55 |
--------------------------------------------------------------------------------
/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 |
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/routes.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { IndexRoute, Route } from 'react-router';
3 | import {
4 | App,
5 | Home,
6 | About,
7 | Contact,
8 | NotFound,
9 | Register,
10 | Login,
11 | ForgotPassword,
12 | ResetPassword,
13 | ConfirmEmail,
14 | Manage,
15 | ManageIndex,
16 | ManageSecurity,
17 | ManageChangePassword,
18 | ManageLogins,
19 | ManageEmail
20 | } from './containers';
21 |
22 | export default (store) => {
23 | const requireLogin = (nextState, replace, cb) => {
24 | const { auth: { user } } = store.getState();
25 | if (!user) {
26 | // oops, not logged in, so can't be here!
27 | replace('/login?returnUrl=' +
28 | encodeURIComponent(nextState.location.pathname + nextState.location.search));
29 | }
30 | cb();
31 | };
32 | return (
33 |
34 | { /* Home (main) route */ }
35 |
36 |
37 | { /* Routes */ }
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 | { /* Catch all route */ }
55 |
56 |
57 | );
58 | };
59 |
--------------------------------------------------------------------------------
/src/ReactBoilerplate/Scripts/redux/modules/manage/security.js:
--------------------------------------------------------------------------------
1 | export const LOADSECURITY_START = 'react/manage/LOADSECURITY_START';
2 | export const LOADSECURITY_COMPLETE = 'react/manage/LOADSECURITY_COMPLETE';
3 | export const LOADSECURITY_ERROR = 'react/manage/LOADSECURITY_ERROR';
4 |
5 | export const SETTWOFACTOR_START = 'react/manage/SETTWOFACTOR_START';
6 | export const SETTWOFACTOR_COMPLETE = 'react/manage/SETTWOFACTOR_COMPLETE';
7 | export const SETTWOFACTOR_ERROR = 'react/manage/SETTWOFACTOR_ERROR';
8 |
9 | export const SECURITY_DESTROY = 'react/manage/SECURITY_DESTROY';
10 |
11 | const initialState = {
12 |
13 | };
14 |
15 | export default function (state = initialState, action = {}) {
16 | switch (action.type) {
17 | case LOADSECURITY_COMPLETE:
18 | return {
19 | ...state,
20 | ...action.result
21 | };
22 | case SETTWOFACTOR_START:
23 | return {
24 | ...state,
25 | settingTwoFactor: true
26 | };
27 | case SETTWOFACTOR_COMPLETE:
28 | return {
29 | ...state,
30 | twoFactorEnabled: action.result.twoFactorEnabled,
31 | settingTwoFactor: false
32 | };
33 | case SETTWOFACTOR_ERROR:
34 | return {
35 | ...state,
36 | settingTwoFactor: false
37 | };
38 | case SECURITY_DESTROY:
39 | return initialState;
40 | default:
41 | return state;
42 | }
43 | }
44 |
45 | export function loadSecurity() {
46 | return {
47 | types: [LOADSECURITY_START, LOADSECURITY_COMPLETE, LOADSECURITY_ERROR],
48 | promise: (client) => client.post('/api/manage/security')
49 | };
50 | }
51 |
52 | export function setTwoFactor(enabled) {
53 | return {
54 | types: [SETTWOFACTOR_START, SETTWOFACTOR_COMPLETE, SETTWOFACTOR_ERROR],
55 | promise: (client) => client.post('/api/manage/settwofactor', { data: { enabled } })
56 | };
57 | }
58 |
59 | export function destroySecurity() {
60 | return { type: SECURITY_DESTROY };
61 | }
62 |
--------------------------------------------------------------------------------
/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 |
42 | {twoFactorEnabled ? 'Disable' : 'Enable'}
43 |
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/server.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom/server';
3 | import Html from './helpers/Html';
4 | import { match } from 'react-router';
5 | import getRoutes from './routes';
6 | import createHistory from 'react-router/lib/createMemoryHistory';
7 | import RouterContext from 'react-router/lib/RouterContext';
8 | import configureStore from './redux/configureStore';
9 | import { Provider } from 'react-redux';
10 | import isEmpty from 'utils/isEmpty';
11 |
12 | export function renderView(callback, path, model, viewBag) {
13 | const history = createHistory(path);
14 | const store = configureStore(model, history);
15 | const result = {
16 | html: null,
17 | status: 404,
18 | redirect: null
19 | };
20 | match(
21 | { history, routes: getRoutes(store), location: path },
22 | (error, redirectLocation, renderProps) => {
23 | if (redirectLocation) {
24 | result.redirect = redirectLocation.pathname + redirectLocation.search;
25 | } else if (error) {
26 | result.status = 500;
27 | } else if (renderProps) {
28 | // if this is the NotFoundRoute, then return a 404
29 | const isNotFound = renderProps.routes.filter((route) => route.status === 404).length > 0;
30 | result.status = isNotFound ? 404 : 200;
31 | const component =
32 | (
33 |
34 |
35 |
36 | );
37 | if (!isEmpty(viewBag)) {
38 | // If the server provided anyhting in ASP.NET's ViewBag, hydrate it to the store/state.
39 | // The contents can be accessed on the client via `state.viewBag`. It exist for the initial
40 | // page load only, and will be cleared when navigating to another page on the client.
41 | store.dispatch({ type: '_HYDRATE_VIEWBAG', viewBag });
42 | }
43 | result.html = ReactDOM.renderToString( );
44 | } else {
45 | result.status = 404;
46 | }
47 | });
48 | callback(null, result);
49 | }
50 |
51 | export function renderPartialView(callback) {
52 | callback('TODO', null);
53 | }
54 |
--------------------------------------------------------------------------------
/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 |
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/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 |
Current email
47 |
50 |
51 |
52 | {!emailConfirmed &&
53 |
54 | Your email is not verified.
55 |
56 |
59 | Verify
60 |
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/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/redux/modules/manage/externalLogins.js:
--------------------------------------------------------------------------------
1 | export const LOADEXTERNALLOGINS_START = 'react/manage/LOADEXTERNALLOGINS_START';
2 | export const LOADEXTERNALLOGINS_COMPLETE = 'react/manage/LOADEXTERNALLOGINS_COMPLETE';
3 | export const LOADEXTERNALLOGINS_ERROR = 'react/manage/LOADEXTERNALLOGINS_ERROR';
4 |
5 | export const ADDEXTERNALLOGIN_START = 'react/manage/ADDEXTERNALLOGIN_START';
6 | export const ADDEXTERNALLOGIN_COMPLETE = 'react/manage/ADDEXTERNALLOGIN_COMPLETE';
7 | export const ADDEXTERNALLOGIN_ERROR = 'react/manage/ADDEXTERNALLOGIN_ERROR';
8 |
9 | export const REMOVEEXTERNALLOGIN_START = 'react/manage/REMOVEEXTERNALLOGIN_START';
10 | export const REMOVEEXTERNALLOGIN_COMPLETE = 'react/manage/REMOVEEXTERNALLOGIN_COMPLETE';
11 | export const REMOVEEXTERNALLOGIN_ERROR = 'react/manage/REMOVEEXTERNALLOGIN_ERROR';
12 |
13 | export const EXTERNALLOGINS_DESTROY = 'react/manage/EXTERNALLOGINS_DESTROY';
14 |
15 | export default function (state = {}, action = {}) {
16 | switch (action.type) {
17 | case LOADEXTERNALLOGINS_START:
18 | return {
19 | ...state,
20 | loading: true
21 | };
22 | case LOADEXTERNALLOGINS_COMPLETE:
23 | case ADDEXTERNALLOGIN_COMPLETE:
24 | case REMOVEEXTERNALLOGIN_COMPLETE:
25 | return {
26 | ...state,
27 | loading: false,
28 | ...action.result.externalLogins,
29 | errors: action.result.errors
30 | };
31 | case LOADEXTERNALLOGINS_ERROR:
32 | return {
33 | ...state,
34 | loading: false,
35 | ...action.result.externalLogins
36 | };
37 | case EXTERNALLOGINS_DESTROY:
38 | return {};
39 | default:
40 | return state;
41 | }
42 | }
43 |
44 | export function loadExternalLogins() {
45 | return {
46 | types: [LOADEXTERNALLOGINS_START, LOADEXTERNALLOGINS_COMPLETE, LOADEXTERNALLOGINS_ERROR],
47 | promise: (client) => client.post('/api/manage/externallogins')
48 | };
49 | }
50 |
51 | export function destroyExternalLogins() {
52 | return { type: EXTERNALLOGINS_DESTROY };
53 | }
54 |
55 | export function addExternalLogin() {
56 | return {
57 | types: [ADDEXTERNALLOGIN_START, ADDEXTERNALLOGIN_COMPLETE, ADDEXTERNALLOGIN_ERROR],
58 | promise: (client) => client.post('/api/manage/addexternallogin')
59 | };
60 | }
61 |
62 | export function removeExternalLogin(body) {
63 | return {
64 | types: [REMOVEEXTERNALLOGIN_START, REMOVEEXTERNALLOGIN_COMPLETE, REMOVEEXTERNALLOGIN_ERROR],
65 | promise: (client) => client.post('/api/manage/removeexternallogin', { data: body })
66 | };
67 | }
68 |
--------------------------------------------------------------------------------
/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/Scripts/webpack/dev.config.js:
--------------------------------------------------------------------------------
1 | var fs = require('fs');
2 | var babelrc = JSON.parse(fs.readFileSync('./.babelrc'));
3 | var ExtractTextPlugin = require('extract-text-webpack-plugin');
4 | var extractCSS = new ExtractTextPlugin('styles.css');
5 | var webpack = require('webpack');
6 | var path = require('path');
7 |
8 | module.exports = {
9 | server: {
10 | entry: {
11 | server: [
12 | path.resolve(__dirname, '..', '..', 'Scripts', 'server.js')
13 | ]
14 | },
15 | resolve: {
16 | modulesDirectories: [
17 | 'Scripts',
18 | 'node_modules'
19 | ],
20 | alias: {
21 | 'superagent': path.resolve(__dirname, '..', 'utils', 'superagent-server.js'),
22 | 'promise-window': path.resolve(__dirname, '..', 'utils', 'promise-window-server.js')
23 | }
24 | },
25 | module: {
26 | loaders: [
27 | { test: /\.jsx?$/, exclude: /node_modules/, loaders: ['babel?' + JSON.stringify(babelrc), 'eslint'] },
28 | { test: /\.css$/, loader: 'css/locals?module' },
29 | { test: /\.scss$/, loader: 'css/locals?module!sass' },
30 | { test: /\.(woff2?|ttf|eot|svg)(\?v=[0-9]\.[0-9]\.[0-9])?$/, loader: 'file' },
31 | { test: /\.(jpeg|jpeg|gif|png|tiff)(\?v=[0-9]\.[0-9]\.[0-9])?$/, loader: 'file' }
32 | ]
33 | },
34 | output: {
35 | filename: '[name].generated.js',
36 | libraryTarget: 'this',
37 | path: path.resolve(__dirname, '..', '..', 'wwwroot', 'pack'),
38 | publicPath: '/pack/'
39 | },
40 | plugins: [
41 | new webpack.DefinePlugin({
42 | __CLIENT__: false,
43 | __SERVER__: true
44 | })
45 | ],
46 | },
47 | client: {
48 | entry: {
49 | client: [
50 | 'bootstrap-loader',
51 | path.resolve(__dirname, '..', '..', 'Scripts', 'client.js')
52 | ]
53 | },
54 | resolve: {
55 | modulesDirectories: [
56 | 'Scripts',
57 | 'node_modules'
58 | ]
59 | },
60 | module: {
61 | loaders: [
62 | { test: /\.jsx?$/, exclude: /node_modules/, loaders: ['babel?' + JSON.stringify(babelrc), 'eslint'] },
63 | { test: /\.css$/, loader: extractCSS.extract('style', 'css?modules') },
64 | { test: /\.scss$/, loader: extractCSS.extract('style', 'css?modules!sass') },
65 | { test: /\.(woff2?|ttf|eot|svg)(\?v=[0-9]\.[0-9]\.[0-9])?$/, loader: 'file' },
66 | { test: /\.(jpeg|jpeg|gif|png|tiff)(\?v=[0-9]\.[0-9]\.[0-9])?$/, loader: 'file' }
67 | ]
68 | },
69 | output: {
70 | filename: '[name].generated.js',
71 | libraryTarget: 'this',
72 | path: path.resolve(__dirname, '..', '..', 'wwwroot', 'pack'),
73 | publicPath: '/pack/'
74 | },
75 | plugins: [
76 | extractCSS,
77 | new webpack.DefinePlugin({
78 | __CLIENT__: true,
79 | __SERVER__: false
80 | })
81 | ],
82 | devtool: 'source-map'
83 | }
84 | };
85 |
--------------------------------------------------------------------------------
/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 |
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/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 |
18 |
19 |
20 | Learn how to build ASP.NET apps that can run anywhere.
21 |
22 | Learn More
23 |
24 |
25 |
26 |
27 |
28 |
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 |
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 |
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/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 |
31 |
32 | Hello {user.userName}!
33 |
34 |
35 | Log off
36 |
37 |
38 | );
39 | }
40 | renderAnonymousLinks() {
41 | return (
42 |
43 |
44 | Register
45 |
46 |
47 | Login
48 |
49 |
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 |
76 |
77 | Home
78 |
79 |
80 | About
81 |
82 |
83 | Contact
84 |
85 |
86 | {loginLinks}
87 |
88 |
89 |
90 | {this.props.children}
91 |
92 |
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/webpack/prod.config.js:
--------------------------------------------------------------------------------
1 | var fs = require('fs');
2 | var babelrc = JSON.parse(fs.readFileSync('./.babelrc'));
3 | var ExtractTextPlugin = require('extract-text-webpack-plugin');
4 | var extractCSS = new ExtractTextPlugin('styles.css');
5 | var webpack = require('webpack');
6 | var path = require('path');
7 |
8 | module.exports = {
9 | server : {
10 | entry: {
11 | server: [
12 | path.resolve(__dirname, '..', '..', 'Scripts', 'server.js')
13 | ]
14 | },
15 | resolve: {
16 | modulesDirectories: [
17 | 'Scripts',
18 | 'node_modules'
19 | ],
20 | alias: {
21 | 'superagent': path.resolve(__dirname, '..', 'utils', 'superagent-server.js'),
22 | 'promise-window': path.resolve(__dirname, '..', 'utils', 'promise-window-server.js')
23 | },
24 | },
25 | module: {
26 | loaders: [
27 | { test: /\.jsx?$/, exclude: /node_modules/, loaders: ['babel?' + JSON.stringify(babelrc), 'eslint'] },
28 | { test: /\.css$/, loader: 'css/locals?module' },
29 | { test: /\.scss$/, loader: 'css/locals?module!sass' },
30 | { test: /\.(woff2?|ttf|eot|svg)(\?v=[0-9]\.[0-9]\.[0-9])?$/, loader: 'file' },
31 | { test: /\.(jpeg|jpeg|gif|png|tiff)(\?v=[0-9]\.[0-9]\.[0-9])?$/, loader: 'file' }
32 | ]
33 | },
34 | output: {
35 | filename: '[name].generated.js',
36 | libraryTarget: 'this',
37 | path: path.resolve(__dirname, '..', '..', 'wwwroot', 'pack'),
38 | publicPath: '/pack/'
39 | },
40 | plugins: [
41 | new webpack.optimize.DedupePlugin(),
42 | new webpack.optimize.OccurenceOrderPlugin(),
43 | new webpack.optimize.UglifyJsPlugin({
44 | compress: {
45 | warnings: false
46 | }
47 | }),
48 | new webpack.DefinePlugin({
49 | 'process.env': {
50 | NODE_ENV: '"production"'
51 | },
52 | __CLIENT__: false,
53 | __SERVER__: true
54 | })
55 | ]
56 | },
57 | client: {
58 | entry: {
59 | 'client': [
60 | 'bootstrap-loader',
61 | path.resolve(__dirname, '..', '..', 'Scripts', 'client.js')
62 | ]
63 | },
64 | resolve: {
65 | modulesDirectories: [
66 | 'Scripts',
67 | 'node_modules'
68 | ]
69 | },
70 | module: {
71 | loaders: [
72 | { test: /\.jsx?$/, exclude: /node_modules/, loaders: ['babel?' + JSON.stringify(babelrc), 'eslint'] },
73 | { test: /\.css$/, loader: extractCSS.extract('style', 'css?modules') },
74 | { test: /\.scss$/, loader: extractCSS.extract('style', 'css?modules!sass') },
75 | { test: /\.(woff2?|ttf|eot|svg)(\?v=[0-9]\.[0-9]\.[0-9])?$/, loader: 'file' },
76 | { test: /\.(jpeg|jpeg|gif|png|tiff)(\?v=[0-9]\.[0-9]\.[0-9])?$/, loader: 'file' }
77 | ]
78 | },
79 | output: {
80 | filename: '[name].generated.js',
81 | libraryTarget: 'this',
82 | path: path.resolve(__dirname, '..', '..', 'wwwroot', 'pack'),
83 | publicPath: '/pack/'
84 | },
85 | plugins: [
86 | extractCSS,
87 | new webpack.optimize.DedupePlugin(),
88 | new webpack.optimize.OccurenceOrderPlugin(),
89 | new webpack.optimize.UglifyJsPlugin({
90 | compress: {
91 | warnings: false
92 | }
93 | }),
94 | new webpack.DefinePlugin({
95 | 'process.env': {
96 | NODE_ENV: '"production"'
97 | },
98 | __CLIENT__: true,
99 | __SERVER__: false
100 | })
101 | ]
102 | }
103 | };
104 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
78 | {options.map((option, i) =>
79 | (
80 | {option.display}
81 | ))}
82 |
83 | );
84 | }
85 | renderCheckBox() {
86 | return (
87 |
88 |
89 | {this.props.label}
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 |
{this.props.label}
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/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 |
67 |
68 | Remove
69 |
70 | {' '}
71 |
75 |
76 |
77 | ))}
78 |
79 |
80 |
81 | }
82 | {(otherLogins.length > 0) &&
83 |
84 |
Add another service to log in.
85 |
86 |
87 | {otherLogins.map((otherLogin, i) =>
88 | (
89 |
90 |
91 |
92 | Add
93 |
94 | {' '}
95 |
99 |
100 |
101 | ))}
102 |
103 |
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/redux/modules/externalLogin.js:
--------------------------------------------------------------------------------
1 | import promiseWindow from 'promise-window';
2 | import { LOGOFF_COMPLETE, LOGINSTATE_RESET } from 'redux/modules/account';
3 | import { LOCATION_CHANGE } from 'react-router-redux';
4 |
5 | export const EXTERNALAUTHENTICATE_START = 'react/externalLogin/EXTERNALAUTHENTICATE_START';
6 | export const EXTERNALAUTHENTICATE_COMPLETE = 'react/externalLogin/EXTERNALAUTHENTICATE_COMPLETE';
7 | export const EXTERNALAUTHENTICATE_ERROR = 'react/externalLogin/EXTERNALAUTHENTICATE_ERROR';
8 |
9 | export const EXTERNALAUTHENTICATE_CLEAR = 'react/externalLogin/EXTERNALAUTHENTICATE_CLEAR';
10 | export const EXTERNALAUTHENTICATE_REHYDATE = 'react/externalLogin/EXTERNALAUTHENTICATE_REHYDATE';
11 |
12 | function popupWindowSize(provider) {
13 | switch (provider.toLowerCase()) {
14 | case 'facebook':
15 | return { width: 580, height: 400 };
16 | case 'google':
17 | return { width: 452, height: 633 };
18 | case 'github':
19 | return { width: 1020, height: 618 };
20 | case 'linkedin':
21 | return { width: 527, height: 582 };
22 | case 'twitter':
23 | return { width: 495, height: 645 };
24 | case 'live':
25 | return { width: 500, height: 560 };
26 | case 'yahoo':
27 | return { width: 559, height: 519 };
28 | default:
29 | return { width: 1020, height: 618 };
30 | }
31 | }
32 |
33 | const initialState = {
34 | loginProviders: [], // it is up to the server to provide these values
35 | externalAuthenticated: false,
36 | externalAuthenticatedProvider: null,
37 | signInError: false,
38 | proposedEmail: '',
39 | proposedUserName: ''
40 | };
41 |
42 | export default function reducer(state = initialState, action = {}) {
43 | switch (action.type) {
44 | case EXTERNALAUTHENTICATE_REHYDATE:
45 | return {
46 | ...state,
47 | ...action.result
48 | };
49 | case EXTERNALAUTHENTICATE_COMPLETE:
50 | if (action.result.signInError) {
51 | // We have a valid account with this external login,
52 | // but we couldn't login for some reason.
53 | // We don't want to store this login though, because
54 | // either a two-factor modal will popup will show,
55 | // or lockout message will show to the user.
56 | // Either way, there is no reason to store this external
57 | // login. We can't register with it, or login with it.
58 | return {
59 | ...state,
60 | externalAuthenticated: false,
61 | externalAuthenticatedProvider: null,
62 | signInError: false,
63 | proposedEmail: '',
64 | proposedUserName: ''
65 | };
66 | }
67 | return {
68 | ...state,
69 | externalAuthenticated: action.result.externalAuthenticated,
70 | externalAuthenticatedProvider: action.result.loginProvider,
71 | signInError: action.result.signInError,
72 | proposedEmail: action.result.proposedEmail,
73 | proposedUserName: action.result.proposedUserName
74 | };
75 | case EXTERNALAUTHENTICATE_CLEAR: // when some requests to clear any previously stored external authentications
76 | case LOGOFF_COMPLETE: // when a user logs off
77 | case LOCATION_CHANGE: // when the user navigates to different pages, we want to clear the user's logged in provider.
78 | case LOGINSTATE_RESET:
79 | // let's clear out the any previously stored external authentications that were done on the client.
80 | return {
81 | ...state,
82 | externalAuthenticated: false,
83 | externalAuthenticatedProvider: null,
84 | signInError: false,
85 | proposedEmail: '',
86 | proposedUserName: ''
87 | };
88 | default:
89 | return state;
90 | }
91 | }
92 |
93 | export function authenticate(provider, autoLogin = true) {
94 | return {
95 | types: [EXTERNALAUTHENTICATE_START, EXTERNALAUTHENTICATE_COMPLETE, EXTERNALAUTHENTICATE_ERROR],
96 | promise: () => new Promise((result, reject) => {
97 | const windowSize = popupWindowSize(provider);
98 | promiseWindow.open('/externalloginredirect?provider=' + provider + '&autoLogin=' + autoLogin, { ...windowSize })
99 | .then((windowResult) => {
100 | result(windowResult);
101 | }, () => {
102 | reject({});
103 | });
104 | })
105 | };
106 | }
107 |
108 | export function rehydrateLogin(login) {
109 | return {
110 | type: EXTERNALAUTHENTICATE_REHYDATE,
111 | result: login
112 | };
113 | }
114 |
115 | export function clearAuthentication() {
116 | return {
117 | type: EXTERNALAUTHENTICATE_CLEAR
118 | };
119 | }
120 |
--------------------------------------------------------------------------------
/src/ReactBoilerplate/Scripts/redux/modules/account.js:
--------------------------------------------------------------------------------
1 | export const REGISTER_START = 'react/account/REGISTER_START';
2 | export const REGISTER_COMPLETE = 'react/account/REGISTER_COMPLETE';
3 | export const REGISTER_ERROR = 'react/account/REGISTER_ERROR';
4 |
5 | export const LOGIN_START = 'react/account/LOGIN_START';
6 | export const LOGIN_COMPLETE = 'react/account/LOGIN_COMPLETE';
7 | export const LOGIN_ERROR = 'react/account/LOGIN_ERROR';
8 |
9 | export const LOGOFF_START = 'react/account/LOGOFF_START';
10 | export const LOGOFF_COMPLETE = 'react/account/LOGOFF_COMPLETE';
11 | export const LOGOFF_ERROR = 'react/account/LOGOFF_ERROR';
12 |
13 | export const FORGOTPASSWORD_START = 'react/account/FORGOTPASSWORD_START';
14 | export const FORGOTPASSWORD_COMPLETE = 'react/account/FORGOTPASSWORD_COMPLETE';
15 | export const FORGOTPASSWORD_ERROR = 'react/account/FORGOTPASSWORD_ERROR';
16 |
17 | export const RESETPASSWORD_START = 'react/account/RESETPASSWORD_START';
18 | export const RESETPASSWORD_COMPLETE = 'react/account/RESETPASSWORD_COMPLETE';
19 | export const RESETPASSWORD_ERROR = 'react/account/RESETPASSWORD_ERROR';
20 |
21 | export const SENDCODE_START = 'react/account/SENDCODE_START';
22 | export const SENDCODE_COMPLETE = 'react/account/SENDCODE_COMPLETE';
23 | export const SENDCODE_ERROR = 'react/account/SENDCODE_ERROR';
24 |
25 | export const VERIFYCODE_START = 'react/account/VERIFYCODE_START';
26 | export const VERIFYCODE_COMPLETE = 'react/account/VERIFYCODE_COMPLETE';
27 | export const VERIFYCODE_ERROR = 'react/account/VERIFYCODE_ERROR';
28 |
29 | export const LOGINSTATE_RESET = 'react/account/LOGINSTATE_RESET';
30 |
31 | import { EXTERNALAUTHENTICATE_COMPLETE } from 'redux/modules/externalLogin';
32 |
33 | const initialState = {
34 | sentCode: false,
35 | sentCodeWithProvider: null,
36 | userFactors: null,
37 | requiresTwoFactor: false
38 | };
39 |
40 | export default function reducer(state = initialState, action = {}) {
41 | switch (action.type) {
42 | case LOGINSTATE_RESET:
43 | return initialState;
44 | case LOGIN_COMPLETE:
45 | return {
46 | ...state,
47 | userFactors: action.result.userFactors,
48 | requiresTwoFactor: action.result.requiresTwoFactor
49 | };
50 | case SENDCODE_COMPLETE:
51 | return {
52 | ...state,
53 | sentCode: action.result.success,
54 | sentCodeWithProvider: action.result.provider
55 | };
56 | case EXTERNALAUTHENTICATE_COMPLETE:
57 | if (action.result.requiresTwoFactor) {
58 | return {
59 | ...state,
60 | userFactors: action.result.userFactors,
61 | requiresTwoFactor: true
62 | };
63 | }
64 | return state;
65 | case VERIFYCODE_COMPLETE:
66 | if (action.result.success) {
67 | return initialState; // we logged the user in, reset all the two-factor stuff
68 | }
69 | return state;
70 | default:
71 | return state;
72 | }
73 | }
74 |
75 | export function register(body) {
76 | return {
77 | types: [REGISTER_START, REGISTER_COMPLETE, REGISTER_ERROR],
78 | promise: (client) => client.post('/api/account/register', { data: body })
79 | };
80 | }
81 |
82 | export function login(body) {
83 | return {
84 | types: [LOGIN_START, LOGIN_COMPLETE, LOGIN_ERROR],
85 | promise: (client) => client.post('/api/account/login', { data: body })
86 | };
87 | }
88 |
89 | export function logoff() {
90 | return {
91 | types: [LOGOFF_START, LOGOFF_COMPLETE, LOGOFF_ERROR],
92 | promise: (client) => client.post('/api/account/logoff')
93 | };
94 | }
95 |
96 | export function forgotPassword(body) {
97 | return {
98 | types: [FORGOTPASSWORD_START, FORGOTPASSWORD_COMPLETE, FORGOTPASSWORD_ERROR],
99 | promise: (client) => client.post('/api/account/forgotpassword', { data: body })
100 | };
101 | }
102 |
103 | export function resetPassword(body) {
104 | return {
105 | types: [RESETPASSWORD_START, RESETPASSWORD_COMPLETE, RESETPASSWORD_ERROR],
106 | promise: (client) => client.post('/api/account/resetpassword', { data: body })
107 | };
108 | }
109 |
110 | export function sendCode(body) {
111 | return {
112 | types: [SENDCODE_START, SENDCODE_COMPLETE, SENDCODE_ERROR],
113 | promise: (client) => client.post('/api/account/sendcode', { data: body })
114 | };
115 | }
116 |
117 | export function verifyCode(body) {
118 | return {
119 | types: [VERIFYCODE_START, VERIFYCODE_COMPLETE, VERIFYCODE_ERROR],
120 | promise: (client) => client.post('/api/account/verifycode', { data: body })
121 | };
122 | }
123 |
124 | export function resetLoginState() {
125 | return {
126 | type: LOGINSTATE_RESET
127 | };
128 | }
129 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/src/ReactBoilerplate/Startup.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.IO;
4 | using JavaScriptViewEngine;
5 | using Microsoft.AspNetCore.Authentication.Cookies;
6 | using Microsoft.AspNetCore.Authentication.Facebook;
7 | using Microsoft.AspNetCore.Authentication.Google;
8 | using Microsoft.AspNetCore.Builder;
9 | using Microsoft.AspNetCore.Hosting;
10 | using Microsoft.AspNetCore.Http;
11 | using Microsoft.AspNetCore.Identity;
12 | using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
13 | using Microsoft.EntityFrameworkCore;
14 | using Microsoft.Extensions.Configuration;
15 | using Microsoft.Extensions.DependencyInjection;
16 | using Microsoft.Extensions.Logging;
17 | using ReactBoilerplate.Models;
18 | using ReactBoilerplate.Services;
19 |
20 | namespace ReactBoilerplate
21 | {
22 | public class Startup
23 | {
24 | IHostingEnvironment _env;
25 | IConfiguration _configuration;
26 | ILoggerFactory _loggerFactory;
27 |
28 | public Startup(IConfiguration configuration, IHostingEnvironment env)
29 | {
30 | _configuration = configuration;
31 | _env = env;
32 | }
33 |
34 | // This method gets called by the runtime. Use this method to add services to the container.
35 | public void ConfigureServices(IServiceCollection services)
36 | {
37 | services.AddJsEngine(builder =>
38 | {
39 | builder.UseSingletonEngineFactory();
40 | builder.UseNodeRenderEngine(nodeOptions =>
41 | {
42 | nodeOptions.ProjectDirectory = Path.Combine(_env.WebRootPath, "pack");
43 | nodeOptions.GetModuleName = (path, model, bag, values, area, type) => "server.generated";
44 | nodeOptions.NodeInstanceOutputLogger = _loggerFactory.CreateLogger("NodeRenderEngine");
45 | });
46 | });
47 | services.AddMvc();
48 |
49 | // Add framework services.
50 | services.AddDbContext(options =>
51 | options.UseSqlServer(_configuration.GetConnectionString("DefaultConnection")));
52 | // services.AddDbContext(options =>
53 | // options.UseNpgsql(Configuration.GetConnectionString("DefaultConnection")));
54 |
55 | services.AddIdentity()
56 | .AddEntityFrameworkStores()
57 | .AddDefaultTokenProviders();
58 |
59 | services.AddSingleton();
60 | services.AddSingleton();
61 |
62 | var authBuilder = services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
63 | .AddCookie(o => o.LoginPath = new PathString("/login"));
64 |
65 | var facebookAppId = _configuration["Authentication:Facebook:AppId"];
66 | var facebookAppSecret = _configuration["Authentication:Facebook:AppSecret"];
67 | if (!string.IsNullOrEmpty(facebookAppId) && !string.IsNullOrEmpty(facebookAppSecret))
68 | {
69 | authBuilder.AddFacebook(o =>
70 | {
71 | o.AppId = facebookAppId;
72 | o.AppSecret = facebookAppSecret;
73 | });
74 | }
75 |
76 | var googleClientId = _configuration["Authentication:Google:ClientId"];
77 | var googleClientSecret = _configuration["Authentication:Google:ClientSecret"];
78 | if (!string.IsNullOrEmpty(googleClientId) && !string.IsNullOrEmpty(googleClientSecret))
79 | {
80 | authBuilder.AddGoogle(o =>
81 | {
82 | o.ClientId = googleClientId;
83 | o.ClientSecret = googleClientSecret;
84 | o.Scope.Add("email");
85 | o.Scope.Add("profile");
86 | });
87 | }
88 | }
89 |
90 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
91 | public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
92 | {
93 | loggerFactory.AddConsole(_configuration.GetSection("Logging"));
94 | loggerFactory.AddDebug();
95 | _loggerFactory = loggerFactory;
96 |
97 | if (env.IsDevelopment())
98 | {
99 | app.UseDeveloperExceptionPage();
100 | app.UseDatabaseErrorPage();
101 | app.UseBrowserLink();
102 | }
103 | else
104 | {
105 | app.UseExceptionHandler("/Home/Error");
106 | }
107 |
108 | app.UseAuthentication();
109 |
110 | app.UseStatusCodePagesWithReExecute("/Status/Status/{0}");
111 |
112 | app.UseStaticFiles();
113 |
114 | app.UseJsEngine(); // gives a js engine to each request, required when using the JsViewEngine
115 |
116 | app.UseMvc(routes =>
117 | {
118 | routes.MapRoute(
119 | name: "default",
120 | template: "{controller=Home}/{action=Index}/{id?}");
121 | });
122 |
123 | app.Use((context, next) => {
124 | context.Response.StatusCode = 404;
125 | return next();
126 | });
127 | }
128 | }
129 | }
130 |
--------------------------------------------------------------------------------
/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/Manage/ApiController.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Linq;
3 | using System.Threading.Tasks;
4 | using Microsoft.AspNetCore.Authorization;
5 | using Microsoft.AspNetCore.Identity;
6 | using Microsoft.AspNetCore.Mvc;
7 | using ReactBoilerplate.Controllers.Manage.Models;
8 | using ReactBoilerplate.Models;
9 | using ReactBoilerplate.Services;
10 | using ReactBoilerplate.State;
11 | using ReactBoilerplate.State.Manage;
12 |
13 | namespace ReactBoilerplate.Controllers.Manage
14 | {
15 | [Authorize]
16 | [Route("api/manage")]
17 | public class ApiController : BaseController
18 | {
19 | private readonly UserManager _userManager;
20 | private readonly SignInManager _signInManager;
21 | private readonly IEmailSender _emailSender;
22 |
23 | public ApiController(UserManager userManager,
24 | SignInManager signInManager,
25 | IEmailSender emailSender)
26 | : base(userManager, signInManager)
27 | {
28 | _userManager = userManager;
29 | _signInManager = signInManager;
30 | _emailSender = emailSender;
31 | }
32 |
33 | [Route("security")]
34 | public async Task Security()
35 | {
36 | var user = await GetCurrentUserAsync();
37 |
38 | return new
39 | {
40 | twoFactorEnabled = await _userManager.GetTwoFactorEnabledAsync(user),
41 | validTwoFactorProviders = await _userManager.GetValidTwoFactorProvidersAsync(user),
42 | emailConfirmed = await _userManager.IsEmailConfirmedAsync(user)
43 | };
44 | }
45 |
46 | [Route("settwofactor")]
47 | public async Task SetTwoFactor([FromBody]SetTwoFactorModel model)
48 | {
49 | var user = await GetCurrentUserAsync();
50 |
51 | if (await _userManager.GetTwoFactorEnabledAsync(user) == model.Enabled)
52 | {
53 | // already set
54 | return new
55 | {
56 | success = true
57 | };
58 | }
59 |
60 | await _userManager.SetTwoFactorEnabledAsync(user, model.Enabled);
61 |
62 | return new
63 | {
64 | twoFactorEnabled = await _userManager.GetTwoFactorEnabledAsync(user),
65 | success = true
66 | };
67 | }
68 |
69 | [Route("email")]
70 | public async Task Email()
71 | {
72 | var user = await GetCurrentUserAsync();
73 |
74 | return new
75 | {
76 | email = await _userManager.GetEmailAsync(user),
77 | emailConfirmed = await _userManager.IsEmailConfirmedAsync(user)
78 | };
79 | }
80 |
81 | [Route("changeemail")]
82 | public async Task ChangeEmail([FromBody]ChangeEmailModel model)
83 | {
84 | if (!ModelState.IsValid)
85 | {
86 | return new
87 | {
88 | success = false,
89 | errors = GetModelState()
90 | };
91 | }
92 |
93 | var user = await GetCurrentUserAsync();
94 |
95 | if (!await _userManager.CheckPasswordAsync(user, model.CurrentPassword))
96 | {
97 | ModelState.AddModelError("currentPassword", "Invalid password.");
98 | return new
99 | {
100 | success = false,
101 | errors = GetModelState()
102 | };
103 | }
104 |
105 | // send an email to the user asking them to finish the change of email.
106 | var code = await _userManager.GenerateChangeEmailTokenAsync(user, model.Email);
107 | var callbackUrl = Url.RouteUrl("confirmemail", new { userId = user.Id, newEmail = model.Email, code = code }, protocol: HttpContext.Request.Scheme);
108 | await _emailSender.SendEmailAsync(model.Email, "Confirm your email change", "Please confirm your new email by clicking this link: link ");
109 |
110 | return new
111 | {
112 | success = true
113 | };
114 | }
115 |
116 | [Route("verifyemail")]
117 | public async Task VerifyEmail()
118 | {
119 | var user = await GetCurrentUserAsync();
120 |
121 | // send an email to the user asking them to finish the change of email.
122 | var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
123 | var callbackUrl = Url.RouteUrl("confirmemail", new { userId = user.Id, code = code }, protocol: HttpContext.Request.Scheme);
124 | await _emailSender.SendEmailAsync(user.Email, "Confirm your email change", "Please confirm your new email by clicking this link: link ");
125 |
126 | return new
127 | {
128 | success = true
129 | };
130 | }
131 |
132 | [Route("changepassword")]
133 | public async Task ChangePassword([FromBody]ChangePasswordModel model)
134 | {
135 | if (!ModelState.IsValid)
136 | {
137 | return new
138 | {
139 | success = false,
140 | errors = GetModelState()
141 | };
142 | }
143 | var user = await GetCurrentUserAsync();
144 | var result = await _userManager.ChangePasswordAsync(user, model.OldPassword, model.NewPassword);
145 | if (result.Succeeded)
146 | {
147 | await _signInManager.SignInAsync(user, isPersistent: false);
148 | return new
149 | {
150 | success = true
151 | };
152 | }
153 | foreach (var error in result.Errors)
154 | {
155 | ModelState.AddModelError(string.Empty, error.Description);
156 | }
157 | return new
158 | {
159 | success = false,
160 | errors = GetModelState()
161 | };
162 | }
163 |
164 | [Route("externallogins")]
165 | public async Task ExternalLogins()
166 | {
167 | return new
168 | {
169 | success = true,
170 | externalLogins = await GetExternalLoginsState()
171 | };
172 | }
173 |
174 | [Route("addexternallogin")]
175 | public async Task AddExternalLogin()
176 | {
177 | var user = await GetCurrentUserAsync();
178 | var info = await _signInManager.GetExternalLoginInfoAsync();
179 | if (info == null)
180 | {
181 | return new
182 | {
183 | success = false,
184 | externalLogins = await GetExternalLoginsState(),
185 | errors = new List { "Enable to authenticate with service." }
186 | };
187 | }
188 | var result = await _userManager.AddLoginAsync(user, info);
189 |
190 | if (result.Succeeded)
191 | {
192 | return new
193 | {
194 | success = true,
195 | externalLogins = await GetExternalLoginsState()
196 | };
197 | }
198 |
199 | return new
200 | {
201 | success = false,
202 | externalLogins = await GetExternalLoginsState(),
203 | errors = result.Errors.Select(x => x.Description)
204 | };
205 | }
206 |
207 | [Route("removeexternallogin")]
208 | public async Task RemoveExternalLogin([FromBody]RemoveExternalLoginModel model)
209 | {
210 | var user = await GetCurrentUserAsync();
211 | var result = await _userManager.RemoveLoginAsync(user, model.LoginProvider, model.ProviderKey);
212 | if (result.Succeeded)
213 | {
214 | await _signInManager.SignInAsync(user, isPersistent: false);
215 | return new
216 | {
217 | success = true,
218 | externalLogins = await GetExternalLoginsState()
219 | };
220 | }
221 | return new
222 | {
223 | success = false,
224 | error = result.Errors.Select(x => x.Description)
225 | };
226 | }
227 |
228 | private async Task GetExternalLoginsState()
229 | {
230 | var user = await GetCurrentUserAsync();
231 | var userLogins = await _userManager.GetLoginsAsync(user);
232 | var schemes = await _signInManager.GetExternalAuthenticationSchemesAsync();
233 | foreach (var userLogin in userLogins.Where(userLogin => string.IsNullOrEmpty(userLogin.ProviderDisplayName)))
234 | {
235 | userLogin.ProviderDisplayName =
236 | schemes
237 | .SingleOrDefault(x => x.Name.Equals(userLogin.LoginProvider))?
238 | .DisplayName;
239 | if (string.IsNullOrEmpty(userLogin.ProviderDisplayName))
240 | {
241 | userLogin.ProviderDisplayName = userLogin.LoginProvider;
242 | }
243 | }
244 | var otherLogins = schemes.Where(auth => userLogins.All(ul => auth.Name != ul.LoginProvider)).ToList();
245 |
246 | return new ExternalLoginsState
247 | {
248 | CurrentLogins = userLogins.Select(x => new ExternalLoginsState.ExternalLogin
249 | {
250 | ProviderKey = x.ProviderKey,
251 | LoginProvider = x.LoginProvider,
252 | LoginProviderDisplayName = x.ProviderDisplayName
253 | }).ToList(),
254 | OtherLogins = otherLogins.Select(x => new ExternalLoginState.ExternalLoginProvider
255 | {
256 | DisplayName = x.DisplayName,
257 | Scheme = x.Name
258 | }).ToList()
259 | };
260 | }
261 | }
262 | }
263 |
--------------------------------------------------------------------------------
/src/ReactBoilerplate/Controllers/Account/ApiController.cs:
--------------------------------------------------------------------------------
1 | using System.Threading.Tasks;
2 | using Microsoft.AspNetCore.Identity;
3 | using Microsoft.AspNetCore.Mvc;
4 | using ReactBoilerplate.Controllers.Account.Models;
5 | using ReactBoilerplate.Models;
6 | using ReactBoilerplate.Services;
7 |
8 | namespace ReactBoilerplate.Controllers.Account
9 | {
10 | [Route("api/account")]
11 | public class ApiController : BaseController
12 | {
13 | UserManager _userManager;
14 | SignInManager _signInManager;
15 | IEmailSender _emailSender;
16 | private readonly ISmsSender _smsSender;
17 |
18 | public ApiController(UserManager userManager,
19 | SignInManager signInManager,
20 | IEmailSender emailSender,
21 | ISmsSender smsSender
22 | )
23 | :base(userManager, signInManager)
24 | {
25 | _userManager = userManager;
26 | _signInManager = signInManager;
27 | _emailSender = emailSender;
28 | _smsSender = smsSender;
29 | }
30 |
31 | [Route("register")]
32 | [HttpPost]
33 | public async Task Register([FromBody]RegisterModel model)
34 | {
35 | ExternalLoginInfo externalLoginInfo = null;
36 | if (model.LinkExternalLogin)
37 | {
38 | externalLoginInfo = await _signInManager.GetExternalLoginInfoAsync();
39 | if (externalLoginInfo == null)
40 | {
41 | ModelState.AddModelError(string.Empty, "Unsuccessful login with service");
42 | }
43 | else
44 | {
45 | var existingLogin = await _userManager.FindByLoginAsync(externalLoginInfo.LoginProvider, externalLoginInfo.ProviderKey);
46 | if (existingLogin != null)
47 | {
48 | ModelState.AddModelError(string.Empty, "An account is already associated with this login server.");
49 | }
50 | }
51 | }
52 |
53 | if (ModelState.IsValid)
54 | {
55 | var user = new ApplicationUser { UserName = model.UserName, Email = model.Email };
56 | var result = await _userManager.CreateAsync(user, model.Password);
57 |
58 | if (result.Succeeded)
59 | {
60 | var code = await _userManager.GenerateEmailConfirmationTokenAsync(user);
61 | var callbackUrl = Url.RouteUrl("confirmemail", new { userId = user.Id, code = code }, protocol: HttpContext.Request.Scheme);
62 | await _emailSender.SendEmailAsync(model.Email, "Confirm your account", "Please confirm your account by clicking this link: link ");
63 |
64 | // add the external login to the account
65 | if (externalLoginInfo != null)
66 | {
67 | var addLoginResult = await _userManager.AddLoginAsync(user, externalLoginInfo);
68 | if (!addLoginResult.Succeeded)
69 | {
70 | foreach (var error in addLoginResult.Errors)
71 | {
72 | // TODO: log
73 | }
74 | }
75 | }
76 |
77 | await _signInManager.SignInAsync(user, false);
78 | return new
79 | {
80 | success = true,
81 | user = ReactBoilerplate.Models.Api.User.From(user)
82 | };
83 | }
84 | foreach (var error in result.Errors)
85 | {
86 | ModelState.AddModelError(string.Empty, error.Description);
87 | }
88 | }
89 |
90 | return new
91 | {
92 | success = false,
93 | errors = GetModelState()
94 | };
95 | }
96 |
97 | [Route("login")]
98 | public async Task Login([FromBody]LoginModel model)
99 | {
100 | if (ModelState.IsValid)
101 | {
102 | var user = await _userManager.FindByNameAsync(model.UserName);
103 | if (user == null)
104 | {
105 | ModelState.AddModelError("UserName", "No user found with the given user name.");
106 | return new
107 | {
108 | success = false,
109 | errors = GetModelState()
110 | };
111 | }
112 |
113 | var result = await _signInManager.PasswordSignInAsync(user, model.Password, model.RememberMe, lockoutOnFailure: false);
114 | if (result.Succeeded)
115 | {
116 | return new
117 | {
118 | success = true,
119 | user = ReactBoilerplate.Models.Api.User.From(user)
120 | };
121 | }
122 | if (result.RequiresTwoFactor)
123 | {
124 | var userFactors = await _userManager.GetValidTwoFactorProvidersAsync(user);
125 | return new
126 | {
127 | success = false,
128 | requiresTwoFactor = true,
129 | userFactors
130 | };
131 | }
132 | if (result.IsLockedOut)
133 | {
134 | ModelState.AddModelError(string.Empty, "You are currently locked out.");
135 | return new
136 | {
137 | success = false,
138 | errors = GetModelState()
139 | };
140 | }
141 |
142 | ModelState.AddModelError(string.Empty, "Invalid login attempt.");
143 | return new
144 | {
145 | success = false,
146 | errors = GetModelState()
147 | };
148 | }
149 |
150 | return new
151 | {
152 | success = false,
153 | errors = GetModelState()
154 | };
155 | }
156 |
157 | [Route("logoff")]
158 | [HttpPost]
159 | public async Task LogOff()
160 | {
161 | await _signInManager.SignOutAsync();
162 | return new
163 | {
164 | success = true
165 | };
166 | }
167 |
168 | [Route("forgotpassword")]
169 | [HttpPost]
170 | public async Task ForgotPassword([FromBody]ForgotPasswordModel model)
171 | {
172 | if (ModelState.IsValid)
173 | {
174 | var user = await _userManager.FindByEmailAsync(model.Email);
175 | if (user == null || !(await _userManager.IsEmailConfirmedAsync(user)))
176 | {
177 | // Don't reveal that the user does not exist or is not confirmed
178 | return new
179 | {
180 | success = true
181 | };
182 | }
183 |
184 | var code = await _userManager.GeneratePasswordResetTokenAsync(user);
185 | var callbackUrl = Url.RouteUrl("resetpassword", new { userId = user.Id, code = code }, protocol: HttpContext.Request.Scheme);
186 | await _emailSender.SendEmailAsync(model.Email, "Reset Password",
187 | "Please reset your password by clicking here: link ");
188 |
189 | return new
190 | {
191 | success = true
192 | };
193 | }
194 |
195 | return new
196 | {
197 | success = false,
198 | errors = GetModelState()
199 | };
200 | }
201 |
202 | [Route("resetpassword")]
203 | [HttpPost]
204 | public async Task ResetPassword([FromBody]ResetPasswordModel model)
205 | {
206 | if (!ModelState.IsValid)
207 | {
208 | return new
209 | {
210 | success = false,
211 | errors = GetModelState()
212 | };
213 | }
214 | var user = await _userManager.FindByEmailAsync(model.Email);
215 | if (user == null)
216 | {
217 | // Don't reveal that the user does not exist
218 | return new
219 | {
220 | success = true
221 | };
222 | }
223 | var result = await _userManager.ResetPasswordAsync(user, model.Code, model.Password);
224 | if (result.Succeeded)
225 | {
226 | return new
227 | {
228 | success = true
229 | };
230 | }
231 |
232 | foreach (var error in result.Errors)
233 | {
234 | ModelState.AddModelError(string.Empty, error.Description);
235 | }
236 |
237 | return new
238 | {
239 | success = false,
240 | errors = GetModelState()
241 | };
242 | }
243 |
244 | [Route("sendcode")]
245 | [HttpPost]
246 | public async Task SendCode([FromBody]SendCodeModel model)
247 | {
248 | if (!ModelState.IsValid)
249 | {
250 | return new
251 | {
252 | success = false,
253 | errors = GetModelState()
254 | };
255 | }
256 |
257 | var user = await _signInManager.GetTwoFactorAuthenticationUserAsync();
258 | if (user == null)
259 | {
260 | ModelState.AddModelError(string.Empty, "Invalid user.");
261 | return new
262 | {
263 | success = false,
264 | errors = GetModelState()
265 | };
266 | }
267 |
268 | var code = await _userManager.GenerateTwoFactorTokenAsync(user, model.Provider);
269 | if (string.IsNullOrEmpty(code))
270 | {
271 | ModelState.AddModelError(string.Empty, "Unknown error.");
272 | return new
273 | {
274 | success = false,
275 | errors = GetModelState()
276 | };
277 | }
278 |
279 | var message = "Your security code is: " + code;
280 | if (model.Provider == "Email")
281 | {
282 | await _emailSender.SendEmailAsync(await _userManager.GetEmailAsync(user), "Security Code", message);
283 | }
284 | else if (model.Provider == "Phone")
285 | {
286 | await _smsSender.SendSmsAsync(await _userManager.GetPhoneNumberAsync(user), message);
287 | }
288 |
289 | return new
290 | {
291 | provider = model.Provider,
292 | success = true
293 | };
294 | }
295 |
296 | [Route("verifycode")]
297 | public async Task VerifyCode([FromBody]VerifyCodeModel model)
298 | {
299 | if (!ModelState.IsValid)
300 | {
301 | return new
302 | {
303 | success = false,
304 | errors = GetModelState()
305 | };
306 | }
307 |
308 | var user = await _signInManager.GetTwoFactorAuthenticationUserAsync();
309 | if (user == null)
310 | {
311 | ModelState.AddModelError(string.Empty, "Invalid user.");
312 | return new
313 | {
314 | success = false,
315 | errors = GetModelState()
316 | };
317 | }
318 |
319 | var result = await _signInManager.TwoFactorSignInAsync(model.Provider, model.Code, model.RememberMe, model.RememberBrowser);
320 |
321 | if (result.Succeeded)
322 | {
323 | return new
324 | {
325 | success = true,
326 | user = ReactBoilerplate.Models.Api.User.From(user)
327 | };
328 | }
329 |
330 | ModelState.AddModelError(string.Empty, result.IsLockedOut ? "User account locked out." : "Invalid code.");
331 |
332 | return new
333 | {
334 | success = false,
335 | errors = GetModelState()
336 | };
337 | }
338 | }
339 | }
340 |
--------------------------------------------------------------------------------