├── .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 |
26 | 27 | 28 | 29 |
, 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 |
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 |
12 | 13 |
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 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | -------------------------------------------------------------------------------- /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 | [![Build Status](https://travis-ci.org/weihanchen/user-authentication-nodejs.svg?branch=master)](https://travis-ci.org/weihanchen/user-authentication-nodejs) 4 | [![Coverage Status](https://coveralls.io/repos/github/weihanchen/user-authentication-nodejs/badge.svg?branch=master&bust=1)](https://coveralls.io/github/weihanchen/user-authentication-nodejs?branch=master) 5 | [![Dependency Status](https://david-dm.org/weihanchen/user-authentication-nodejs.svg)](https://david-dm.org/weihanchen/user-authentication-nodejs) 6 | [![devDependencies Status](https://david-dm.org/weihanchen/user-authentication-nodejs/dev-status.svg)](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 | [![Deploy](https://www.herokucdn.com/deploy/button.svg)](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 | --------------------------------------------------------------------------------