├── .gitignore
├── .travis.yml
├── Procfile
├── app.json
├── client
├── actions
│ ├── authentication.js
│ ├── index.js
│ ├── login.js
│ ├── logout.js
│ └── user.js
├── api
│ ├── AuthService.js
│ ├── UserService.js
│ ├── helper.js
│ └── index.js
├── client.js
├── components
│ ├── App
│ │ ├── App.js
│ │ └── index.js
│ ├── Body
│ │ ├── Body.js
│ │ └── index.js
│ ├── ErrorContent
│ │ ├── ErrorContent.js
│ │ └── index.js
│ ├── Footer
│ │ ├── Footer.js
│ │ └── index.js
│ ├── Header
│ │ ├── Header.js
│ │ └── index.js
│ ├── Login
│ │ ├── Login.js
│ │ └── index.js
│ ├── Profile
│ │ ├── Profile.js
│ │ └── index.js
│ └── Register
│ │ ├── Register.js
│ │ ├── RegisterSuccess.js
│ │ └── index.js
├── config.js
├── containers
│ ├── AuthContainer.js
│ ├── LoginContainer.js
│ ├── ProfileContainer.js
│ ├── RegisterContainer.js
│ └── index.js
├── index.html
├── package.json
├── readme.md
├── reducers
│ ├── authentication.js
│ ├── index.js
│ ├── login.js
│ ├── logout.js
│ └── user.js
├── routes.js
├── sagas
│ ├── authentication.js
│ ├── index.js
│ ├── login.js
│ ├── logout.js
│ └── user.js
├── store
│ └── configureStore.js
├── stylesheets
│ ├── _mixins.scss
│ ├── _variables.scss
│ ├── components
│ │ ├── footer.scss
│ │ └── login.scss
│ └── style.scss
└── webpack.config.js
├── package.json
├── public
├── 448c34a56d699c29117adc64c43affeb.woff2
├── 674f50d287a8c48dc19ba404d20fe713.eot
├── 89889688147bd7575d6327160d64e760.svg
├── 912ec66d7572ff821749319396470bde.svg
├── af7ae505a9eed503f8b8e6982036873e.woff2
├── b06871f281fee6b241d60582ae9369b9.ttf
├── bundle.min.js
├── e18bbf611f2a2e43afc071aa2f4e1512.ttf
├── f4769f9bdb7466be65088239c12046d1.eot
├── fa2772327f55d8198301fdb8bcfc8158.woff
├── fee66e712a8a08eef5805a46892932ad.woff
└── index.html
├── readme.md
└── server
├── .eslintrc.js
├── config
├── database.js
└── initial.js
├── dev.js
├── middleware
├── jwtauth.js
└── token_manager.js
├── models
├── expireToken.js
├── role.js
└── user.js
├── package.json
├── routes
├── initial.js
└── users.js
├── run.js
├── services
├── error
│ └── builder.js
└── permissions
│ └── validator.js
└── test
├── all.test.js
└── routes
└── users.js
/.gitignore:
--------------------------------------------------------------------------------
1 | server/node_modules
2 | server/coverage
3 | server/npm-debug.log
4 | client/node_modules
5 | npm-debug.log
6 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - 7.0.0
4 | services:
5 | - mongodb
6 | before_script:
7 | - sleep 15
8 | - mongo user_auth_demo --eval 'db.createUser({user:"travis",pwd:"test",roles:["readWrite"]});'
9 | - cd server
10 | - npm install
11 | branches:
12 | only:
13 | - master
14 | script:
15 | - npm run test:coverage
16 | - cd ..
17 | notification:
18 | email:
19 | - willhanchen@gmail.com
20 | on_success: always
21 | on_failure: always
22 | deploy:
23 | provider: heroku
24 | api_key:
25 | secure: UDsPlgN9rMxFB+bR2U8LD1MIX4Fat7eNATy6nfFn33YXoVcxFdXbbVvLpW6RWkq4rutC5QNgryLNjWWc3YTPbSdqV9xzp4Jn0sv3Kmd6+Urp926Iw/NUvQ2tFnqAE9M4gR9SNR1COO87p9vMxSP+jn0KpFlCawNBBek/d9V2xHZIX+Ngh8d5Cv80a1fGlsJXjVgSAa92Vk1ih8pf9oSimy3PSpQtUtuurfMPIKLd9+zzNMlqtpAg5Imh4LDzBvxw6BijAvFkCqQZyq0vSzLiInsy0qX49OKRM7yebS/O4irATejwF4hZ9Xwcl6zgwy/QctB9a/6MHnQ783xZtpG4UrRG+5TAcnN5QEB3gPU8kTL1YtVl1x8vCAGd834eeyEBbP4C+BzrFvbyUJAit7IWRmwt167gMzppdVjpk3r3osa7RSMQjDNFD0dwj/BDvSFOgVL7LkvRD/K2/ajHw/cgaiAoOQu7tNJN19SSkvCdFqIKBdYfv8wEipTWb/sgE3KnjZJE5gVbRPLUcumGBTIm2DG3eA3RfkMNF2oiPkzn5Mnci23u3Eghf6zQ8X03N+cD9kCy62cme38Px15sFPDIV1tOEKN5rV760wz3X0x9+D9KImfcPwE7fw3lFBgVKNwlAMDwGcPtGtCBRIGJy4PSRByLqe/G3UOXlHu3fGZgR+E=
26 | app:
27 | master: user-authentication-nodejs
28 | on:
29 | repo: weihanchen/user-authentication-nodejs
30 |
--------------------------------------------------------------------------------
/Procfile:
--------------------------------------------------------------------------------
1 | web: npm start
2 |
--------------------------------------------------------------------------------
/app.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Node.js User Authentication",
3 | "description": "A user authentication Node.js api using React to demo",
4 | "repository": "https://github.com/weihanchen/user-authentication-nodejs",
5 | "keywords": ["node", "express", "authentication", "jwt", "react", "redux"]
6 | }
--------------------------------------------------------------------------------
/client/actions/authentication.js:
--------------------------------------------------------------------------------
1 | export const REQUEST_AUTHENTICATION = 'REQUEST_AUTHENTICATION'
2 | export const REQUEST_AUTHENTICATION_FAILD = 'REQUEST_AUTHENTICATION_FAILD'
3 | export const REQUEST_AUTHENTICATION_SUCCESS = 'REQUEST_AUTHENTICATION_SUCCESS'
4 |
5 | export function requestAuthentication(token) {
6 | return {
7 | type: REQUEST_AUTHENTICATION,
8 | token
9 | }
10 | }
11 |
12 | export function requestAuthenticationFaild() {
13 | return {
14 | type: REQUEST_AUTHENTICATION_FAILD
15 | }
16 | }
17 |
18 | export function requestAuthenticationSuccess() {
19 | return {
20 | type: REQUEST_AUTHENTICATION_SUCCESS
21 | }
22 | }
--------------------------------------------------------------------------------
/client/actions/index.js:
--------------------------------------------------------------------------------
1 | export * from './authentication'
2 | export * from './login'
3 | export * from './logout'
4 | export * from './user'
--------------------------------------------------------------------------------
/client/actions/login.js:
--------------------------------------------------------------------------------
1 | export const REQUEST_LOGIN = 'REQUEST_LOGIN'
2 | export const REQUEST_LOGIN_FAILD = 'REQUEST_LOGIN_FAILD'
3 | export const REQUEST_LOGIN_SUCCESS = 'REQUEST_LOGIN_SUCCESS'
4 | export const RESET_LOGIN_STATUS = 'RESET_LOGIN_STATUS'
5 |
6 | export function requestLogin(username, password) {
7 | return {
8 | type: REQUEST_LOGIN,
9 | username,
10 | password
11 | }
12 | }
13 |
14 | export function requestLoginFaild() {
15 | return {
16 | type: REQUEST_LOGIN_FAILD
17 | }
18 | }
19 |
20 | export function requestLoginSuccess() {
21 | return {
22 | type: REQUEST_LOGIN_SUCCESS
23 | }
24 | }
25 |
26 | export function resetLoginStatus() {
27 | return {
28 | type: RESET_LOGIN_STATUS
29 | }
30 | }
--------------------------------------------------------------------------------
/client/actions/logout.js:
--------------------------------------------------------------------------------
1 | export const REQUEST_LOGOUT = 'REQUEST_LOGOUT'
2 | export const REQUEST_LOGOUT_FAILD = 'REQUEST_LOGOUT_FAILD'
3 | export const REQUEST_LOGOUT_SUCCESS = 'REQUEST_LOGOUT_SUCCESS'
4 | export const RESET_LOGOUT_STATUS = 'RESET_LOGOUT_STATUS'
5 |
6 | export function requestLogout(token) {
7 | return {
8 | type: REQUEST_LOGOUT,
9 | token
10 | }
11 | }
12 |
13 | export function requestLogoutFaild() {
14 | return {
15 | type: REQUEST_LOGOUT_FAILD
16 | }
17 | }
18 |
19 | export function requestLogoutSuccess() {
20 | return {
21 | type: REQUEST_LOGOUT_SUCCESS
22 | }
23 | }
24 |
25 | export function resetLogoutStatus() {
26 | return {
27 | type: RESET_LOGOUT_STATUS
28 | }
29 | }
--------------------------------------------------------------------------------
/client/actions/user.js:
--------------------------------------------------------------------------------
1 | export const REQUEST_CURRENTUSER = 'REQUEST_CURRENTUSER'
2 | export const REQUEST_CURRENTUSER_SUCCESS = 'REQUEST_CURRENTUSER_SUCCESS'
3 | export const REQUEST_FAILD = 'REQUEST_FAILD'
4 | export const REQUEST_SIGNUP_USER = 'REQUEST_SIGNUP_USER'
5 | export const REQUEST_SIGNUP_USER_SUCCESS = 'REQUEST_SIGNUP_USER_SUCCESS'
6 | export const REQUEST_UPDATEUSER = 'REQUEST_UPDATEUSER'
7 | export const REQUEST_UPDATEUSER_SUCCESS = 'REQUEST_UPDATEUSER_SUCCESS'
8 | export const RESET_USER_STATUS = 'RESET_USER_STATUS'
9 |
10 | export function requestCurrentUser(token) {
11 | return {
12 | type: REQUEST_CURRENTUSER,
13 | token
14 | }
15 | }
16 |
17 | export function requestSignupUser(displayName, password, username) {
18 | return {
19 | type: REQUEST_SIGNUP_USER,
20 | displayName,
21 | password,
22 | username
23 | }
24 | }
25 |
26 | export function requestUpdateUser(token, user) {
27 | return {
28 | type: REQUEST_UPDATEUSER,
29 | token,
30 | user
31 | }
32 | }
33 |
34 | export function resetUserStatus() {
35 | return {
36 | type: RESET_USER_STATUS
37 | }
38 | }
--------------------------------------------------------------------------------
/client/api/AuthService.js:
--------------------------------------------------------------------------------
1 | import {
2 | checkStatus,
3 | parseJSON
4 | } from './helper.js'
5 |
6 | import {
7 | CURRENT_USER_URL,
8 | LOGIN_URL,
9 | LOGOUT_URL
10 | } from '../config'
11 |
12 | export default class AuthService {
13 | requestAuth(token) {
14 | const options = {
15 | method: 'HEAD',
16 | headers: new Headers({
17 | 'Authorization': token
18 | }),
19 | mode: 'cors'
20 | }
21 | return fetch(CURRENT_USER_URL, options)
22 | .then(checkStatus)
23 | .then(parseJSON)
24 | }
25 |
26 | requestLogin(username, password) {
27 | const options = {
28 | method: 'POST',
29 | headers: new Headers({
30 | 'Accept': 'application/json',
31 | 'Content-Type': 'application/json'
32 | }),
33 | mode: 'cors',
34 | body: JSON.stringify({
35 | username: username,
36 | password: password
37 | })
38 | }
39 | return fetch(LOGIN_URL, options)
40 | .then(checkStatus)
41 | .then(parseJSON)
42 | }
43 |
44 | requestLogout(token) {
45 | const options = {
46 | method: 'POST',
47 | headers: new Headers({
48 | 'Accept': 'application/json',
49 | 'Content-Type': 'application/json',
50 | 'Authorization': token
51 | }),
52 | mode: 'cors'
53 | }
54 | return fetch(LOGOUT_URL, options)
55 | .then(checkStatus)
56 | .then(parseJSON)
57 | }
58 | }
--------------------------------------------------------------------------------
/client/api/UserService.js:
--------------------------------------------------------------------------------
1 | import {
2 | checkStatus,
3 | parseJSON
4 | } from './helper.js'
5 |
6 | import {
7 | CURRENT_USER_URL,
8 | USERS_URL
9 | } from '../config'
10 |
11 | export default class UserService {
12 | requestCurrentUser(token) {
13 | const options = {
14 | method: 'GET',
15 | headers: new Headers({
16 | 'Authorization': token
17 | }),
18 | mode: 'cors'
19 | }
20 | return fetch(CURRENT_USER_URL, options)
21 | .then(checkStatus)
22 | .then(parseJSON)
23 | }
24 |
25 | requestSignupUser(displayName, password, username) {
26 | const options = {
27 | method: 'POST',
28 | headers: new Headers({
29 | 'Accept': 'application/json',
30 | 'Content-Type': 'application/json'
31 | }),
32 | mode: 'cors',
33 | body: JSON.stringify({
34 | displayName: displayName,
35 | password: password,
36 | username: username
37 | })
38 | }
39 | return fetch(USERS_URL, options)
40 | .then(checkStatus)
41 | .then(parseJSON)
42 |
43 |
44 | }
45 |
46 | requestUpdateUser(token, user) {
47 | const options = {
48 | method: 'PUT',
49 | headers: new Headers({
50 | 'Authorization': token
51 | }),
52 | body: JSON.stringify(user),
53 | mode: 'cors'
54 | }
55 | return fetch(`${USERS_URL}/${user.uid}`, options)
56 | .then(checkStatus)
57 | .then(parseJSON)
58 | }
59 | }
--------------------------------------------------------------------------------
/client/api/helper.js:
--------------------------------------------------------------------------------
1 | export function checkStatus(response) {
2 | if (response.status >= 200 && response.status < 300) {
3 | return response
4 | } else {
5 | return response.json()
6 | .then(json => {
7 | const errorMsg = json.hasOwnProperty('message') ? json.message : response.statusText
8 | const error = new Error(errorMsg)
9 | throw error
10 | })
11 | }
12 | }
13 |
14 | export function parseJSON(response) {
15 | return response.text()
16 | .then((text) => {
17 | return text ? JSON.parse(text) : {}
18 | })
19 |
20 | }
--------------------------------------------------------------------------------
/client/api/index.js:
--------------------------------------------------------------------------------
1 | import AuthService from './AuthService'
2 | import UserService from './UserService'
3 |
4 | export {
5 | AuthService,
6 | UserService
7 | }
--------------------------------------------------------------------------------
/client/client.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import {
3 | render
4 | } from 'react-dom'
5 | import {
6 | Router,
7 | browserHistory,
8 | hashHistory
9 | } from 'react-router'
10 | import {
11 | Provider
12 | } from 'react-redux'
13 | import injectTapEventPlugin from 'react-tap-event-plugin';
14 |
15 | import routes from './routes.js'
16 | import store from './store/configureStore'
17 |
18 | /* Stylesheets*/
19 | import 'bootstrap-sass/assets/stylesheets/_bootstrap.scss'
20 | import 'font-awesome/scss/font-awesome.scss'
21 | import './stylesheets/style.scss'
22 | injectTapEventPlugin();
23 |
24 | render(
25 |
,
30 | document.getElementById('root')
31 | )
--------------------------------------------------------------------------------
/client/components/App/App.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider';
3 | import getMuiTheme from 'material-ui/styles/getMuiTheme';
4 | import lightBaseTheme from 'material-ui/styles/baseThemes/lightBaseTheme';
5 | import * as Colors from 'material-ui/styles/colors';
6 | //import components
7 | import Header from '../Header'
8 | import Body from '../Body'
9 | import Footer from '../Footer'
10 |
11 | const muiTheme = getMuiTheme({
12 | palette: {
13 | primary1Color: Colors.teal400
14 |
15 | },
16 | });
17 |
18 | class App extends React.Component {
19 | constructor(props) {
20 | super(props)
21 | }
22 | render() {
23 | return (
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | )
32 | }
33 | }
34 |
35 | export default App
--------------------------------------------------------------------------------
/client/components/App/index.js:
--------------------------------------------------------------------------------
1 | import App from './App'
2 |
3 | export default App
--------------------------------------------------------------------------------
/client/components/Body/Body.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | class Body extends React.Component {
4 | constructor(props) {
5 | super(...arguments)
6 | }
7 | render() {
8 | return (
9 |
10 |
11 |
12 | {this.props.children}
13 |
14 |
15 |
16 | )
17 | }
18 | }
19 |
20 | export default Body
--------------------------------------------------------------------------------
/client/components/Body/index.js:
--------------------------------------------------------------------------------
1 | import Body from './Body'
2 |
3 | export default Body
--------------------------------------------------------------------------------
/client/components/ErrorContent/ErrorContent.js:
--------------------------------------------------------------------------------
1 | import React, {
2 | Component,
3 | PropTypes
4 | } from 'react'
5 | import {
6 | hashHistory
7 | } from 'react-router'
8 |
9 | class ErrorContent extends Component {
10 | handleLinkClick(e) {
11 | window.location.reload()
12 | }
13 |
14 | render() {
15 | const {
16 | message
17 | } = this.props
18 | return (
19 |
20 |
21 |
22 |
Oh Snap!
23 |
24 |
{message}
25 |
26 |
try it again?
27 |
28 | )
29 | }
30 | }
31 |
32 | ErrorContent.propTypes = {
33 | message: PropTypes.string
34 | }
35 |
36 | export default ErrorContent
--------------------------------------------------------------------------------
/client/components/ErrorContent/index.js:
--------------------------------------------------------------------------------
1 | import ErrorContent from './ErrorContent'
2 |
3 | export default ErrorContent
--------------------------------------------------------------------------------
/client/components/Footer/Footer.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import AppBar from 'material-ui/AppBar';
3 | class Footer extends React.Component {
4 | render() {
5 | return (
6 |
11 | )
12 | }
13 | }
14 |
15 | export default Footer
--------------------------------------------------------------------------------
/client/components/Footer/index.js:
--------------------------------------------------------------------------------
1 | import Footer from './Footer'
2 |
3 | export default Footer
--------------------------------------------------------------------------------
/client/components/Header/Header.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import {
3 | Link
4 | } from 'react-router'
5 | //components
6 | import AppBar from 'material-ui/AppBar';
7 |
8 | class Header extends React.Component {
9 | render() {
10 | return (
11 |
14 | )
15 | }
16 | }
17 |
18 | export default Header
--------------------------------------------------------------------------------
/client/components/Header/index.js:
--------------------------------------------------------------------------------
1 | import Header from './Header'
2 |
3 | export default Header
--------------------------------------------------------------------------------
/client/components/Login/Login.js:
--------------------------------------------------------------------------------
1 | import React, {
2 | Component,
3 | PropTypes
4 | } from 'react'
5 | import {
6 | Link
7 | } from 'react-router'
8 | import {
9 | Card,
10 | CardActions,
11 | CardHeader,
12 | CardMedia,
13 | CardTitle,
14 | CardText
15 | } from 'material-ui/Card'
16 | import * as Colors from 'material-ui/styles/colors'
17 | import TextField from 'material-ui/TextField'
18 | import RaisedButton from 'material-ui/RaisedButton'
19 |
20 |
21 |
22 | class Login extends Component {
23 | constructor(props) {
24 | super(props)
25 | this.state = {
26 | password: '',
27 | passwordErrorText: 'Password is required.',
28 | username: '',
29 | usernameErrorText: 'Username is required.',
30 | loginDisabled: true
31 | }
32 | }
33 |
34 | onLoginClicked() {
35 | const {
36 | handleLogin
37 | } = this.props
38 | const username = this.state.username
39 | const password = this.state.password
40 | handleLogin(username, password)
41 | }
42 |
43 | onPasswordChanged(e) {
44 | const password = e.target.value;
45 | const passwordErrorText = password.length > 0 ? null : 'Password is required.'
46 | const loginDisabled = passwordErrorText != null || this.state.usernameErrorText != null
47 | this.setState({
48 | password: e.target.value,
49 | passwordErrorText: passwordErrorText,
50 | loginDisabled: loginDisabled
51 | })
52 | }
53 |
54 | onUsernameChanged(e) {
55 | const username = e.target.value
56 | const usernameErrorText = username.length > 0 ? null : 'Username is required.'
57 | const loginDisabled = usernameErrorText != null || this.state.passwordErrorText != null
58 | this.setState({
59 | username: username,
60 | usernameErrorText: usernameErrorText,
61 | loginDisabled: loginDisabled
62 | })
63 | }
64 |
65 | render() {
66 | return (
67 |
68 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 | Not a member?Sign up now
80 |
81 |
82 | )
83 | }
84 | }
85 |
86 | Login.propTypes = {
87 | handleLogin: PropTypes.func
88 | }
89 |
90 | export default Login
--------------------------------------------------------------------------------
/client/components/Login/index.js:
--------------------------------------------------------------------------------
1 | import Login from './Login'
2 |
3 | export default Login
--------------------------------------------------------------------------------
/client/components/Profile/Profile.js:
--------------------------------------------------------------------------------
1 | import React, {
2 | Component,
3 | PropTypes
4 | } from 'react'
5 | import {
6 | Link
7 | } from 'react-router'
8 | //components
9 | import {
10 | Card,
11 | CardActions,
12 | CardHeader,
13 | CardMedia,
14 | CardTitle,
15 | CardText
16 | } from 'material-ui/Card'
17 | import RaisedButton from 'material-ui/RaisedButton'
18 | import TextField from 'material-ui/TextField'
19 | import FormsyText from 'formsy-material-ui/lib/FormsyText';
20 | import * as Colors from 'material-ui/styles/colors'
21 |
22 | class Profile extends Component {
23 | constructor(props) {
24 | super(props)
25 | const {
26 | displayName,
27 | role,
28 | uid,
29 | username
30 | } = this.props
31 | this.state = {
32 | displayName: displayName,
33 | role: role,
34 | uid: uid,
35 | username: username
36 | }
37 | }
38 |
39 | enableButton() {
40 | this.setState({
41 | canSubmit: true
42 | })
43 | }
44 |
45 | disableButton() {
46 | this.setState({
47 | canSubmit: false
48 | })
49 | }
50 |
51 | onFieldChanged(field, e) {
52 | const updateObject = {}
53 | updateObject[field] = e.target.value
54 | this.setState(updateObject)
55 | }
56 |
57 | onLogoutClicked() {
58 | const {
59 | handleLogout
60 | } = this.props
61 | handleLogout()
62 | }
63 |
64 | onUpdateClicked() {
65 | const {
66 | handleUpdateUser
67 | } = this.props
68 | handleUpdateUser(this.state.displayName, this.state.uid, this.state.username)
69 | }
70 |
71 | render() {
72 |
73 | return (
74 |
75 |
76 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 | )
93 | }
94 | }
95 |
96 | export default Profile
97 |
98 | Profile.propTypes = {
99 | displayName: PropTypes.string,
100 | handleLogout: PropTypes.func,
101 | handleUpdateUser: PropTypes.func,
102 | role: PropTypes.string,
103 | status: PropTypes.string,
104 | uid: PropTypes.string,
105 | username: PropTypes.string
106 | }
--------------------------------------------------------------------------------
/client/components/Profile/index.js:
--------------------------------------------------------------------------------
1 | import Profile from './Profile'
2 |
3 | export default Profile
--------------------------------------------------------------------------------
/client/components/Register/Register.js:
--------------------------------------------------------------------------------
1 | import React, {
2 | Component,
3 | PropTypes
4 | } from 'react'
5 | import {
6 | Link
7 | } from 'react-router'
8 | import {
9 | Card,
10 | CardActions,
11 | CardHeader,
12 | CardMedia,
13 | CardTitle,
14 | CardText
15 | } from 'material-ui/Card'
16 | import * as Colors from 'material-ui/styles/colors';
17 | import TextField from 'material-ui/TextField';
18 | import FormsyText from 'formsy-material-ui/lib/FormsyText';
19 | import RaisedButton from 'material-ui/RaisedButton';
20 | class Register extends Component {
21 | constructor(props) {
22 | super(props)
23 | this.state = {
24 | canSubmit: false,
25 | displayName: '',
26 | username: '',
27 | password: '',
28 | confirmPassword: '',
29 | confirmPasswordError: ''
30 | }
31 | }
32 |
33 | enableButton() {
34 | this.setState({
35 | canSubmit: true
36 | })
37 | }
38 |
39 | disableButton() {
40 | this.setState({
41 | canSubmit: false
42 | })
43 | }
44 |
45 | onFieldChanged(field, e) {
46 | const updateObject = {}
47 | updateObject[field] = e.target.value
48 | this.setState(updateObject)
49 | if (field === 'confirmPassword' && this.state.password != e.target.value) {
50 | this.setState({
51 | canSubmit: false,
52 | confirmPasswordError: 'Password do not match'
53 | })
54 | }
55 | if (field === 'confirmPassword' && this.state.password === e.target.value) {
56 | this.setState({
57 | canSubmit: true,
58 | confirmPasswordError: ''
59 | })
60 | }
61 | }
62 |
63 | onSignupUser() {
64 | this.props.handleSignupUser(this.state.displayName, this.state.password, this.state.username)
65 | }
66 |
67 | render() {
68 | return (
69 |
70 |
73 |
74 |
75 |
77 |
78 |
79 |
80 |
84 |
85 |
86 |
87 |
88 |
89 |
90 | Member Login
91 |
92 |
93 | )
94 | }
95 | }
96 |
97 | Register.propTypes = {
98 | handleSignupUser: PropTypes.func
99 | }
100 |
101 | export default Register
--------------------------------------------------------------------------------
/client/components/Register/RegisterSuccess.js:
--------------------------------------------------------------------------------
1 | import React, {
2 | Component,
3 | PropTypes
4 | } from 'react'
5 | import {
6 | Link
7 | } from 'react-router'
8 |
9 |
10 | class RegisterSuccess extends Component {
11 | render() {
12 | const {
13 | displayName,
14 | username
15 | } = this.props
16 | return (
17 |
18 |
19 |
20 |
Signup Success
21 |
22 |
Your displayName: {displayName}
23 |
Your username: {username}
24 |
25 |
I want to login now!
26 |
27 | )
28 | }
29 | }
30 |
31 | RegisterSuccess.propTypes = {
32 | displayName: PropTypes.string,
33 | username: PropTypes.string
34 | }
35 |
36 | export default RegisterSuccess
--------------------------------------------------------------------------------
/client/components/Register/index.js:
--------------------------------------------------------------------------------
1 | import Register from './Register'
2 | import RegisterSuccess from './RegisterSuccess'
3 |
4 | export default {
5 | Register,
6 | RegisterSuccess
7 | }
8 |
9 | export {
10 | Register,
11 | RegisterSuccess
12 | }
--------------------------------------------------------------------------------
/client/config.js:
--------------------------------------------------------------------------------
1 | export const API_ENDPOINT = process.env.NODE_ENV === 'production' ? `${window.location.origin}/api` : 'http://localhost:3000/api'
2 | export const CURRENT_USER_URL = `${API_ENDPOINT}/users/me`
3 | export const LOGIN_URL = `${API_ENDPOINT}/users/login`
4 | export const LOGOUT_URL = `${API_ENDPOINT}/users/logout`
5 | export const USERS_URL = `${API_ENDPOINT}/users`
--------------------------------------------------------------------------------
/client/containers/AuthContainer.js:
--------------------------------------------------------------------------------
1 | import React, {
2 | Component,
3 | PropTypes
4 | } from 'react'
5 | import {
6 | connect
7 | } from 'react-redux'
8 | import {
9 | bindActionCreators
10 | } from 'redux'
11 | import {
12 | hashHistory
13 | } from 'react-router'
14 | import LinearProgress from 'material-ui/LinearProgress';
15 | import {
16 | requestAuthentication
17 | } from '../actions'
18 |
19 | class AuthContainer extends Component {
20 |
21 | componentDidMount() {
22 | const token = localStorage.getItem('token')
23 | if (!token) hashHistory.push('/login')
24 | else this.props.requestAuthentication(token)
25 | }
26 |
27 | componentWillReceiveProps(nextProps) {
28 | const status = nextProps.authentication.status
29 | const statusFunction = {
30 | 'success': function() {
31 | hashHistory.push('/profile')
32 | },
33 | 'error': function() {
34 | hashHistory.push('/login')
35 | }
36 | }
37 | if (statusFunction.hasOwnProperty(status)) statusFunction[status]()
38 | }
39 |
40 | render() {
41 | return (
42 |
43 | )
44 | }
45 | }
46 |
47 | const mapStateToProps = (state) => {
48 | return {
49 | authentication: state.authentication
50 | }
51 | }
52 |
53 | const mapDispatchToProps = (dispatch) => {
54 | return bindActionCreators({
55 | requestAuthentication
56 | }, dispatch)
57 | }
58 |
59 | AuthContainer.propTypes = {
60 | requestAuthentication: PropTypes.func
61 | }
62 |
63 | export default connect(mapStateToProps, mapDispatchToProps)(AuthContainer)
--------------------------------------------------------------------------------
/client/containers/LoginContainer.js:
--------------------------------------------------------------------------------
1 | import React, {
2 | Component,
3 | PropTypes
4 | } from 'react'
5 | import {
6 | connect
7 | } from 'react-redux'
8 | import {
9 | hashHistory
10 | } from 'react-router'
11 | import {
12 | bindActionCreators
13 | } from 'redux'
14 | import {
15 | requestLogin,
16 | resetLoginStatus
17 | } from '../actions'
18 | import CircularProgress from 'material-ui/CircularProgress';
19 | //components
20 | import ErrorContent from '../components/ErrorContent'
21 | import Login from '../components/Login'
22 |
23 | class LoginContainer extends Component {
24 |
25 | constructor(props) {
26 | super(props)
27 | this.props.resetLoginStatus()
28 | }
29 |
30 | componentWillReceiveProps(nextProps) {
31 | const status = nextProps.login.status
32 | if (status === 'success') hashHistory.push('/profile')
33 | }
34 |
35 | handleLogin(username, password) {
36 | this.props.requestLogin(username, password)
37 | }
38 |
39 | render() {
40 | const {
41 | login
42 | } = this.props
43 | const renderStatus = {
44 | loading: function() {
45 | return (
46 |
47 |
)
48 | },
49 | error: function() {
50 | return
51 | }
52 | }
53 | if (renderStatus.hasOwnProperty(login.status)) return renderStatus[login.status]()
54 | return (
55 |
56 | )
57 | }
58 | }
59 |
60 | const mapStateToProps = (state) => {
61 | return {
62 | login: state.login
63 | }
64 | }
65 |
66 | const mapDispatchToProps = (dispatch) => {
67 | return bindActionCreators({
68 | requestLogin,
69 | resetLoginStatus
70 | }, dispatch)
71 | }
72 |
73 | LoginContainer.propTypes = {
74 | login: PropTypes.object,
75 | requestLogin: PropTypes.func,
76 | resetLoginStatus: PropTypes.func
77 | }
78 |
79 | export default connect(mapStateToProps, mapDispatchToProps)(LoginContainer)
--------------------------------------------------------------------------------
/client/containers/ProfileContainer.js:
--------------------------------------------------------------------------------
1 | import React, {
2 | Component,
3 | PropTypes
4 | } from 'react'
5 | import {
6 | Link
7 | } from 'react-router'
8 | import {
9 | connect
10 | } from 'react-redux'
11 | import {
12 | bindActionCreators
13 | } from 'redux'
14 | import {
15 | hashHistory
16 | } from 'react-router'
17 | import CircularProgress from 'material-ui/CircularProgress';
18 | import {
19 | requestCurrentUser,
20 | requestLogout,
21 | requestUpdateUser,
22 | resetLogoutStatus,
23 | resetUserStatus
24 | } from '../actions'
25 | import ErrorContent from '../components/ErrorContent'
26 | import Profile from '../components/Profile'
27 |
28 |
29 | class ProfileContainer extends Component {
30 |
31 | constructor(props) {
32 | super(props)
33 | this.props.resetLogoutStatus()
34 | this.props.resetUserStatus()
35 | }
36 |
37 | componentDidMount() {
38 | const token = localStorage.getItem('token')
39 | if (!token) hashHistory.push('/login')
40 | else this.props.requestCurrentUser(token)
41 | }
42 |
43 | componentWillReceiveProps(nextProps) {
44 | const logout_status = nextProps.logout.status
45 | if (logout_status === 'success') hashHistory.push('/login')
46 | }
47 |
48 | handleLogout() {
49 | const token = localStorage.getItem('token')
50 | this.props.requestLogout(token)
51 | }
52 |
53 |
54 | handleUpdateUser(displayName, uid, username) {
55 | const token = localStorage.getItem('token')
56 | const {
57 | user
58 | } = this.props
59 | user.displayName = displayName
60 | user.uid = uid
61 | user.username = username
62 | this.props.requestUpdateUser(token, user)
63 | }
64 |
65 | render() {
66 | const {
67 | user
68 | } = this.props
69 | const self = this
70 | const renderStatus = {
71 | loading: function() {
72 | return (
73 |
74 |
)
75 | },
76 | error: function() {
77 | return (
78 |
79 |
80 | Redirect to login
81 |
)
82 | },
83 | success: function() {
84 | return (
85 |
86 | )
87 | }
88 | }
89 | if (renderStatus.hasOwnProperty(user.status)) return renderStatus[user.status]()
90 | return ()
91 | }
92 | }
93 |
94 | const mapStateToProps = (state) => {
95 | return {
96 | user: state.user,
97 | logout: state.logout
98 | }
99 | }
100 |
101 | const mapDispatchToProps = (dispatch) => {
102 | return bindActionCreators({
103 | requestCurrentUser,
104 | requestLogout,
105 | requestUpdateUser,
106 | resetLogoutStatus,
107 | resetUserStatus
108 | }, dispatch)
109 | }
110 |
111 | ProfileContainer.propTypes = {
112 | requestCurrentUser: PropTypes.func,
113 | requestLogout: PropTypes.func,
114 | requestUpdateUser: PropTypes.func,
115 | resetLogoutStatus: PropTypes.func,
116 | resetUserStatus: PropTypes.func,
117 | user: PropTypes.object
118 | }
119 |
120 | export default connect(mapStateToProps, mapDispatchToProps)(ProfileContainer)
--------------------------------------------------------------------------------
/client/containers/RegisterContainer.js:
--------------------------------------------------------------------------------
1 | import React, {
2 | Component,
3 | PropTypes
4 | } from 'react'
5 | import {
6 | connect
7 | } from 'react-redux'
8 | import {
9 | bindActionCreators
10 | } from 'redux'
11 | import {
12 | hashHistory
13 | } from 'react-router'
14 | import CircularProgress from 'material-ui/CircularProgress';
15 | import {
16 | requestSignupUser,
17 | resetUserStatus
18 | } from '../actions'
19 | import ErrorContent from '../components/ErrorContent'
20 | import {
21 | Register,
22 | RegisterSuccess
23 | } from '../components/Register'
24 |
25 |
26 | class RegisterContainer extends Component {
27 |
28 | constructor(props) {
29 | super(props)
30 | this.props.resetUserStatus()
31 | }
32 |
33 | handleSignupUser(displayName, password, username) {
34 | this.props.requestSignupUser(displayName, password, username)
35 | }
36 |
37 | render() {
38 | const {
39 | user
40 | } = this.props
41 | const renderStatus = {
42 | loading: function() {
43 | return (
44 |
45 |
)
46 | },
47 | error: function() {
48 | return
49 | },
50 | success: function() {
51 | return ()
52 | }
53 | }
54 | if (renderStatus.hasOwnProperty(user.status)) return renderStatus[user.status]()
55 | return ()
56 | }
57 | }
58 |
59 | const mapStateToProps = (state) => {
60 | return {
61 | user: state.user
62 | }
63 | }
64 |
65 | const mapDispatchToProps = (dispatch) => {
66 | return bindActionCreators({
67 | requestSignupUser,
68 | resetUserStatus
69 | }, dispatch)
70 | }
71 |
72 | RegisterContainer.propTypes = {
73 | requestSignupUser: PropTypes.func,
74 | resetUserStatus: PropTypes.func,
75 | user: PropTypes.object
76 | }
77 |
78 | export default connect(mapStateToProps, mapDispatchToProps)(RegisterContainer)
--------------------------------------------------------------------------------
/client/containers/index.js:
--------------------------------------------------------------------------------
1 | import AuthContainer from './AuthContainer'
2 | import LoginContainer from './LoginContainer'
3 | import ProfileContainer from './ProfileContainer'
4 | import RegisterContainer from './RegisterContainer'
5 |
6 | export {
7 | AuthContainer,
8 | LoginContainer,
9 | ProfileContainer,
10 | RegisterContainer
11 | }
--------------------------------------------------------------------------------
/client/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | User Authentication
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/client/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "user-authentication",
3 | "version": "0.1.0",
4 | "devDependencies": {
5 | "babel-core": "^6.17.0",
6 | "babel-loader": "^6.2.7",
7 | "babel-polyfill": "^6.16.0",
8 | "babel-preset-es2015": "^6.18.0",
9 | "babel-preset-react": "^6.16.0",
10 | "css-loader": "^0.25.0",
11 | "json-loader": "^0.5.4",
12 | "sass-loader": "^4.0.2",
13 | "style-loader": "^0.13.1",
14 | "webpack": "^1.13.3",
15 | "webpack-dev-server": "^1.16.2"
16 | },
17 | "dependencies": {
18 | "bootstrap-sass": "^3.3.7",
19 | "file-loader": "^0.9.0",
20 | "font-awesome ": "^4.7.0",
21 | "formsy-material-ui": "^0.5.3",
22 | "formsy-react": "^0.18.1",
23 | "history": "^4.3.0",
24 | "html-webpack-plugin": "^2.24.1",
25 | "material-ui": "^0.16.4",
26 | "moment": "^2.15.2",
27 | "node-sass": "^3.11.2",
28 | "react": "^15.3.2",
29 | "react-dom": "^15.3.2",
30 | "react-redux": "^4.4.5",
31 | "react-router": "^3.0.0",
32 | "react-css-modules": "^3.7.10",
33 | "react-tap-event-plugin": "^2.0.1",
34 | "redux": "3.6.0",
35 | "redux-saga": "^0.12.1",
36 | "url-loader": "^0.5.7"
37 | },
38 | "scripts": {
39 | "build": "webpack",
40 | "dev": "webpack-dev-server --inline --devtool eval --progress --colors --hot --content-base ../public"
41 | }
42 | }
--------------------------------------------------------------------------------
/client/readme.md:
--------------------------------------------------------------------------------
1 | # Reactjs demo with user authentication #
2 |
3 | ## Requirement ##
4 | * [Nodejs](https://nodejs.org/en/)
5 | * [NPM](https://www.npmjs.com/) - A package manager for build environment
6 |
7 | ## System Environment ##
8 | * NODE_ENV (development、production)
9 |
10 | ## Quick Start ##
11 | * npm install
12 | * npm run dev - development
13 | * npm run build - build to ../public/bundle.js
14 |
15 | ## Action Flow ##
16 |
17 | ### Vaild Token ###
18 | >1. Auth -> Profile
19 |
20 | ### Invaild Token ###
21 | >1. Auth -> Login -> Success -> Profile
22 | >2. Auth -> Login -> Faild -> Login
--------------------------------------------------------------------------------
/client/reducers/authentication.js:
--------------------------------------------------------------------------------
1 | import {
2 | REQUEST_AUTHENTICATION,
3 | REQUEST_AUTHENTICATION_FAILD,
4 | REQUEST_AUTHENTICATION_SUCCESS
5 | } from '../actions'
6 |
7 | export default function authentication(state = {
8 | error: null,
9 | status: 'init'
10 | }, action) {
11 | switch (action.type) {
12 | case REQUEST_AUTHENTICATION:
13 | return Object.assign({}, state, {
14 | status: 'loading',
15 | error: null
16 | })
17 | break
18 | case REQUEST_AUTHENTICATION_FAILD:
19 | return Object.assign({}, state, {
20 | status: 'error',
21 | error: action.error
22 | })
23 | break
24 | case REQUEST_AUTHENTICATION_SUCCESS:
25 | return Object.assign({}, state, {
26 | status: 'success',
27 | user: action.user
28 | })
29 |
30 | break
31 | default:
32 | return state
33 | }
34 | }
--------------------------------------------------------------------------------
/client/reducers/index.js:
--------------------------------------------------------------------------------
1 | import {
2 | combineReducers
3 | } from 'redux'
4 | import authentication from './authentication'
5 | import login from './login'
6 | import logout from './logout'
7 | import user from './user'
8 |
9 | export default combineReducers({
10 | authentication,
11 | login,
12 | logout,
13 | user
14 | })
--------------------------------------------------------------------------------
/client/reducers/login.js:
--------------------------------------------------------------------------------
1 | import {
2 | REQUEST_LOGIN,
3 | REQUEST_LOGIN_FAILD,
4 | REQUEST_LOGIN_SUCCESS,
5 | RESET_LOGIN_STATUS
6 | } from '../actions'
7 |
8 | const initState = {
9 | error: null,
10 | status: 'init',
11 | token: null
12 | }
13 |
14 | export default function login(state = initState, action) {
15 | switch (action.type) {
16 | case REQUEST_LOGIN:
17 | return Object.assign({}, state, {
18 | status: 'loading',
19 | error: null
20 | })
21 | break
22 | case REQUEST_LOGIN_FAILD:
23 | return Object.assign({}, state, {
24 | status: 'error',
25 | error: action.error
26 | })
27 | break
28 | case REQUEST_LOGIN_SUCCESS:
29 | return Object.assign({}, state, {
30 | status: 'success',
31 | token: action.token
32 | })
33 | break
34 | case RESET_LOGIN_STATUS:
35 | return initState
36 | break
37 | default:
38 | return state
39 | }
40 | }
--------------------------------------------------------------------------------
/client/reducers/logout.js:
--------------------------------------------------------------------------------
1 | import {
2 | REQUEST_LOGOUT,
3 | REQUEST_LOGOUT_FAILD,
4 | REQUEST_LOGOUT_SUCCESS,
5 | RESET_LOGOUT_STATUS
6 | } from '../actions'
7 |
8 | const initState = {
9 | error: null,
10 | status: 'init'
11 | }
12 |
13 | export default function logout(state = initState, action) {
14 | switch (action.type) {
15 | case REQUEST_LOGOUT:
16 | return Object.assign({}, state, {
17 | status: 'loading',
18 | error: null
19 | })
20 | break
21 | case REQUEST_LOGOUT_FAILD:
22 | return Object.assign({}, state, {
23 | status: 'error',
24 | error: action.error
25 | })
26 | break
27 | case REQUEST_LOGOUT_SUCCESS:
28 | return Object.assign({}, state, {
29 | status: 'success'
30 | })
31 | break
32 | case RESET_LOGOUT_STATUS:
33 | return initState
34 | break
35 | default:
36 | return state
37 | }
38 | }
--------------------------------------------------------------------------------
/client/reducers/user.js:
--------------------------------------------------------------------------------
1 | import {
2 | REQUEST_CURRENTUSER,
3 | REQUEST_CURRENTUSER_SUCCESS,
4 | REQUEST_FAILD,
5 | REQUEST_SIGNUP_USER,
6 | REQUEST_SIGNUP_USER_SUCCESS,
7 | REQUEST_UPDATEUSER,
8 | REQUEST_UPDATEUSER_SUCCESS,
9 | RESET_USER_STATUS
10 | } from '../actions'
11 |
12 | const initState = {
13 | error: null,
14 | status: 'init'
15 | }
16 |
17 | export default function user(state = initState, action) {
18 | switch (action.type) {
19 | case REQUEST_CURRENTUSER:
20 | return Object.assign({}, state, {
21 | status: 'loading',
22 | error: null
23 | })
24 | break
25 | case REQUEST_CURRENTUSER_SUCCESS:
26 | return Object.assign({}, state, {
27 | status: 'success',
28 | displayName: action.user.displayName,
29 | role: action.user.role,
30 | uid: action.user.uid,
31 | username: action.user.username
32 | })
33 | break
34 | case REQUEST_FAILD:
35 | return Object.assign({}, state, {
36 | status: 'error',
37 | error: action.error
38 | })
39 | break
40 | case REQUEST_SIGNUP_USER:
41 | return Object.assign({}, state, {
42 | status: 'loading',
43 | error: null
44 | })
45 | break
46 | case REQUEST_SIGNUP_USER_SUCCESS:
47 | return Object.assign({}, state, {
48 | status: 'success',
49 | displayName: action.displayName,
50 | username: action.username
51 | })
52 | break
53 | case REQUEST_UPDATEUSER:
54 | return Object.assign({}, state, {
55 | status: 'loading',
56 | error: null
57 | })
58 | break
59 | case REQUEST_UPDATEUSER_SUCCESS:
60 | return Object.assign({}, state, {
61 | status: 'success',
62 | displayName: action.user.displayName,
63 | role: action.user.role,
64 | uid: action.user.uid,
65 | username: action.user.username
66 | })
67 | case RESET_USER_STATUS:
68 | return initState
69 | break
70 | default:
71 | return state
72 | }
73 | }
--------------------------------------------------------------------------------
/client/routes.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import {
3 | Redirect,
4 | Route,
5 | IndexRoute
6 | } from 'react-router'
7 |
8 | //import components
9 | import App from './components/App'
10 |
11 | import {
12 | AuthContainer,
13 | LoginContainer,
14 | ProfileContainer,
15 | RegisterContainer
16 | } from './containers'
17 |
18 | const Routes = (
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | )
27 |
28 | export default Routes
--------------------------------------------------------------------------------
/client/sagas/authentication.js:
--------------------------------------------------------------------------------
1 | import {
2 | takeEvery
3 | } from 'redux-saga'
4 |
5 | import {
6 | put,
7 | call
8 | } from 'redux-saga/effects'
9 |
10 | import {
11 | REQUEST_AUTHENTICATION,
12 | REQUEST_AUTHENTICATION_FAILD,
13 | REQUEST_AUTHENTICATION_SUCCESS
14 | } from '../actions'
15 |
16 | import {
17 | AuthService
18 | } from '../api'
19 |
20 | export function* watchAuthentication() {
21 | yield call(takeEvery, REQUEST_AUTHENTICATION, authenticationFlow)
22 | }
23 |
24 | export function* authenticationFlow(action) {
25 | try {
26 | const authService = new AuthService()
27 | yield call(authService.requestAuth, action.token)
28 | yield put({
29 | type: REQUEST_AUTHENTICATION_SUCCESS
30 | })
31 | } catch (error) {
32 | yield put({
33 | type: REQUEST_AUTHENTICATION_FAILD,
34 | error
35 | })
36 | }
37 | }
--------------------------------------------------------------------------------
/client/sagas/index.js:
--------------------------------------------------------------------------------
1 | import {
2 | watchLogin
3 | } from './login'
4 | import {
5 | watchLogout
6 | } from './logout'
7 | import {
8 | watchAuthentication
9 | } from './authentication'
10 | import {
11 | watchCurrentUser,
12 | watchSignupUser,
13 | watchUpdateUser
14 | } from './user'
15 | export default function* rootSaga() {
16 | yield [
17 | watchAuthentication(),
18 | watchCurrentUser(),
19 | watchLogin(),
20 | watchLogout(),
21 | watchSignupUser(),
22 | watchUpdateUser()
23 | ]
24 | }
--------------------------------------------------------------------------------
/client/sagas/login.js:
--------------------------------------------------------------------------------
1 | import {
2 | takeEvery
3 | } from 'redux-saga'
4 |
5 | import {
6 | put,
7 | call
8 | } from 'redux-saga/effects'
9 |
10 | import {
11 | REQUEST_LOGIN,
12 | REQUEST_LOGIN_FAILD,
13 | REQUEST_LOGIN_SUCCESS
14 | } from '../actions'
15 |
16 | import {
17 | AuthService
18 | } from '../api'
19 |
20 | export function* watchLogin() {
21 | yield call(takeEvery, REQUEST_LOGIN, loginFlow)
22 | }
23 |
24 | export function* loginFlow(action) {
25 | try {
26 | const authService = new AuthService()
27 | const userInfo = yield call(authService.requestLogin, action.username, action.password)
28 | const token = userInfo.token
29 | localStorage.setItem('token', token)
30 | yield put({
31 | type: REQUEST_LOGIN_SUCCESS,
32 | token
33 | })
34 | } catch (error) {
35 | yield put({
36 | type: REQUEST_LOGIN_FAILD,
37 | error
38 | })
39 | }
40 | }
--------------------------------------------------------------------------------
/client/sagas/logout.js:
--------------------------------------------------------------------------------
1 | import {
2 | takeEvery
3 | } from 'redux-saga'
4 |
5 | import {
6 | put,
7 | call
8 | } from 'redux-saga/effects'
9 |
10 | import {
11 | REQUEST_LOGOUT,
12 | REQUEST_LOGOUT_FAILD,
13 | REQUEST_LOGOUT_SUCCESS
14 | } from '../actions'
15 |
16 | import {
17 | AuthService
18 | } from '../api'
19 |
20 | export function* watchLogout() {
21 | yield call(takeEvery, REQUEST_LOGOUT, logoutFlow)
22 | }
23 |
24 | export function* logoutFlow(action) {
25 | try {
26 | const authService = new AuthService()
27 | yield call(authService.requestLogout, action.token)
28 | localStorage.removeItem('token')
29 | yield put({
30 | type: REQUEST_LOGOUT_SUCCESS
31 | })
32 | } catch (error) {
33 | yield put({
34 | type: REQUEST_LOGOUT_FAILD,
35 | error
36 | })
37 | }
38 | }
--------------------------------------------------------------------------------
/client/sagas/user.js:
--------------------------------------------------------------------------------
1 | import {
2 | takeEvery
3 | } from 'redux-saga'
4 |
5 | import {
6 | put,
7 | call
8 | } from 'redux-saga/effects'
9 |
10 | import {
11 | REQUEST_CURRENTUSER,
12 | REQUEST_CURRENTUSER_SUCCESS,
13 | REQUEST_FAILD,
14 | REQUEST_SIGNUP_USER,
15 | REQUEST_SIGNUP_USER_SUCCESS,
16 | REQUEST_UPDATEUSER,
17 | REQUEST_UPDATEUSER_SUCCESS
18 | } from '../actions'
19 |
20 | import {
21 | AuthService,
22 | UserService
23 | } from '../api'
24 | const userService = new UserService()
25 | export function* watchCurrentUser() {
26 | yield call(takeEvery, REQUEST_CURRENTUSER, currentUserFlow)
27 | }
28 |
29 | export function* watchSignupUser() {
30 | yield call(takeEvery, REQUEST_SIGNUP_USER, signupUserFlow)
31 | }
32 |
33 | export function* watchUpdateUser() {
34 | yield call(takeEvery, REQUEST_UPDATEUSER, updateUserFlow)
35 | }
36 |
37 | export function* currentUserFlow(action) {
38 | try {
39 | const user = yield call(userService.requestCurrentUser, action.token)
40 | yield put({
41 | type: REQUEST_CURRENTUSER_SUCCESS,
42 | user
43 | })
44 | } catch (error) {
45 | yield put({
46 | type: REQUEST_FAILD,
47 | error
48 | })
49 | }
50 | }
51 |
52 | export function* signupUserFlow(action) {
53 | try {
54 | yield call(userService.requestSignupUser, action.displayName, action.password, action.username)
55 | yield put({
56 | type: REQUEST_SIGNUP_USER_SUCCESS,
57 | displayName: action.displayName,
58 | username: action.username
59 | })
60 | } catch (error) {
61 | yield put({
62 | type: REQUEST_FAILD,
63 | error
64 | })
65 | }
66 | }
67 |
68 | export function* updateUserFlow(action) {
69 | try {
70 | const result = yield call(userService.requestUpdateUser, action.token, action.user)
71 | const user = Object.assign(action.user, result)
72 | yield put({
73 | type: REQUEST_UPDATEUSER_SUCCESS,
74 | user
75 | })
76 | } catch (error) {
77 | yield put({
78 | type: REQUEST_FAILD,
79 | error
80 | })
81 | }
82 | }
--------------------------------------------------------------------------------
/client/store/configureStore.js:
--------------------------------------------------------------------------------
1 | import {
2 | createStore,
3 | applyMiddleware
4 | } from 'redux'
5 | import createSagaMiddleware from 'redux-saga'
6 | import rootReducers from '../reducers/index'
7 | import rootSaga from '../sagas'
8 |
9 | const sagaMiddleware = createSagaMiddleware()
10 | const store = createStore(rootReducers, applyMiddleware(sagaMiddleware))
11 |
12 | sagaMiddleware.run(rootSaga)
13 | export default store
--------------------------------------------------------------------------------
/client/stylesheets/_mixins.scss:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/weihanchen/user-authentication-nodejs/3c8e8e84c06e6258d8cafcb1a81a53d62d1ad758/client/stylesheets/_mixins.scss
--------------------------------------------------------------------------------
/client/stylesheets/_variables.scss:
--------------------------------------------------------------------------------
1 | $color-navy: #1d9e74;
2 | $color-primary: #26A69A;
3 | $color-white: #ffffff;
4 | $color-yellow: yellow;
--------------------------------------------------------------------------------
/client/stylesheets/components/footer.scss:
--------------------------------------------------------------------------------
1 | .footer {
2 | color: $color-white;
3 | background: $color-primary;
4 | &-text {
5 | margin-top: 1em;
6 | margin-bottom: 1em;
7 | }
8 | a {
9 | color: yellow ;
10 | }
11 | }
--------------------------------------------------------------------------------
/client/stylesheets/components/login.scss:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/weihanchen/user-authentication-nodejs/3c8e8e84c06e6258d8cafcb1a81a53d62d1ad758/client/stylesheets/components/login.scss
--------------------------------------------------------------------------------
/client/stylesheets/style.scss:
--------------------------------------------------------------------------------
1 | @import "./variables";
2 | @import "./mixins";
3 | /* components */
4 | @import "./components/login";
5 | @import "./components/footer";
6 |
7 | .content {
8 | &-block {
9 | margin: 10em 2em 10em 2em;
10 | }
11 | &-container{
12 | padding: 1em;
13 | }
14 | }
15 |
16 | .fill {
17 | display: flex;
18 | flex: 1 1 auto;
19 | }
20 |
21 | .element-horizontal-gap {
22 | margin: 0 0.2em 0 0.2em
23 | }
24 |
--------------------------------------------------------------------------------
/client/webpack.config.js:
--------------------------------------------------------------------------------
1 | let path = require('path');
2 | let webpack = require('webpack');
3 | let HtmlWebpackPlugin = require('html-webpack-plugin');
4 |
5 | const ROOT_PATH = path.resolve(__dirname);
6 | const APP_PATH = path.resolve(__dirname, './client.js');
7 | const BUILD_PATH = path.resolve(__dirname, '../public');
8 | const INDEX_TEMPLATE_PATH = path.resolve(__dirname, './index.html');
9 |
10 | module.exports = {
11 | entry: {
12 | main: ['babel-polyfill', APP_PATH]
13 | },
14 | output: {
15 | path: BUILD_PATH,
16 | filename: 'bundle.min.js'
17 | },
18 | module: {
19 | loaders: [{
20 | test: /\.js[x]?$/,
21 | exclude: /(node_modules|bower_components)/,
22 | loaders: ['babel-loader?presets[]=es2015,presets[]=react']
23 | }, {
24 | test: /\.json$/,
25 | loader: 'json'
26 | }, {
27 | test: /\.css$/,
28 | loader: 'style-loader!css-loader'
29 | }, {
30 | test: /\.scss$/,
31 | loaders: ["style", "css", "sass"]
32 | }, {
33 | test: /\.(png|jpg)$/,
34 | loader: 'file-loader'
35 | }, {
36 | test: /\.woff(\?v=\d+\.\d+\.\d+)?$/,
37 | loader: "url-loader?limit=10000&mimetype=application/font-woff"
38 | }, {
39 | test: /\.woff2(\?v=\d+\.\d+\.\d+)?$/,
40 | loader: "url-loader?limit=10000&mimetype=application/font-woff"
41 | }, {
42 | test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/,
43 | loader: "url-loader?limit=10000&mimetype=application/octet-stream"
44 | }, {
45 | test: /\.eot(\?v=\d+\.\d+\.\d+)?$/,
46 | loader: "file"
47 | }, {
48 | test: /\.svg(\?v=\d+\.\d+\.\d+)?$/,
49 | loader: "url-loader?limit=10000&mimetype=image/svg+xml"
50 | }]
51 | },
52 | plugins: [
53 | new webpack.optimize.UglifyJsPlugin({
54 | minimize: true
55 | }),
56 | new HtmlWebpackPlugin({
57 | title: 'User Authentication',
58 | template: INDEX_TEMPLATE_PATH,
59 | minify: {
60 | collapseWhitespace: true,
61 | removeComments: true,
62 | removeRedundantAttributes: true,
63 | removeScriptTypeAttributes: true,
64 | removeStyleLinkTypeAttributes: true
65 | }
66 | }),
67 | new webpack.DefinePlugin({
68 | "process.env": {
69 | NODE_ENV: JSON.stringify(process.env.NODE_ENV || 'development')
70 | }
71 | })
72 | ]
73 | }
74 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "user_authentication",
3 | "description": "deployment",
4 | "scripts": {
5 | "build:client": "cd client && npm run build",
6 | "dev:client": "cd client && npm run dev",
7 | "dev:server": "cd server && npm run dev",
8 | "heroku-postbuild": "echo This runs afterwards.",
9 | "start": "cd server && npm install && npm run product",
10 | "test:server": "cd server && npm run test",
11 | "test:server:coverage": "cd server && npm run test:coverage"
12 | },
13 | "engines": {
14 | "node": "7.1.0",
15 | "npm": "3.10.9"
16 | },
17 | "author": "weihanchen",
18 | "license": "MIT"
19 | }
20 |
--------------------------------------------------------------------------------
/public/448c34a56d699c29117adc64c43affeb.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/weihanchen/user-authentication-nodejs/3c8e8e84c06e6258d8cafcb1a81a53d62d1ad758/public/448c34a56d699c29117adc64c43affeb.woff2
--------------------------------------------------------------------------------
/public/674f50d287a8c48dc19ba404d20fe713.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/weihanchen/user-authentication-nodejs/3c8e8e84c06e6258d8cafcb1a81a53d62d1ad758/public/674f50d287a8c48dc19ba404d20fe713.eot
--------------------------------------------------------------------------------
/public/89889688147bd7575d6327160d64e760.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/public/af7ae505a9eed503f8b8e6982036873e.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/weihanchen/user-authentication-nodejs/3c8e8e84c06e6258d8cafcb1a81a53d62d1ad758/public/af7ae505a9eed503f8b8e6982036873e.woff2
--------------------------------------------------------------------------------
/public/b06871f281fee6b241d60582ae9369b9.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/weihanchen/user-authentication-nodejs/3c8e8e84c06e6258d8cafcb1a81a53d62d1ad758/public/b06871f281fee6b241d60582ae9369b9.ttf
--------------------------------------------------------------------------------
/public/e18bbf611f2a2e43afc071aa2f4e1512.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/weihanchen/user-authentication-nodejs/3c8e8e84c06e6258d8cafcb1a81a53d62d1ad758/public/e18bbf611f2a2e43afc071aa2f4e1512.ttf
--------------------------------------------------------------------------------
/public/f4769f9bdb7466be65088239c12046d1.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/weihanchen/user-authentication-nodejs/3c8e8e84c06e6258d8cafcb1a81a53d62d1ad758/public/f4769f9bdb7466be65088239c12046d1.eot
--------------------------------------------------------------------------------
/public/fa2772327f55d8198301fdb8bcfc8158.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/weihanchen/user-authentication-nodejs/3c8e8e84c06e6258d8cafcb1a81a53d62d1ad758/public/fa2772327f55d8198301fdb8bcfc8158.woff
--------------------------------------------------------------------------------
/public/fee66e712a8a08eef5805a46892932ad.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/weihanchen/user-authentication-nodejs/3c8e8e84c06e6258d8cafcb1a81a53d62d1ad758/public/fee66e712a8a08eef5805a46892932ad.woff
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 | User Authentication
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | # Nodejs user authentication sample base on json web token #
2 |
3 | [](https://travis-ci.org/weihanchen/user-authentication-nodejs)
4 | [](https://coveralls.io/github/weihanchen/user-authentication-nodejs?branch=master)
5 | [](https://david-dm.org/weihanchen/user-authentication-nodejs)
6 | [](https://david-dm.org/weihanchen/user-authentication-nodejs?type=dev)
7 |
8 | A nodejs server api for user authentication and use react to design frontend
9 |
10 | ## [Demo Site](https://user-authentication-nodejs.herokuapp.com/) ##
11 |
12 | ## Heroku Deployment ##
13 |
14 | You can quickly setup a sample heroku application by clicking the button below.
15 |
16 | [](https://heroku.com/deploy)
17 |
18 | ## Requirement ##
19 | * [MongoDB](https://www.mongodb.com/) - Our Database v3.2
20 | * [Expressjs](http://expressjs.com/zh-tw/) - API Server
21 | * [Nodejs](https://nodejs.org/en/) - Backend Framework v7.1.0
22 | * [NPM](https://www.npmjs.com/) - Package Management v3.10.9
23 |
24 | ## System Environment Variables ##
25 | * `PORT`
26 | * `SECRET_KEY`
27 | * `MONGO_CONNECTION`
28 |
29 |
30 | ## Install dependence packages ##
31 | ```
32 | $ cd server
33 | $ npm install
34 | $ cd ../client
35 | $ npm install
36 | ```
37 |
38 | ## [client react documentation](client/readme.md) ##
39 |
40 | ## Config ##
41 | * `server/config/database.js` database and jwt secret configuration, default using system variables
42 | >1. secret - jwt auth secret
43 | >2. database - database connection
44 |
45 | ## Packages ##
46 | >1. [Mongoose](http://mongoosejs.com/) - mongodb object modeling
47 | >2. [Simple JWT](https://www.npmjs.com/package/jwt-simple) - token use
48 | >3. [Morgan](https://github.com/expressjs/morgan) - HTTP request logger middleware for node.js
49 | >4. [moment](http://momentjs.com/docs/) - date parse
50 | >5. [bcrypt-nodejs](https://www.npmjs.com/package/bcrypt-nodejs) - ecrypt password
51 |
52 | ## Step ##
53 | ### General config
54 | >1. edit server/config/database.js or system variable for `MONGO_CONNECTION`、`SECRET_KEY` - database connection and jwt secret
55 | >2. edit server/config/initial.js - super admin account and role's permissions
56 | >3. export `API_ENDPOINT` with system variable, allow client connection with server endpoint.
57 | ### Start with development
58 | >1. server development: `npm run dev:server`
59 | >2. client development: `npm run dev:client`, default port `8080`
60 | ### Production build and run
61 | >1. `npm run build:client`
62 | >2. `npm start`
63 | ### initial users and rols step
64 | >1. post `/api/initialize` to create roles and super admin account
65 | >2. post `api/users` - create new account
66 | >3. post `api/users/login` - login and get jwt token then frontend can store this token to use other api
67 | >4. use request header: `{Authorization: (jwt token)}` when use other api
68 |
69 | ## Authentication ##
70 | Check token valid
71 | * `/api/users/logout`
72 |
73 | Check token valid and expired
74 | * `/api/users/:id`
75 | * `/api/users/me`
76 |
77 | ## Permissions(roles) ##
78 | * admin
79 | * `delete` - other users and roles
80 | * `get` - all users and roles
81 | * `post` - user and role
82 | * `put` - all users and other user's role
83 |
84 | * user
85 | * `delete` - self
86 | * `get` - self
87 | * `post` - signup
88 | * `put` - self but cannot update role
89 |
90 | ## Documentation ##
91 |
92 | * request header - Authorization (json web token)
93 |
94 | * **api** - api root
95 |
96 | * **api/initialize**
97 |
98 | ` post - create roles and admin user`
99 |
100 | * **api/users**
101 |
102 | ` post - create new user `
103 |
104 |
105 | * **api/users/login**
106 |
107 | `post - login and get jwt token`
108 |
109 | * **api/users/me**
110 |
111 | `get - get current user info`
112 |
113 | * **api/users/:id**
114 |
115 | ` delete - delete user `
116 |
117 | ` get - get user info `
118 |
119 | ` put - update username、displayName only superadmin can update other user's role`
120 |
121 |
122 |
123 | ## API Test ##
124 | * npm install --dev
125 | * npm run test:server
126 |
127 |
128 | ## To Do ##
129 | - [ ] admin dashboard
130 | - [ ] edit role name
131 | - [ ] edit password
132 | - [ ] add more test case for permissions
133 | - [ ] add business logic extension framework document
134 | - [ ] add swagger ui
135 |
--------------------------------------------------------------------------------
/server/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | "env": {
3 | "commonjs": true,
4 | "es6": true,
5 | "browser": true
6 | },
7 | "extends": "eslint:recommended",
8 | "globals": {
9 | "before": true,
10 | "describe": true,
11 | "it": true,
12 | "process": true,
13 | "__base": true,
14 | "__dirname": true,
15 | "__testBase": true
16 |
17 | },
18 | "parserOptions": {
19 | "sourceType": "module"
20 | },
21 | "rules": {
22 | "linebreak-style": [
23 | "error",
24 | "windows"
25 | ],
26 | "semi": [
27 | "error",
28 | "always"
29 | ],
30 | "no-console": "off",
31 | "no-var": "warn",
32 | "prefer-const": "warn",
33 | "arrow-body-style": ["error", "always",{ "requireReturnForObjectLiteral": true }]
34 | }
35 | };
36 |
--------------------------------------------------------------------------------
/server/config/database.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | 'secret': process.env.SECRET_KEY || /* istanbul ignore next: tired of writing tests */ 'user_auth_demo',
3 | 'database': process.env.MONGO_CONNECTION || /* istanbul ignore next: tired of writing tests */ 'mongodb://travis:test@127.0.0.1:27017/user_auth_demo'
4 | };
5 |
--------------------------------------------------------------------------------
/server/config/initial.js:
--------------------------------------------------------------------------------
1 | const adminRoleLevel = Number.MAX_SAFE_INTEGER;
2 | const userRoleLevel = 0;
3 | module.exports = {
4 | 'admin_account': process.env.ADMIN_ACCOUNT || /* istanbul ignore next: tired of writing tests */ 'superadmin',
5 | 'admin_password': process.env.ADMIN_PASSWORD || /* istanbul ignore next: tired of writing tests */ 'superadmin',
6 | 'admin_role_level': adminRoleLevel,
7 | 'roles': [
8 | {
9 | 'role': 'admin',
10 | 'level': adminRoleLevel
11 | },
12 | {
13 | 'role': 'user',
14 | 'level': userRoleLevel
15 | }
16 | ],
17 | 'user_role_level': userRoleLevel
18 | };
19 |
--------------------------------------------------------------------------------
/server/dev.js:
--------------------------------------------------------------------------------
1 | const nodemon = require('nodemon');
2 |
3 | nodemon({
4 | script:'run.js',
5 | watch:['*.js','./routes/*.js','./models/*.js','./middleware/*.js','./config/*.js','./services/error/*.js','./services/permissions/*.js']
6 | });
7 |
8 |
--------------------------------------------------------------------------------
/server/middleware/jwtauth.js:
--------------------------------------------------------------------------------
1 | const passport = require("passport");
2 | const passportJWT = require("passport-jwt");
3 | const config = require(__base + 'config/database');
4 | const errorBuilder = require(__base + 'services/error/builder');
5 | const ExtractJwt = passportJWT.ExtractJwt; //extract jwt token
6 | const Strategy = passportJWT.Strategy; //策略選擇為jwt
7 | const params = {
8 | secretOrKey: config.secret,
9 | jwtFromRequest: ExtractJwt.fromAuthHeader() //creates a new extractor that looks for the JWT in the authorization header with the scheme 'JWT',e.g JWT + 'token'
10 | };
11 |
12 | module.exports = () => {
13 | const strategy = new Strategy(params, (payload, done) => {
14 | //驗證token是否失效
15 | if (payload.exp <= Date.now()) {
16 | return done(errorBuilder.unauthorized('Access token has expired'), false);
17 | }
18 | const extracted = {
19 | uid: payload.iss,
20 | expireAt: payload.exp
21 | };
22 | done(null, extracted);
23 | });
24 | passport.use(strategy);
25 | return {
26 | initialize: () => passport.initialize(),
27 | authenticate: () => passport.authenticate("jwt", {
28 | session: false
29 | })
30 | };
31 | };
32 |
--------------------------------------------------------------------------------
/server/middleware/token_manager.js:
--------------------------------------------------------------------------------
1 | const ExpireToken = require(__base + 'models/expireToken');
2 | const errorBuilder = require(__base + 'services/error/builder');
3 | exports.vertifyToken = (req, res, next) => {
4 | const token = getToken(req.headers);
5 | ExpireToken.findOne({
6 | token: token
7 | }).then((result) => {
8 | if (result) next(errorBuilder.unauthorized('Access token has expired'));
9 | else next();
10 | }).catch(error => {
11 | /* istanbul ignore next */
12 | next(error);
13 | });
14 | };
15 |
16 | const getToken = (headers) => {
17 | if (headers && headers.authorization) return headers.authorization;
18 | return null;
19 | };
20 |
--------------------------------------------------------------------------------
/server/models/expireToken.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose');
2 | const Schema = mongoose.Schema;
3 |
4 | const ExpireTokenSchema = new Schema({
5 | token: {
6 | type: String,
7 | unique: true,
8 | required: true
9 | },
10 | expireAt: {
11 | type: Date,
12 | required: true
13 | }
14 | });
15 |
16 | ExpireTokenSchema.index({
17 | expireAt: 1
18 | }, {
19 | expireAfterSeconds: 0
20 | });
21 |
22 | module.exports = mongoose.model('ExpireToken', ExpireTokenSchema);
23 |
--------------------------------------------------------------------------------
/server/models/role.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose');
2 | const Schema = mongoose.Schema;
3 |
4 | const RoleSchema = new Schema({
5 | role: {
6 | type: String,
7 | unique: true,
8 | required: true
9 | },
10 | level: {
11 | type: Number,
12 | unique: true,
13 | required: true
14 | }
15 | });
16 | module.exports = mongoose.model('Role', RoleSchema);
17 |
--------------------------------------------------------------------------------
/server/models/user.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | const mongoose = require('mongoose');
3 | const Schema = mongoose.Schema;
4 | const bcrypt = require('bcrypt-nodejs');
5 |
6 | // set up a mongoose model
7 | const UserSchema = new Schema({
8 | username: {
9 | type: String,
10 | unique: true,
11 | required: true
12 | },
13 | displayName: {
14 | type: String,
15 | unique: true,
16 | required: true
17 | },
18 | password: {
19 | type: String,
20 | required: true
21 | },
22 | roleId: {
23 | type: String,
24 | required: true
25 | }
26 | });
27 |
28 | UserSchema.pre('save', function (next) {
29 | const user = this;
30 | //密碼變更或新密碼時
31 | if (user.isModified('password') || this.isNew) {
32 | bcrypt.genSalt(10, (err, salt) => {
33 | /* istanbul ignore if */
34 | if (err) {
35 | return next(err);
36 | }
37 | bcrypt.hash(user.password, salt, null, (err, hash) => {
38 | /* istanbul ignore if */
39 | if (err) {
40 | return next(err);
41 | }
42 | //使用hash取代明文密碼
43 | user.password = hash;
44 | next();
45 | });
46 | });
47 | } else {
48 | return next();
49 | }
50 | });
51 |
52 | /**
53 | * mongoose支持擴展方法,因此撰寫密碼驗證
54 | * @param {[string]} password [密碼]
55 | * @param {Function} callback [description]
56 | * @return {[type]} [description]
57 | */
58 | UserSchema.methods.comparePassword = function (candidatePassword, callback){
59 | bcrypt.compare(candidatePassword, this.password, (err, isMatch) => {
60 | /* istanbul ignore if */
61 | if (err) {
62 | return callback(err);
63 | }
64 | callback(null, isMatch);
65 | });
66 | };
67 |
68 | module.exports = mongoose.model('User', UserSchema);
69 |
--------------------------------------------------------------------------------
/server/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "user_authentication_api",
3 | "description": "api server",
4 | "node-main": "./run.js",
5 | "dependencies": {
6 | "bcrypt-nodejs": "~0.0.3",
7 | "body-parser": "~1.15.2",
8 | "cors": "~2.8.1",
9 | "express": "~4.14.0",
10 | "jwt-simple": "~0.5.0",
11 | "morgan": "~1.7.0",
12 | "moment": "^2.21.0",
13 | "mongoose": "~4.7.1",
14 | "passport": "~0.3.2",
15 | "passport-jwt": "~2.1.0"
16 | },
17 | "devDependencies": {
18 | "coveralls": "~2.11.15",
19 | "eslint": "~3.17.1",
20 | "istanbul": "^0.4.5",
21 | "mocha": "^3.0.2",
22 | "mocha-lcov-reporter": "~1.2.0",
23 | "nodemon": "^1.9.1",
24 | "supertest": "^2.0.0",
25 | "should": "^11.1.0"
26 | },
27 | "scripts": {
28 | "dev": "node dev.js",
29 | "eslint": "eslint",
30 | "test": "./node_modules/.bin/mocha test --timeout 20000",
31 | "test:coverage": "./node_modules/.bin/istanbul cover ./node_modules/mocha/bin/_mocha -- --timeout 10000 --recursive test --report lcovonly -R spec && cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js && rm -rf ./coverage",
32 | "product": "node run.js"
33 | },
34 | "author": "will.chen",
35 | "license": "MIT"
36 | }
37 |
--------------------------------------------------------------------------------
/server/routes/initial.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | const initial_config = require(__base + 'config/initial'), // get initial config file
3 | errorBuilder = require(__base + 'services/error/builder'),
4 | User = require(__base + 'models/user'),
5 | Role = require(__base + 'models/role');
6 |
7 | exports.initialize = (req, res, next) => {
8 | /* istanbul ignore next */
9 | const errorHandler = (error) => {
10 | next(error);
11 | };
12 | _setRoles()
13 | .then(() => _setAdminUser())
14 | .then(() => res.json({
15 | success: true,
16 | message: 'Successful initialize.'
17 | }))
18 | .catch(errorHandler);
19 | };
20 |
21 | //private methods
22 | const _dbErrorHandler = (error) => Promise.reject(errorBuilder.badRequest(error.errmsg));
23 | const _insertAdminUser = () => Role.findOne({ level: initial_config.admin_role_level })
24 | .then(role => {
25 | const adminUser = new User({
26 | displayName: initial_config.admin_account,
27 | username: initial_config.admin_account,
28 | password: initial_config.admin_password,
29 | roleId: role._id
30 | });
31 | return adminUser.save();
32 | });
33 | const _insertRoles = () => {
34 | const promises = [],
35 | roles = initial_config.roles;
36 | roles.forEach(role => {
37 | const newRole = new Role(role);
38 | promises.push(newRole.save());
39 | });
40 | return Promise.all(promises)
41 | .catch(error => Promise.reject(errorBuilder.badRequest(error.errmsg)));
42 | };
43 | const _setAdminUser = () => User.findOne({ username: initial_config.admin_account })
44 | .then(user => {
45 | if (user) {
46 | return Promise.resolve();
47 | } else {
48 | return _insertAdminUser();
49 | }
50 | })
51 | .catch(_dbErrorHandler);
52 |
53 | const _setRoles = () => Role.count()
54 | .then(count => {
55 | if (count > 0) {
56 | return Promise.resolve();
57 | } else {
58 | return _insertRoles();
59 | }
60 | });
61 |
--------------------------------------------------------------------------------
/server/routes/users.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | const config = require(__base + 'config/database'); // get db config file
3 | const errorBuilder = require(__base + 'services/error/builder');
4 | const initial_config = require(__base + 'config/initial'); // get initial config file
5 | const jwt = require('jwt-simple');
6 | const moment = require('moment');
7 | const PermissionValidator = require(__base + 'services/permissions/validator');
8 | const permissionValidator = new PermissionValidator();
9 | const User = require(__base + 'models/user'); // get the mongoose model
10 | const Role = require(__base + 'models/role');
11 | const ExpireToken = require(__base + 'models/expireToken');
12 |
13 | /**
14 | * super admin 不能刪除自己帳號,待優化id不存在
15 | */
16 | exports.delete = (req, res, next) => {
17 | const userid = req.params.id;
18 | const loginUserId = req.user.uid;
19 | const errorHandler = (error) => next(error);
20 | permissionValidator.currentUserOperation(loginUserId, userid).then((result) => {
21 | if (result.isAdmin && result.isSelf) errorHandler(errorBuilder.badRequest('admin cannot remove self\'s account'))
22 | else {
23 | result.user.remove().then(() => {
24 | res.json({
25 | success: true,
26 | uid: result.user._id
27 | });
28 | }).catch(errorHandler);
29 | }
30 | }).catch(errorHandler);
31 | };
32 |
33 | /**
34 | * 業務邏輯
35 | * 系統管理者不可更動自己的role但可以更動他人
36 | * 一般使用者不可更動role
37 | */
38 | exports.edit = (req, res, next) => {
39 | const userid = req.params.id;
40 | const errorHandler = (error) => {
41 | next(error);
42 | };
43 | const updateUser = (user) => {
44 | Object.assign(user, req.body);
45 | user.save().then(() => {
46 | res.json({
47 | uid: user.id,
48 | username: user.username,
49 | displayName: user.displayName
50 | });
51 | }).catch(errorHandler);
52 | };
53 | permissionValidator.currentUserOperation(req.user.uid, userid).then((result) => {
54 | if (req.body.hasOwnProperty('roleId')) {
55 | permissionValidator.editRoleInRoles(req.body.roleId).then(() => {
56 | if (result.isSelf) errorHandler(errorBuilder.badRequest('cannot update role'));
57 | else if (result.isAdmin) {
58 | updateUser(result.user);
59 | }
60 |
61 | }).catch(errorHandler);
62 | } else {
63 | updateUser(result.user);
64 | }
65 | }).catch(errorHandler);
66 | };
67 |
68 | exports.info = (req, res, next) => {
69 | const userid = req.params.id;
70 | const loginUserId = req.user.uid;
71 | permissionValidator.currentUserOperation(loginUserId, userid).then((result) => {
72 | res.json({
73 | uid: result.user._id,
74 | username: result.user.username,
75 | displayName: result.user.displayName,
76 | role: result.role.role
77 | });
78 | }).catch(error => {
79 | next(error);
80 | });
81 | };
82 |
83 | exports.login = (req, res, next) => {
84 | User.findOne({
85 | username: req.body.username
86 | }).exec().then(user => {
87 | if (!user) next(errorBuilder.badRequest('User not found.'));
88 | else {
89 | user.comparePassword(req.body.password, (error, isMatch) => { //使用user schema中定義的comparePassword檢查請求密碼是否正確
90 | if (isMatch && !error) {
91 | const expires = moment().add(1, 'days').valueOf();
92 | const token = jwt.encode({
93 | iss: user.id, //加密對象
94 | exp: expires
95 | }, config.secret);
96 | res.json({
97 | success: true,
98 | uid: user.id,
99 | token: 'JWT ' + token
100 | }); //JWT for passport-jwt extract fromAuthHeader
101 | } else {
102 | next(errorBuilder.badRequest('Wrong password.'));
103 | }
104 | });
105 | }
106 | }).catch(error => {
107 | next(errorBuilder.badRequest(error));
108 | });
109 | };
110 |
111 | exports.logout = (req, res, next) => {
112 | const token = req.headers.authorization;
113 | const user = req.user;
114 | const expireToken = new ExpireToken({
115 | token: token,
116 | expireAt: user.expireAt
117 | });
118 | const promise = expireToken.save();
119 | promise.then(() => {
120 | res.json({
121 | success: true,
122 | message: 'Successful Logout.'
123 | });
124 | }).catch(error => {
125 | next(errorBuilder.internalServerError(error));
126 | });
127 | };
128 |
129 | exports.me = (req, res, next) => { //get users/me之前經過中間件驗證用戶權限
130 | User.findOne({
131 | _id: req.user.uid
132 | }).then(user => Role.findById(user.roleId).then(role => {
133 | return {
134 | role: role,
135 | user: user
136 | };
137 | })).then(result => {
138 | const responseBody = {
139 | uid: result.user.id,
140 | username: result.user.username,
141 | displayName: result.user.displayName,
142 | role: result.role.role
143 | };
144 | res.send(responseBody);
145 | })
146 | .catch(error => {
147 | next(errorBuilder.internalServerError(error))
148 | });
149 | };
150 |
151 | exports.signup = (req, res, next) => {
152 | const requireProperties = ['displayName', 'password', 'username'];
153 | let propertyMissingMsg = '';
154 | const requireValid = requireProperties.every(property => {
155 | if (!req.body.hasOwnProperty(property)) {
156 | propertyMissingMsg = 'Please pass ' + property;
157 | return false;
158 | }
159 | return true;
160 | });
161 | if (!requireValid) {
162 | next(errorBuilder.badRequest(propertyMissingMsg));
163 | return;
164 | }
165 | const dbErrorHandler = (error) => next(errorBuilder.internalServerError(error));
166 |
167 | Role.findOne({
168 | level: initial_config.user_role_level
169 | }).then(role => { //get min level role to set signup user
170 | if (!role) {
171 | next(errorBuilder.badRequest('please post /api/initialize'));
172 | return;
173 | }
174 | const newUser = new User({
175 | username: req.body.username,
176 | displayName: req.body.displayName,
177 | password: req.body.password,
178 | roleId: role._id
179 | });
180 | User.findOne({
181 | username: newUser.username
182 | }).exec().then((user) => {
183 | if (user) next(errorBuilder.badRequest('username already exist.'));
184 | else {
185 | newUser.save().then(() => {
186 | res.json({
187 | success: true,
188 | message: 'Successful signup.'
189 | });
190 | }).catch(dbErrorHandler);
191 | }
192 | }).catch(dbErrorHandler);
193 | });
194 | };
195 |
--------------------------------------------------------------------------------
/server/run.js:
--------------------------------------------------------------------------------
1 | global.__base = __dirname + '/';
2 | const express = require('express');
3 | const bodyParser = require('body-parser');
4 | const app = express();
5 | const cors = require('cors');
6 |
7 | //the routing modules
8 | const users = require(__base + 'routes/users');
9 | const initial = require(__base + 'routes/initial');
10 | app.set('port', process.env.PORT || 3000);
11 | const config = require(__base + 'config/database'); // get db config file
12 | const morgan = require('morgan');
13 | const mongoose = require('mongoose');
14 | //middleware
15 | const jwtauth = require(__base + 'middleware/jwtauth')();
16 | const tokenManager = require(__base + 'middleware/token_manager');
17 |
18 | app.use(bodyParser.urlencoded({
19 | extended: false
20 | }));
21 |
22 | app.use(bodyParser.json({
23 | type: '*/*'
24 | }));
25 | //cors middleware,you can use this to ensure security
26 | app.use(cors());
27 |
28 | // log to console
29 | app.use(morgan('dev'));
30 | //public folder
31 | app.use(express.static('../public'));
32 | app.use(jwtauth.initialize());
33 | mongoose.Promise = global.Promise;
34 | mongoose.connect(config.database);
35 | const apiRoutes = express.Router(),
36 | errorHandler = (err, req, res, next) => {
37 | res.status(err.status || /* istanbul ignore next: tired of writing tests */ 500).json(err);
38 | next();
39 | };
40 | apiRoutes.route('/initialize')
41 | .post(initial.initialize);
42 |
43 | apiRoutes.route('/users')
44 | .post(users.signup);
45 |
46 | apiRoutes.route('/users/login')
47 | .post(users.login);
48 |
49 | apiRoutes.route('/users/logout')
50 | .post(jwtauth.authenticate(), users.logout);
51 |
52 | apiRoutes.route('/users/me')
53 | .get(tokenManager.vertifyToken, jwtauth.authenticate(), users.me);
54 |
55 | apiRoutes.route('/users/:id')
56 | .delete(tokenManager.vertifyToken, jwtauth.authenticate(), users.delete)
57 | .get(tokenManager.vertifyToken, jwtauth.authenticate(), users.info)
58 | .put(tokenManager.vertifyToken, jwtauth.authenticate(), users.edit);
59 |
60 |
61 | app.use('/api', apiRoutes);
62 | app.use(errorHandler);
63 |
64 |
65 | app.listen(app.get('port'), () => {
66 | console.log('Express server listening on port ' + app.get('port'));
67 | });
68 |
69 | module.exports = app;
70 |
--------------------------------------------------------------------------------
/server/services/error/builder.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | exports.badRequest = (message) => {
3 | return {
4 | message: message || /* istanbul ignore next: tired of writing tests */ 'bad request',
5 | status: 400
6 | };
7 | };
8 | exports.unauthorized = (message) => {
9 | return {
10 | message: message || /* istanbul ignore next: tired of writing tests */ 'unauthorized',
11 | status: 401
12 | };
13 | };
14 | exports.notFound = (message) => {
15 | return {
16 | message: message || /* istanbul ignore next: tired of writing tests */ 'not found',
17 | status: 404
18 | };
19 | };
20 | /* istanbul ignore next */
21 | exports.internalServerError = (message) => {
22 | return {
23 | message: message || 'Internal server error',
24 | status: 500
25 | };
26 | };
27 |
--------------------------------------------------------------------------------
/server/services/permissions/validator.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | const errorBuilder = require(__base + 'services/error/builder');
3 | const initial_config = require(__base + 'config/initial'); // get initial config file
4 | const User = require(__base + 'models/user.js'); // get the mongoose model
5 | const Role = require(__base + 'models/role');
6 |
7 | class PermissionValidator {
8 | constructor() {
9 | }
10 |
11 | currentUserOperation(loginUserId, userid) { //驗證使用者是否瀏覽、編輯自己的資訊,僅最高管理者不受此限
12 | return new Promise((resolve, reject) => {
13 | const dbErrorHandler = (error) => {
14 | reject(errorBuilder.internalServerError(error));
15 | };
16 |
17 | User.findOne({ _id: userid }).then(user => {
18 | if (!user) reject(errorBuilder.notFound('resource not found'));
19 | Role.findOne({ _id: user.roleId }).then(role => {
20 | const isAdmin = role.level === initial_config.admin_role_level;
21 | const isSelf = userid.toString() === loginUserId.toString();
22 | if (isAdmin || isSelf) {
23 | resolve({
24 | user: user,
25 | role: role,
26 | isAdmin: isAdmin,
27 | isSelf: isSelf
28 | });
29 | } else reject(errorBuilder.unauthorized('permission denied'));
30 | }).catch(dbErrorHandler);
31 | }).catch(dbErrorHandler);
32 | });
33 | }
34 |
35 | editRoleInRoles(roleId) {
36 | return Role.findOne({_id: roleId})
37 | .then(role => {
38 | if (!role) return Promise.reject(errorBuilder.badRequest('roleId not exist'));
39 | else return Promise.resolve(role);
40 | })
41 | .catch(error => Promise.reject(error));
42 | }
43 |
44 | }
45 |
46 | module.exports = PermissionValidator;
47 |
--------------------------------------------------------------------------------
/server/test/all.test.js:
--------------------------------------------------------------------------------
1 | global.__testBase = __dirname + '/';
2 | const app = require('../run');
3 | const userTest = require(__testBase + 'routes/users');
4 | describe('API Testing', () => {
5 | userTest(app,'test','test','test');
6 | });
7 |
--------------------------------------------------------------------------------
/server/test/routes/users.js:
--------------------------------------------------------------------------------
1 | const supertest = require('supertest');
2 | require('should');
3 | module.exports = (app, username, displayName, password) => {
4 | const request = supertest(app);
5 | const initializeUrl = '/api/initialize';
6 | const usersUrl = '/api/users';
7 | const loginUrl = '/api/users/login';
8 | const logoutUrl = '/api/users/logout';
9 | const meUrl = '/api/users/me';
10 | describe('initialize...', () => {
11 | it('should response status 400 when initialize not ready', (done) => {
12 | request.post(usersUrl)
13 | .set('Content-Type', 'application/json')
14 | .send({
15 | username: username,
16 | displayName: displayName,
17 | password: password,
18 | })
19 | .expect(400)
20 | .end((err, res) => {
21 | res.body.message.should.equal('please post /api/initialize');
22 | done(err);
23 | });
24 | });
25 |
26 |
27 | it('should response status 200 when initialize', (done) => {
28 | request.post(initializeUrl)
29 | .expect(200)
30 | .end((err) => {
31 | done(err);
32 | });
33 | });
34 |
35 | it('should response Successful initialize. when initialized', (done) => {
36 | request.post(initializeUrl)
37 | .expect(200)
38 | .end((err, res) => {
39 | res.body.message.should.equal('Successful initialize.');
40 | done(err);
41 | });
42 | });
43 | });
44 | describe('signup...', () => {
45 | it('should response status 400 when property missing', (done) => {
46 | request.post(usersUrl)
47 | .set('Content-Type', 'application/json')
48 | .send({
49 |
50 | })
51 | .expect(400)
52 | .end((err) => {
53 | done(err);
54 | });
55 | });
56 |
57 | it('should response status 200 when signup', (done) => {
58 | request.post(usersUrl)
59 | .set('Content-Type', 'application/json')
60 | .send({
61 | username: username,
62 | displayName: displayName,
63 | password: password,
64 | })
65 | .expect(200)
66 | .end((err) => {
67 | done(err);
68 | });
69 | });
70 |
71 | it('should response status 400 and response message contain username already exist. when sigup exist user', (done) => {
72 | request.post(usersUrl)
73 | .set('Content-Type', 'application/json')
74 | .send({
75 | username: username,
76 | displayName: displayName,
77 | password: password,
78 | })
79 | .expect(400)
80 | .end((err, res) => {
81 | res.body.message.should.equal('username already exist.');
82 | done(err);
83 | });
84 | });
85 | });
86 |
87 | describe('Login', () => {
88 | it('should response status 400 and message conatins User not found.', (done) => {
89 | request.post(loginUrl)
90 | .set('Content-Type', 'application/json')
91 | .send({
92 | username: 'User Not Found',
93 | password: 'User Not Found'
94 | })
95 | .expect(400)
96 | .end(function (err, res) {
97 | res.body.message.should.equal('User not found.');
98 | done(err);
99 | });
100 | });
101 |
102 | it('should response status 400 and message contains Wrong password.', (done) => {
103 | request.post(loginUrl)
104 | .set('Content-Type', 'application/json')
105 | .send({
106 | username: username,
107 | password: 'User Not Found'
108 | })
109 | .expect(400)
110 | .end((err, res) => {
111 | res.body.message.should.equal('Wrong password.');
112 | done(err);
113 | });
114 | });
115 | });
116 |
117 | describe('Logout', () => {
118 | let token;
119 | let userid;
120 | before((done) => {
121 | request.post(loginUrl)
122 | .set('Content-Type', 'application/json')
123 | .send({
124 | username: username,
125 | password: password
126 | })
127 | .expect(200)
128 | .end((err, res) => {
129 | token = res.body.token;
130 | userid = res.body.uid;
131 | done(err);
132 | });
133 | });
134 |
135 | it('should response status 401 when not send with token', (done) => {
136 | request.post(logoutUrl)
137 | .expect(401)
138 | .end((err) => {
139 | done(err);
140 | });
141 | });
142 |
143 | it('should response 200 and contains Successful Logout.', (done) => {
144 | request.post(logoutUrl)
145 | .set('Authorization', token)
146 | .expect(200)
147 | .end((err, res) => {
148 | res.body.message.should.equal('Successful Logout.');
149 | done(err);
150 | });
151 | });
152 |
153 | it('should response status 401 when delete user after logout', (done) => {
154 | const resourceUrl = usersUrl + '/' + userid;
155 | request.delete(resourceUrl)
156 | .set('Authorization', token)
157 | .expect(401)
158 | .end((err) => {
159 | done(err);
160 | });
161 | });
162 | });
163 |
164 | describe('User Role Operation', () => {
165 | let token;
166 | let userid;
167 | let resourceUrl;
168 | before((done) => { //login and save token
169 | request.post(loginUrl)
170 | .set('Content-Type', 'application/json')
171 | .send({
172 | username: username,
173 | password: password
174 | })
175 | .expect(200)
176 | .end((err, res) => {
177 | token = res.body.token;
178 | userid = res.body.uid;
179 | resourceUrl = usersUrl + '/' + userid;
180 | done(err);
181 | });
182 |
183 | });
184 |
185 | describe('get user info by users/me...', () => {
186 | it('should response status 401 when not send with token', (done) => {
187 | request.get(meUrl)
188 | .expect(401)
189 | .end((err) => {
190 | done(err);
191 | });
192 | });
193 | it('should response status 200 and contains username、displayName when header has jwt token when get /users/me', (done) => {
194 | request.get(meUrl)
195 | .set('Authorization', token)
196 | .expect(200)
197 | .end((err, res) => {
198 | res.body.should.have.property('uid');
199 | res.body.should.have.property('username');
200 | res.body.should.have.property('displayName');
201 | done(err);
202 | });
203 | });
204 | });
205 |
206 | describe('get user info by users/:id', () => {
207 | it('should response 200 and contains uid、username、displayName、role', (done) => {
208 | request.get(resourceUrl)
209 | .set('Authorization', token)
210 | .expect(200)
211 | .end((err, res) => {
212 | res.body.should.have.property('uid');
213 | res.body.should.have.property('username');
214 | res.body.should.have.property('displayName');
215 | res.body.should.have.property('role');
216 | done(err);
217 | });
218 | });
219 | });
220 |
221 | describe('edit user...', () => {
222 | // it('should response status 400 when update role', function(done) {
223 | // request.put(resourceUrl)
224 | // .set('Authorization', token)
225 | // .send({
226 | // roldId: $roldId
227 | // })
228 | // .expect(400)
229 | // .end(function(err, res) {
230 | // res.body.message.should.equal('cannot update role');
231 | // done(err);
232 | // })
233 | // })
234 |
235 | it('should user changed username and displayName status 200', (done) => {
236 | request.put(resourceUrl)
237 | .set('Authorization', token)
238 | .send({
239 | username: 'test1',
240 | displayName: 'test1'
241 | })
242 | .expect(200)
243 | .end((err, res) => {
244 | res.body.username.should.equal('test1');
245 | res.body.displayName.should.equal('test1');
246 | done(err);
247 | });
248 | });
249 | });
250 |
251 | describe('delete user...', () => {
252 | it('should response success when deleting user exist', (done) => {
253 | request.delete(resourceUrl)
254 | .set('Authorization', token)
255 | .expect(200)
256 | .end((err, res) => {
257 | res.body.should.have.property('success');
258 | res.body.success.should.equal(true);
259 | done(err);
260 | });
261 | });
262 | it('should response not found when user not exist', (done) => {
263 | request.delete(resourceUrl)
264 | .set('Authorization', token)
265 | .expect(404)
266 | .end((err, res) => {
267 | res.body.message.should.equal('resource not found');
268 | done(err);
269 | });
270 | });
271 | });
272 | });
273 | };
274 |
--------------------------------------------------------------------------------