├── client ├── .env ├── public │ ├── favicon.ico │ ├── manifest.json │ └── index.html ├── src │ ├── components │ │ ├── Home.js │ │ ├── App.js │ │ ├── CustomInput.js │ │ ├── HOCs │ │ │ └── authGuard.js │ │ ├── Header.js │ │ ├── Dashboard.js │ │ ├── SignIn.js │ │ └── SignUp.js │ ├── reducers │ │ ├── index.js │ │ ├── auth.js │ │ └── dashboard.js │ ├── actions │ │ ├── types.js │ │ └── index.js │ ├── index.js │ └── registerServiceWorker.js ├── .gitignore └── package.json ├── __tests__ ├── report │ ├── lcov-report │ │ ├── sort-arrow-sprite.png │ │ ├── prettify.css │ │ ├── server │ │ │ ├── configuration │ │ │ │ ├── index.js.html │ │ │ │ └── index.html │ │ │ ├── index.html │ │ │ ├── models │ │ │ │ ├── index.html │ │ │ │ └── user.js.html │ │ │ ├── routes │ │ │ │ ├── index.html │ │ │ │ └── users.js.html │ │ │ ├── controllers │ │ │ │ ├── index.html │ │ │ │ └── users.js.html │ │ │ ├── helpers │ │ │ │ ├── index.html │ │ │ │ └── routeHelpers.js.html │ │ │ ├── app.js.html │ │ │ └── passport.js.html │ │ ├── routes │ │ │ ├── index.html │ │ │ └── users.js.html │ │ ├── controllers │ │ │ ├── index.html │ │ │ └── users.js.html │ │ ├── helpers │ │ │ ├── index.html │ │ │ └── routeHelpers.js.html │ │ ├── configuration │ │ │ ├── index.html │ │ │ └── index.js.html │ │ ├── index.html │ │ ├── sorter.js │ │ ├── base.css │ │ └── prettify.js │ └── lcov.info └── server │ ├── routes │ └── users.test.js │ └── controllers │ └── users.test.js ├── .gitignore ├── .travis.yml ├── server ├── index.js ├── helpers │ └── routeHelpers.js ├── configuration │ └── index.js ├── app.js ├── routes │ └── users.js ├── models │ └── user.js ├── controllers │ └── users.js └── passport.js ├── .editorconfig ├── package.json └── README.md /client/.env: -------------------------------------------------------------------------------- 1 | SKIP_PREFLIGHT_CHECK=true -------------------------------------------------------------------------------- /client/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eXtremeXR/APIAuthenticationWithNode/HEAD/client/public/favicon.ico -------------------------------------------------------------------------------- /__tests__/report/lcov-report/sort-arrow-sprite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eXtremeXR/APIAuthenticationWithNode/HEAD/__tests__/report/lcov-report/sort-arrow-sprite.png -------------------------------------------------------------------------------- /client/src/components/Home.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default () => { 4 | return ( 5 |
6 | Welcome to our home page! 7 |
8 | ); 9 | }; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | /node_modules 3 | package-lock.json 4 | .DS_Store 5 | /.vimrc 6 | /.ackrc 7 | /coverage 8 | npm-debug.log 9 | yarn-error.log 10 | .idea 11 | .nyc_output 12 | /target 13 | /.vscode 14 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "8" 4 | services: 5 | - mongodb 6 | before_script: 7 | - yarn install 8 | script: 9 | - yarn test 10 | cache: 11 | directories: 12 | - node_modules 13 | -------------------------------------------------------------------------------- /server/index.js: -------------------------------------------------------------------------------- 1 | const app = require('./app'); 2 | 3 | // Start the server 4 | const port = process.env.PORT || 5000; 5 | app.listen(port); 6 | console.log(`Server listening at ${port}`); 7 | 8 | // refactored code for easier test and feature scale 9 | -------------------------------------------------------------------------------- /client/src/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import { reducer as formReducer } from 'redux-form'; 3 | import authReducer from './auth'; 4 | import dashboardReducer from './dashboard'; 5 | 6 | export default combineReducers({ 7 | form: formReducer, 8 | auth: authReducer, 9 | dash: dashboardReducer 10 | }); 11 | 12 | -------------------------------------------------------------------------------- /client/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | -------------------------------------------------------------------------------- /client/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /client/src/actions/types.js: -------------------------------------------------------------------------------- 1 | export const AUTH_SIGN_UP = 'AUTH_SIGN_UP'; 2 | export const AUTH_SIGN_OUT = 'AUTH_SIGN_OUT'; 3 | export const AUTH_SIGN_IN = 'AUTH_SIGN_IN'; 4 | export const AUTH_LINK_GOOGLE = 'AUTH_LINK_GOOGLE'; 5 | export const AUTH_LINK_FACEBOOK = 'AUTH_LINK_FACEBOOK'; 6 | export const AUTH_UNLINK_GOOGLE = 'AUTH_UNLINK_GOOGLE'; 7 | export const AUTH_UNLINK_FACEBOOK = 'AUTH_UNLINK_FACEBOOK'; 8 | 9 | export const AUTH_ERROR = 'AUTH_ERROR'; 10 | export const DASHBOARD_GET_DATA = 'DASHBOARD_GET_DATA'; 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | 8 | [*] 9 | 10 | # Change these settings to your own preference 11 | indent_style = space 12 | indent_size = 2 13 | 14 | # We recommend you to keep these unchanged 15 | end_of_line = lf 16 | charset = utf-8 17 | trim_trailing_whitespace = true 18 | insert_final_newline = true 19 | 20 | [*.md] 21 | trim_trailing_whitespace = false 22 | -------------------------------------------------------------------------------- /client/src/components/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | 4 | import Header from './Header'; 5 | import * as actions from '../actions'; 6 | 7 | class App extends Component { 8 | componentDidMount() { 9 | this.props.checkAuth(); 10 | } 11 | 12 | render() { 13 | return ( 14 |
15 |
16 |
17 | { this.props.children } 18 |
19 |
20 | ); 21 | } 22 | } 23 | 24 | export default connect(null, actions)(App); -------------------------------------------------------------------------------- /server/helpers/routeHelpers.js: -------------------------------------------------------------------------------- 1 | const Joi = require('joi'); 2 | 3 | module.exports = { 4 | validateBody: (schema) => { 5 | return (req, res, next) => { 6 | const result = Joi.validate(req.body, schema); 7 | if (result.error) { 8 | return res.status(400).json(result.error); 9 | } 10 | 11 | if (!req.value) { req.value = {}; } 12 | req.value['body'] = result.value; 13 | next(); 14 | } 15 | }, 16 | 17 | schemas: { 18 | authSchema: Joi.object().keys({ 19 | email: Joi.string().email().required(), 20 | password: Joi.string().required() 21 | }) 22 | } 23 | } -------------------------------------------------------------------------------- /client/src/components/CustomInput.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | export default class CustomInput extends Component { 4 | render() { 5 | const { input: { value, onChange } } = this.props; 6 | return ( 7 |
8 | 9 | 18 |
19 | ); 20 | } 21 | } -------------------------------------------------------------------------------- /__tests__/report/lcov-report/prettify.css: -------------------------------------------------------------------------------- 1 | .pln{color:#000}@media screen{.str{color:#080}.kwd{color:#008}.com{color:#800}.typ{color:#606}.lit{color:#066}.pun,.opn,.clo{color:#660}.tag{color:#008}.atn{color:#606}.atv{color:#080}.dec,.var{color:#606}.fun{color:red}}@media print,projection{.str{color:#060}.kwd{color:#006;font-weight:bold}.com{color:#600;font-style:italic}.typ{color:#404;font-weight:bold}.lit{color:#044}.pun,.opn,.clo{color:#440}.tag{color:#006;font-weight:bold}.atn{color:#404}.atv{color:#060}}pre.prettyprint{padding:2px;border:1px solid #888}ol.linenums{margin-top:0;margin-bottom:0}li.L0,li.L1,li.L2,li.L3,li.L5,li.L6,li.L7,li.L8{list-style-type:none}li.L1,li.L3,li.L5,li.L7,li.L9{background:#eee} 2 | -------------------------------------------------------------------------------- /server/configuration/index.js: -------------------------------------------------------------------------------- 1 | if (process.env.NODE_ENV === 'test') { 2 | module.exports = { 3 | JWT_SECRET: 'codeworkrauthentication', 4 | oauth: { 5 | google: { 6 | clientID: 'number', 7 | clientSecret: 'string', 8 | }, 9 | facebook: { 10 | clientID: 'number', 11 | clientSecret: 'string', 12 | }, 13 | }, 14 | }; 15 | } else { 16 | module.exports = { 17 | JWT_SECRET: 'codeworkrauthentication', 18 | oauth: { 19 | google: { 20 | clientID: 'number', 21 | clientSecret: 'string', 22 | }, 23 | facebook: { 24 | clientID: 'number', 25 | clientSecret: 'string', 26 | }, 27 | }, 28 | }; 29 | } -------------------------------------------------------------------------------- /client/src/reducers/auth.js: -------------------------------------------------------------------------------- 1 | import { 2 | AUTH_SIGN_UP, 3 | AUTH_SIGN_OUT, 4 | AUTH_SIGN_IN, 5 | AUTH_ERROR } from '../actions/types'; 6 | 7 | const DEFAULT_STATE = { 8 | isAuthenticated: false, 9 | errorMessage: '' 10 | } 11 | 12 | export default (state = DEFAULT_STATE, action) => { 13 | switch(action.type) { 14 | case AUTH_SIGN_UP: 15 | return { ...state, isAuthenticated: true, errorMessage: '' } 16 | case AUTH_SIGN_IN: 17 | return { ...state, isAuthenticated: true, errorMessage: '' } 18 | case AUTH_SIGN_OUT: 19 | return { ...state, isAuthenticated: false, errorMessage: '' } 20 | case AUTH_ERROR: 21 | return { ...state, errorMessage: action.payload } 22 | default: 23 | return state 24 | } 25 | } -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "client", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "react": "^16.8.6", 7 | "react-dom": "^16.8.6", 8 | "react-facebook-login": "^4.1.1", 9 | "react-google-login": "^5.0.4", 10 | "react-redux": "^6.0.1", 11 | "react-router-dom": "^5.0.0", 12 | "react-scripts": "2.1.8", 13 | "redux": "^4.0.1", 14 | "redux-form": "^8.1.0", 15 | "redux-thunk": "^2.3.0" 16 | }, 17 | "scripts": { 18 | "start": "react-scripts start", 19 | "build": "react-scripts build", 20 | "test": "react-scripts test --env=jsdom", 21 | "eject": "react-scripts eject" 22 | }, 23 | "browserslist": [ 24 | ">0.2%", 25 | "not dead", 26 | "not ie <= 11", 27 | "not op_mini all" 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /client/src/components/HOCs/authGuard.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | 4 | export default (OriginalComponent) => { 5 | class MixedComponent extends Component { 6 | 7 | checkAuth() { 8 | if (!this.props.isAuth && !this.props.jwtToken) { 9 | this.props.history.push('/'); 10 | } 11 | } 12 | 13 | componentDidMount() { 14 | this.checkAuth(); 15 | } 16 | 17 | componentDidUpdate() { 18 | this.checkAuth(); 19 | } 20 | 21 | render() { 22 | return ; 23 | } 24 | } 25 | 26 | function mapStateToProps(state) { 27 | return { 28 | isAuth: state.auth.isAuthenticated, 29 | jwtToken: state.auth.token 30 | } 31 | } 32 | 33 | return connect(mapStateToProps)(MixedComponent); 34 | }; 35 | 36 | 37 | -------------------------------------------------------------------------------- /client/src/reducers/dashboard.js: -------------------------------------------------------------------------------- 1 | import { 2 | DASHBOARD_GET_DATA, 3 | AUTH_LINK_GOOGLE, 4 | AUTH_LINK_FACEBOOK, 5 | AUTH_UNLINK_GOOGLE, 6 | AUTH_UNLINK_FACEBOOK 7 | } from '../actions/types'; 8 | 9 | const DEFAULT_STATE = { 10 | secret: '', 11 | methods: [] 12 | } 13 | 14 | export default (state = DEFAULT_STATE, action) => { 15 | switch(action.type) { 16 | case AUTH_LINK_GOOGLE: 17 | return { ...state, methods: action.payload.methods } 18 | case AUTH_LINK_FACEBOOK: 19 | return { ...state, methods: action.payload.methods } 20 | case AUTH_UNLINK_GOOGLE: 21 | return { ...state, methods: action.payload.methods } 22 | case AUTH_UNLINK_FACEBOOK: 23 | return { ...state, methods: action.payload.methods } 24 | case DASHBOARD_GET_DATA: 25 | return { ...state, secret: action.payload.secret, methods: action.payload.methods } 26 | default: 27 | return state 28 | } 29 | } -------------------------------------------------------------------------------- /server/app.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const morgan = require("morgan"); 3 | const mongoose = require("mongoose"); 4 | const cors = require("cors"); 5 | const cookieParser = require("cookie-parser"); 6 | 7 | mongoose.Promise = global.Promise; 8 | if (process.env.NODE_ENV === "test") { 9 | mongoose.connect("mongodb://localhost/APIAuthenticationTEST", { 10 | useNewUrlParser: true 11 | }); 12 | } else { 13 | mongoose.connect("mongodb://localhost/APIAuthentication", { 14 | useNewUrlParser: true 15 | }); 16 | } 17 | 18 | const app = express(); 19 | app.use(cookieParser()); 20 | app.use( 21 | cors({ 22 | origin: "http://localhost:3000", 23 | credentials: true 24 | }) 25 | ); 26 | 27 | // Middlewares moved morgan into if for clear tests 28 | if (!process.env.NODE_ENV === "test") { 29 | app.use(morgan("dev")); 30 | } 31 | 32 | app.use(express.json()); 33 | 34 | // Routes 35 | app.use("/users", require("./routes/users")); 36 | 37 | module.exports = app; 38 | -------------------------------------------------------------------------------- /__tests__/report/lcov.info: -------------------------------------------------------------------------------- 1 | TN: 2 | SF:/Users/michael/zStudy/forks/APIAuthenticationWithNode/server/controllers/users.js 3 | FN:5,(anonymous_0) 4 | FN:15,(anonymous_1) 5 | FN:41,(anonymous_2) 6 | FN:47,(anonymous_3) 7 | FN:53,(anonymous_4) 8 | FN:59,(anonymous_5) 9 | FNF:6 10 | FNH:6 11 | FNDA:6,(anonymous_0) 12 | FNDA:7,(anonymous_1) 13 | FNDA:3,(anonymous_2) 14 | FNDA:1,(anonymous_3) 15 | FNDA:1,(anonymous_4) 16 | FNDA:2,(anonymous_5) 17 | DA:1,2 18 | DA:2,2 19 | DA:3,2 20 | DA:5,2 21 | DA:6,6 22 | DA:14,2 23 | DA:16,7 24 | DA:19,7 25 | DA:20,7 26 | DA:21,2 27 | DA:25,5 28 | DA:33,5 29 | DA:36,5 30 | DA:38,5 31 | DA:43,3 32 | DA:44,3 33 | DA:49,1 34 | DA:50,1 35 | DA:55,1 36 | DA:56,1 37 | DA:60,2 38 | DA:61,2 39 | LF:22 40 | LH:22 41 | BRDA:20,0,0,2 42 | BRDA:20,0,1,5 43 | BRF:2 44 | BRH:2 45 | end_of_record 46 | TN: 47 | SF:/Users/michael/zStudy/forks/APIAuthenticationWithNode/server/routes/users.js 48 | FNF:0 49 | FNH:0 50 | DA:1,1 51 | DA:2,1 52 | DA:3,1 53 | DA:4,1 54 | DA:6,1 55 | DA:7,1 56 | DA:8,1 57 | DA:9,1 58 | DA:11,1 59 | DA:14,1 60 | DA:17,1 61 | DA:20,1 62 | DA:23,1 63 | DA:26,1 64 | LF:14 65 | LH:14 66 | BRF:0 67 | BRH:0 68 | end_of_record 69 | -------------------------------------------------------------------------------- /client/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { BrowserRouter, Route } from 'react-router-dom'; 4 | import { createStore, applyMiddleware } from 'redux'; 5 | import { Provider } from 'react-redux'; 6 | import reduxThunk from 'redux-thunk'; 7 | import axios from 'axios'; 8 | 9 | import registerServiceWorker from './registerServiceWorker'; 10 | import App from './components/App'; 11 | import Home from './components/Home'; 12 | import SignUp from './components/SignUp'; 13 | import SignIn from './components/SignIn'; 14 | import Dashboard from './components/Dashboard'; 15 | import reducers from './reducers'; 16 | 17 | import authGuard from './components/HOCs/authGuard'; 18 | 19 | axios.defaults.withCredentials = true; 20 | 21 | /* 22 | 1) Disable the httpOnly property :( 23 | 2) Fire off a request on app load to the BE which will check if the user is auth-ed 24 | */ 25 | 26 | ReactDOM.render( 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | , 37 | document.querySelector('#root')); 38 | registerServiceWorker(); 39 | -------------------------------------------------------------------------------- /server/routes/users.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = require('express-promise-router')(); 3 | const passport = require('passport'); 4 | const passportConf = require('../passport'); 5 | 6 | const { validateBody, schemas } = require('../helpers/routeHelpers'); 7 | const UsersController = require('../controllers/users'); 8 | const passportSignIn = passport.authenticate('local', { session: false }); 9 | const passportJWT = passport.authenticate('jwt', { session: false }); 10 | 11 | router.route('/signup') 12 | .post(validateBody(schemas.authSchema), UsersController.signUp); 13 | 14 | router.route('/signin') 15 | .post(validateBody(schemas.authSchema), passportSignIn, UsersController.signIn); 16 | 17 | router.route('/signout') 18 | .get(passportJWT, UsersController.signOut); 19 | 20 | router.route('/oauth/google') 21 | .post(passport.authenticate('googleToken', { session: false }), UsersController.googleOAuth); 22 | 23 | router.route('/oauth/facebook') 24 | .post(passport.authenticate('facebookToken', { session: false }), UsersController.facebookOAuth); 25 | 26 | router.route('/oauth/link/google') 27 | .post(passportJWT, passport.authorize('googleToken', { session: false }), UsersController.linkGoogle) 28 | 29 | router.route('/oauth/unlink/google') 30 | .post(passportJWT, UsersController.unlinkGoogle); 31 | 32 | router.route('/oauth/link/facebook') 33 | .post(passportJWT, passport.authorize('facebookToken', { session: false }), UsersController.linkFacebook) 34 | 35 | router.route('/oauth/unlink/facebook') 36 | .post(passportJWT, UsersController.unlinkFacebook); 37 | 38 | router.route('/dashboard') 39 | .get(passportJWT, UsersController.dashboard); 40 | 41 | router.route('/status') 42 | .get(passportJWT, UsersController.checkAuth); 43 | 44 | module.exports = router; -------------------------------------------------------------------------------- /client/src/components/Header.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import { connect } from 'react-redux'; 4 | 5 | import * as actions from '../actions'; 6 | 7 | class Header extends Component { 8 | constructor(props) { 9 | super(props); 10 | this.signOut = this.signOut.bind(this); 11 | } 12 | 13 | signOut() { 14 | this.props.signOut(); 15 | } 16 | 17 | render() { 18 | return ( 19 | 45 | ); 46 | } 47 | } 48 | 49 | function mapStateToProps(state) { 50 | return { 51 | isAuth: state.auth.isAuthenticated 52 | }; 53 | } 54 | 55 | export default connect(mapStateToProps, actions)(Header); 56 | 57 | 58 | -------------------------------------------------------------------------------- /server/models/user.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const bcrypt = require('bcryptjs'); 3 | const Schema = mongoose.Schema; 4 | 5 | // Create a schema 6 | const userSchema = new Schema({ 7 | methods: { 8 | type: [String], 9 | required: true 10 | }, 11 | local: { 12 | email: { 13 | type: String, 14 | lowercase: true 15 | }, 16 | password: { 17 | type: String 18 | } 19 | }, 20 | google: { 21 | id: { 22 | type: String 23 | }, 24 | email: { 25 | type: String, 26 | lowercase: true 27 | } 28 | }, 29 | facebook: { 30 | id: { 31 | type: String 32 | }, 33 | email: { 34 | type: String, 35 | lowercase: true 36 | } 37 | } 38 | }); 39 | 40 | userSchema.pre('save', async function (next) { 41 | try { 42 | console.log('entered'); 43 | if (!this.methods.includes('local')) { 44 | next(); 45 | } 46 | //the user schema is instantiated 47 | const user = this; 48 | //check if the user has been modified to know if the password has already been hashed 49 | if (!user.isModified('local.password')) { 50 | next(); 51 | } 52 | // Generate a salt 53 | const salt = await bcrypt.genSalt(10); 54 | // Generate a password hash (salt + hash) 55 | const passwordHash = await bcrypt.hash(this.local.password, salt); 56 | // Re-assign hashed version over original, plain text password 57 | this.local.password = passwordHash; 58 | console.log('exited'); 59 | next(); 60 | } catch (error) { 61 | next(error); 62 | } 63 | }); 64 | 65 | userSchema.methods.isValidPassword = async function (newPassword) { 66 | try { 67 | return await bcrypt.compare(newPassword, this.local.password); 68 | } catch (error) { 69 | throw new Error(error); 70 | } 71 | } 72 | 73 | // Create a model 74 | const User = mongoose.model('user', userSchema); 75 | 76 | // Export the model 77 | module.exports = User; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "apiauthentication", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "server-dev": "cross-env NODE_ENV=prod nodemon server/index.js", 8 | "client-dev": "npm start --prefix client", 9 | "start-dev": "concurrently \"npm run server-dev\" \"npm run client-dev\"", 10 | "test": "cross-env NODE_ENV=test mocha --timeout 5000 __tests__/server/**/*.test.js", 11 | "report": "cross-env NODE_ENV=test nyc --reporter=lcov mocha --timeout 5000 __tests__/server/**/*.test.js", 12 | "text-report": "cross-env NODE_ENV=test nyc --reporter=text-summary mocha --timeout 5000 __tests__/server/**/*.test.js", 13 | "nyc": "cross-env NODE_ENV=test nyc mocha --timeout 5000 __tests__/server/**/*.test.js" 14 | }, 15 | "author": "CodeWorkr", 16 | "license": "ISC", 17 | "dependencies": { 18 | "axios": "^0.21.1", 19 | "bcryptjs": "^2.4.3", 20 | "concurrently": "^4.1.0", 21 | "cookie-parser": "^1.4.4", 22 | "cors": "^2.8.5", 23 | "cross-env": "^5.2.0", 24 | "express": "^4.16.4", 25 | "express-promise-router": "^3.0.3", 26 | "joi": "^14.3.1", 27 | "jsonwebtoken": "^8.5.1", 28 | "mongoose": "^5.7.5", 29 | "morgan": "^1.9.1", 30 | "passport": "^0.4.0", 31 | "passport-facebook-token": "^3.3.0", 32 | "passport-google-plus-token": "^2.1.0", 33 | "passport-jwt": "^4.0.0", 34 | "passport-local": "^1.0.0" 35 | }, 36 | "devDependencies": { 37 | "chai": "^4.2.0", 38 | "chai-http": "^4.2.1", 39 | "faker": "^4.1.0", 40 | "mocha": "^6.1.1", 41 | "nodemon": "^1.18.10", 42 | "nyc": "^14.1.1", 43 | "rewire": "^4.0.1", 44 | "sinon": "^7.3.1", 45 | "sinon-chai": "^3.3.0" 46 | }, 47 | "nyc": { 48 | "exclude": [ 49 | "server/app.js", 50 | "server/configuration/index.js", 51 | "server/passport.js", 52 | "server/models/**/*.js", 53 | "server/helpers/**/*.js", 54 | "__tests__", 55 | "node_modules" 56 | ], 57 | "cache": false, 58 | "report-dir": "./__tests__/report" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /client/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | 13 | 14 | 23 | React App 24 | 25 | 26 | 29 |
30 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /client/src/components/Dashboard.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import GoogleLogin from 'react-google-login'; 4 | import FacebookLogin from 'react-facebook-login/dist/facebook-login-render-props' 5 | 6 | import * as actions from '../actions'; 7 | 8 | class Dashboard extends Component { 9 | async componentDidMount() { 10 | this.props.getDashboard() 11 | } 12 | 13 | linkFacebook = async (res) => { 14 | console.log('Link with Facebook', res) 15 | await this.props.linkFacebook(res.accessToken); 16 | } 17 | 18 | linkGoogle = async (res) => { 19 | console.log('Link with Google', res) 20 | await this.props.linkGoogle(res.accessToken); 21 | } 22 | 23 | unlinkGoogle = async () => { 24 | console.log('Unlink Google') 25 | await this.props.unlinkGoogle(); 26 | } 27 | 28 | unlinkFacebook = async (res) => { 29 | console.log('Unlink Facebook') 30 | await this.props.unlinkFacebook(); 31 | } 32 | 33 | render() { 34 | return ( 35 |
36 | This is a Dashboard component 37 |
38 | Our secret:

{this.props.secret}

39 | 40 |

Link your social media accounts

41 | ( 45 | 46 | )} 47 | fields="name,email,picture" 48 | callback={this.linkFacebook} 49 | cssClass="btn btn-outline-primary" 50 | /> 51 | ( 55 | 56 | )} 57 | onSuccess={this.linkGoogle} 58 | onFailure={this.linkGoogle} 59 | /> 60 |
61 |
62 |
63 |

Unlink your social media accounts

64 | 72 | 79 |
80 | ); 81 | } 82 | } 83 | 84 | function mapStateToProps(state) { 85 | console.log('state', state) 86 | return { 87 | secret: state.dash.secret, 88 | dashboard: state.dash, 89 | auth: state.auth 90 | } 91 | } 92 | 93 | export default connect(mapStateToProps, actions)(Dashboard); 94 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # API Authentication with Node 2 | > Modern Full-Stack Starter/Tutorial Using React/Redux/Node/Mongoose. 3 | 4 | [![Build Status](https://travis-ci.org/Alaev/APIAuthenticationWithNode.svg?branch=master)](https://travis-ci.org/Alaev/APIAuthenticationWithNode) 5 | 6 | This is the project files for CodeWorkr's API Authentication with Node series on YouTube. 7 | Series link: [YouTube Playlist](https://www.youtube.com/watch?v=zx6jnaLuB9Q&list=PLSpJkDDmpFZ7GowbJE-mvX09zY9zfYatI) 8 | 9 | ### Phase I - Backend 10 | 1. [Introduction](https://www.youtube.com/watch?v=zx6jnaLuB9Q) 11 | 2. [Cookies vs Tokens](https://www.youtube.com/watch?v=4Y5a_iKXihw) 12 | 3. [Node/Express API Setup](https://www.youtube.com/watch?v=x_HRoXKo2es) 13 | 4. [Server-Side Validation](https://www.youtube.com/watch?v=XFpV8b5937M) 14 | 5. [MongoDB/Mongoose](https://www.youtube.com/watch?v=QCJCglPLUgg) 15 | 6. [JSON Web Tokens](https://www.youtube.com/watch?v=YxFZC8FtRao) 16 | 7. [Passport and Strategies](https://www.youtube.com/watch?v=lbmOoZuElKI) 17 | 8. [Bcrypt Explained](https://www.youtube.com/watch?v=Peww_cdgka4) 18 | 9. [OAuth Explained](https://youtu.be/H0IxtqZ08Jo) 19 | 10. [Unit Testing](https://youtu.be/O1TYpzm5Uh0) 20 | 11. [Google OAuth](https://youtu.be/JgSLf-HS5gg) 21 | 12. [Facebook OAuth](https://youtu.be/MXle6TrjI64) 22 | 23 | ### Phase II - Frontend 24 | 13. [React Setup](https://youtu.be/NZdji3u5t4M) 25 | 14. [React UI with Routing](https://youtu.be/BerEJPQOwOE) 26 | 15. [Adding Redux and Redux Form](https://youtu.be/WRlw4lEijUk) 27 | 16. [Action Creators, Actions and Reducers](https://youtu.be/rIdHDaoCcXw) 28 | 17. [Google & Facebook OAuth Integration](https://youtu.be/W87udYTA1rw) 29 | 18. [React Higher Order Components](https://youtu.be/sKtSwu9vvDk) 30 | 31 | ### Phase III - Improvements! 32 | 19. [Improving - Introduction](https://youtu.be/LmElmaidFWg) 33 | 20. [Improving - Updating Dependencies](https://youtu.be/pnl1ecgDAYw) 34 | 21. [Improving - Linking Accounts](https://youtu.be/7yAmV17ufUY) 35 | 22. [Improving - Storing JWTs in Cookies](https://youtu.be/qPWkPZwMze0) 36 | 37 | ### To view desired branch for each episode simply clone this git repo 38 | 39 | ```bash 40 | $ git clone https://github.com/eXtremeXR/APIAuthenticationWithNode.git 41 | ``` 42 | 43 | then checkout the branch as follow 44 | 45 | ```bash 46 | $ git checkout 'branch_name' 47 | ``` 48 | 49 | This project uses async/await it would be best if you use **node 8+** 50 | after checkout be sure to run 51 | 52 | ```bash 53 | $ npm install 54 | ``` 55 | installing using yarn 56 | ```bash 57 | $ yarn install 58 | ``` 59 | To run the project use 60 | ```bash 61 | $ npm run start-dev 62 | ``` 63 | starting the project using yarn 64 | ```bash 65 | $ yarn start-dev 66 | ``` 67 | 68 | run unit tests 69 | ```bash 70 | $ yarn test 71 | ``` 72 | 73 | genarate code coverage report 74 | ```bash 75 | $ yarn report 76 | ``` 77 | 78 | run unit tests with nyc output in terminal 79 | ```bash 80 | $ yarn nyc 81 | ``` 82 | 83 | run unit tests with nyc summery report 84 | ```bash 85 | $ yarn text-report 86 | ``` 87 | 88 | ##### Code Coverage 89 | 90 | |File | % Stmts | % Branch | % Funcs | % Lines |Uncovered Lines | 91 | |-------------|----------|----------|----------|----------|----------------| 92 | |All files | 100 | 100 | 100 | 100 | | 93 | | controllers | 100 | 100 | 100 | 100 | | 94 | | users.js | 100 | 100 | 100 | 100 | | 95 | | routes | 100 | 100 | 100 | 100 | | 96 | | users.js | 100 | 100 | 100 | 100 | | 97 | 98 | ## Happy Coding! 99 | -------------------------------------------------------------------------------- /__tests__/report/lcov-report/server/configuration/index.js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Code coverage report for server/configuration/index.js 5 | 6 | 7 | 8 | 9 | 14 | 15 | 16 |
17 |
18 |

19 | All files / server/configuration index.js 20 |

21 |
22 |
23 | 100% 24 | Statements 25 | 1/1 26 |
27 |
28 | 100% 29 | Branches 30 | 0/0 31 |
32 |
33 | 100% 34 | Functions 35 | 0/0 36 |
37 |
38 | 100% 39 | Lines 40 | 1/1 41 |
42 |
43 |
44 |
45 |

 46 | 
 86 | 
1 47 | 2 48 | 3 49 | 4 50 | 5 51 | 6 52 | 7 53 | 8 54 | 9 55 | 10 56 | 11 57 | 12 58 | 13 59 | 141x 60 |   61 |   62 |   63 |   64 |   65 |   66 |   67 |   68 |   69 |   70 |   71 |   72 |  
module.exports = {
 73 |   JWT_SECRET: 'codeworkrauthentication',
 74 |   oauth: {
 75 |     google: {
 76 |       clientID: 'number',
 77 |       clientSecret: 'string'
 78 |     },
 79 |     facebook: {
 80 |       clientID: 'number',
 81 |       clientSecret: 'string'
 82 |     }
 83 |   }
 84 | };
 85 |  
87 |
88 |
89 | 93 | 94 | 95 | 102 | 103 | 104 | 105 | -------------------------------------------------------------------------------- /__tests__/report/lcov-report/server/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Code coverage report for server 5 | 6 | 7 | 8 | 9 | 14 | 15 | 16 |
17 |
18 |

19 | All files server 20 |

21 |
22 |
23 | 44.9% 24 | Statements 25 | 22/49 26 |
27 |
28 | 30% 29 | Branches 30 | 3/10 31 |
32 |
33 | 50% 34 | Functions 35 | 2/4 36 |
37 |
38 | 44.9% 39 | Lines 40 | 22/49 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 |
FileStatementsBranchesFunctionsLines
passport.js
44.9%22/4930%3/1050%2/444.9%22/49
76 |
77 |
78 | 82 | 83 | 84 | 91 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /__tests__/report/lcov-report/routes/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Code coverage report for routes 5 | 6 | 7 | 8 | 9 | 14 | 15 | 16 |
17 |
18 |

19 | All files routes 20 |

21 |
22 |
23 | 100% 24 | Statements 25 | 14/14 26 |
27 |
28 | 100% 29 | Branches 30 | 0/0 31 |
32 |
33 | 100% 34 | Functions 35 | 0/0 36 |
37 |
38 | 100% 39 | Lines 40 | 14/14 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 |
FileStatementsBranchesFunctionsLines
users.js
100%14/14100%0/0100%0/0100%14/14
76 |
77 |
78 | 82 | 83 | 84 | 91 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /__tests__/report/lcov-report/controllers/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Code coverage report for controllers 5 | 6 | 7 | 8 | 9 | 14 | 15 | 16 |
17 |
18 |

19 | All files controllers 20 |

21 |
22 |
23 | 100% 24 | Statements 25 | 22/22 26 |
27 |
28 | 100% 29 | Branches 30 | 2/2 31 |
32 |
33 | 100% 34 | Functions 35 | 6/6 36 |
37 |
38 | 100% 39 | Lines 40 | 22/22 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 |
FileStatementsBranchesFunctionsLines
users.js
100%22/22100%2/2100%6/6100%22/22
76 |
77 |
78 | 82 | 83 | 84 | 91 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /__tests__/report/lcov-report/helpers/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Code coverage report for helpers 5 | 6 | 7 | 8 | 9 | 14 | 15 | 16 |
17 |
18 |

19 | All files helpers 20 |

21 |
22 |
23 | 100% 24 | Statements 25 | 10/10 26 |
27 |
28 | 75% 29 | Branches 30 | 3/4 31 |
32 |
33 | 100% 34 | Functions 35 | 2/2 36 |
37 |
38 | 100% 39 | Lines 40 | 9/9 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 |
FileStatementsBranchesFunctionsLines
routeHelpers.js
100%10/1075%3/4100%2/2100%9/9
76 |
77 |
78 | 82 | 83 | 84 | 91 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /__tests__/report/lcov-report/configuration/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Code coverage report for configuration 5 | 6 | 7 | 8 | 9 | 14 | 15 | 16 |
17 |
18 |

19 | All files configuration 20 |

21 |
22 |
23 | 66.67% 24 | Statements 25 | 2/3 26 |
27 |
28 | 50% 29 | Branches 30 | 1/2 31 |
32 |
33 | 100% 34 | Functions 35 | 0/0 36 |
37 |
38 | 66.67% 39 | Lines 40 | 2/3 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 |
FileStatementsBranchesFunctionsLines
index.js
66.67%2/350%1/2100%0/066.67%2/3
76 |
77 |
78 | 82 | 83 | 84 | 91 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /__tests__/report/lcov-report/server/models/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Code coverage report for server/models 5 | 6 | 7 | 8 | 9 | 14 | 15 | 16 |
17 |
18 |

19 | All files server/models 20 |

21 |
22 |
23 | 88.24% 24 | Statements 25 | 15/17 26 |
27 |
28 | 100% 29 | Branches 30 | 0/0 31 |
32 |
33 | 100% 34 | Functions 35 | 2/2 36 |
37 |
38 | 88.24% 39 | Lines 40 | 15/17 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 |
FileStatementsBranchesFunctionsLines
user.js
88.24%15/17100%0/0100%2/288.24%15/17
76 |
77 |
78 | 82 | 83 | 84 | 91 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /__tests__/report/lcov-report/server/routes/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Code coverage report for server/routes 5 | 6 | 7 | 8 | 9 | 14 | 15 | 16 |
17 |
18 |

19 | All files server/routes 20 |

21 |
22 |
23 | 100% 24 | Statements 25 | 14/14 26 |
27 |
28 | 100% 29 | Branches 30 | 0/0 31 |
32 |
33 | 100% 34 | Functions 35 | 0/0 36 |
37 |
38 | 100% 39 | Lines 40 | 14/14 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 |
FileStatementsBranchesFunctionsLines
users.js
100%14/14100%0/0100%0/0100%14/14
76 |
77 |
78 | 82 | 83 | 84 | 91 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /__tests__/report/lcov-report/server/configuration/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Code coverage report for server/configuration 5 | 6 | 7 | 8 | 9 | 14 | 15 | 16 |
17 |
18 |

19 | All files server/configuration 20 |

21 |
22 |
23 | 100% 24 | Statements 25 | 1/1 26 |
27 |
28 | 100% 29 | Branches 30 | 0/0 31 |
32 |
33 | 100% 34 | Functions 35 | 0/0 36 |
37 |
38 | 100% 39 | Lines 40 | 1/1 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 |
FileStatementsBranchesFunctionsLines
index.js
100%1/1100%0/0100%0/0100%1/1
76 |
77 |
78 | 82 | 83 | 84 | 91 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /__tests__/report/lcov-report/server/controllers/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Code coverage report for server/controllers 5 | 6 | 7 | 8 | 9 | 14 | 15 | 16 |
17 |
18 |

19 | All files server/controllers 20 |

21 |
22 |
23 | 100% 24 | Statements 25 | 22/22 26 |
27 |
28 | 100% 29 | Branches 30 | 2/2 31 |
32 |
33 | 100% 34 | Functions 35 | 6/6 36 |
37 |
38 | 100% 39 | Lines 40 | 22/22 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 |
FileStatementsBranchesFunctionsLines
users.js
100%22/22100%2/2100%6/6100%22/22
76 |
77 |
78 | 82 | 83 | 84 | 91 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /__tests__/report/lcov-report/server/helpers/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Code coverage report for server/helpers 5 | 6 | 7 | 8 | 9 | 14 | 15 | 16 |
17 |
18 |

19 | All files server/helpers 20 |

21 |
22 |
23 | 100% 24 | Statements 25 | 10/10 26 |
27 |
28 | 75% 29 | Branches 30 | 3/4 31 |
32 |
33 | 100% 34 | Functions 35 | 2/2 36 |
37 |
38 | 100% 39 | Lines 40 | 9/9 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 |
FileStatementsBranchesFunctionsLines
routeHelpers.js
100%10/1075%3/4100%2/2100%9/9
76 |
77 |
78 | 82 | 83 | 84 | 91 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /client/src/components/SignIn.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { reduxForm, Field } from 'redux-form'; 3 | import { connect } from 'react-redux'; 4 | import { compose } from 'redux'; 5 | import GoogleLogin from 'react-google-login'; 6 | import FacebookLogin from 'react-facebook-login/dist/facebook-login-render-props' 7 | 8 | import * as actions from '../actions'; 9 | import CustomInput from './CustomInput'; 10 | 11 | class SignIn extends Component { 12 | constructor(props) { 13 | super(props); 14 | this.onSubmit = this.onSubmit.bind(this); 15 | this.responseGoogle = this.responseGoogle.bind(this); 16 | this.responseFacebook = this.responseFacebook.bind(this); 17 | } 18 | 19 | async onSubmit(formData) { 20 | await this.props.signIn(formData); 21 | if (!this.props.errorMessage) { 22 | this.props.history.push('/dashboard'); 23 | } 24 | } 25 | 26 | async responseGoogle(res) { 27 | await this.props.oauthGoogle(res.accessToken); 28 | if (!this.props.errorMessage) { 29 | this.props.history.push('/dashboard'); 30 | } 31 | } 32 | 33 | async responseFacebook(res) { 34 | await this.props.oauthFacebook(res.accessToken); 35 | if (!this.props.errorMessage) { 36 | this.props.history.push('/dashboard'); 37 | } 38 | } 39 | 40 | render() { 41 | const { handleSubmit } = this.props; 42 | return ( 43 |
44 |
45 |
46 |
47 | 54 |
55 |
56 | 63 |
64 | 65 | { this.props.errorMessage ? 66 |
67 | { this.props.errorMessage } 68 |
: null } 69 | 70 | 71 |
72 |
73 |
74 |
75 |
76 | Or sign in using third-party services 77 |
78 | ( 81 | 82 | )} 83 | fields="name,email,picture" 84 | callback={this.responseFacebook} 85 | cssClass="btn btn-outline-primary" 86 | /> 87 | ( 90 | 91 | )} 92 | onSuccess={this.responseGoogle} 93 | onFailure={this.responseGoogle} 94 | className="btn btn-outline-danger" 95 | /> 96 |
97 |
98 |
99 | ); 100 | } 101 | } 102 | 103 | function mapStateToProps(state) { 104 | return { 105 | errorMessage: state.auth.errorMessage 106 | } 107 | } 108 | 109 | export default compose( 110 | connect(mapStateToProps, actions), 111 | reduxForm({ form: 'signin' }) 112 | )(SignIn) 113 | -------------------------------------------------------------------------------- /client/src/components/SignUp.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { reduxForm, Field } from 'redux-form'; 3 | import { connect } from 'react-redux'; 4 | import { compose } from 'redux'; 5 | import GoogleLogin from 'react-google-login'; 6 | import FacebookLogin from 'react-facebook-login/dist/facebook-login-render-props' 7 | 8 | import * as actions from '../actions'; 9 | import CustomInput from './CustomInput'; 10 | 11 | class SignUp extends Component { 12 | constructor(props) { 13 | super(props); 14 | this.onSubmit = this.onSubmit.bind(this); 15 | this.responseGoogle = this.responseGoogle.bind(this); 16 | this.responseFacebook = this.responseFacebook.bind(this); 17 | } 18 | 19 | async onSubmit(formData) { 20 | await this.props.signUp(formData); 21 | if (!this.props.errorMessage) { 22 | this.props.history.push('/dashboard'); 23 | } 24 | } 25 | 26 | async responseGoogle(res) { 27 | await this.props.oauthGoogle(res.accessToken); 28 | if (!this.props.errorMessage) { 29 | this.props.history.push('/dashboard'); 30 | } 31 | } 32 | 33 | async responseFacebook(res) { 34 | await this.props.oauthFacebook(res.accessToken); 35 | if (!this.props.errorMessage) { 36 | this.props.history.push('/dashboard'); 37 | } 38 | } 39 | 40 | render() { 41 | const { handleSubmit } = this.props; 42 | return ( 43 |
44 |
45 |
46 |
47 | 54 |
55 |
56 | 63 |
64 | 65 | { this.props.errorMessage ? 66 |
67 | { this.props.errorMessage } 68 |
: null } 69 | 70 | 71 |
72 |
73 |
74 |
75 |
76 | Or sign up using third-party services 77 |
78 | ( 81 | 82 | )} 83 | fields="name,email,picture" 84 | callback={this.responseFacebook} 85 | cssClass="btn btn-outline-primary" 86 | /> 87 | ( 90 | 91 | )} 92 | onSuccess={this.responseGoogle} 93 | onFailure={this.responseGoogle} 94 | className="btn btn-outline-danger" 95 | /> 96 |
97 |
98 |
99 | ); 100 | } 101 | } 102 | 103 | function mapStateToProps(state) { 104 | return { 105 | errorMessage: state.auth.errorMessage 106 | } 107 | } 108 | 109 | export default compose( 110 | connect(mapStateToProps, actions), 111 | reduxForm({ form: 'signup' }) 112 | )(SignUp) 113 | -------------------------------------------------------------------------------- /__tests__/server/routes/users.test.js: -------------------------------------------------------------------------------- 1 | // will work for linux for windows we are going to user cross-env in package json 2 | //process.env.NODE_ENV = 'test'; 3 | 4 | const chai = require('chai'); 5 | const chaiHttp = require('chai-http'); 6 | const faker = require('faker'); 7 | const mongoose = require('mongoose'); 8 | const { expect } = chai; 9 | 10 | const server = require('../../../server/app'); 11 | 12 | chai.use(chaiHttp); 13 | 14 | let token; 15 | 16 | describe('Users route', () => { 17 | const signup = '/users/signup'; 18 | const signin = '/users/signin'; 19 | const secret = '/users/secret'; 20 | const user = { 21 | email: faker.internet.email(), 22 | password: faker.internet.password(), 23 | }; 24 | const preSave = { 25 | email: 'mr.sometest@gmail.com', 26 | password: faker.internet.password(), 27 | }; 28 | 29 | before(async () => { 30 | const result = await chai 31 | .request(server) 32 | .post(signup) 33 | .send(preSave); 34 | expect(result.status).to.equal(200); 35 | token = result.body.token; 36 | }); 37 | 38 | // after all test have run we drop our test database 39 | after('droping test db', async () => { 40 | await mongoose.connection.dropDatabase(() => { 41 | console.log('\n Test database dropped'); 42 | }); 43 | await mongoose.connection.close(); 44 | }); 45 | 46 | describe('signup', () => { 47 | it('should crete new user if email not found', async () => { 48 | try { 49 | const result = await chai 50 | .request(server) 51 | .post(signup) 52 | .send(user); 53 | expect(result.status).to.equal(200); 54 | expect(result.body).not.to.be.empty; 55 | expect(result.body).to.have.property('token'); 56 | } catch (error) { 57 | console.log(error); 58 | } 59 | }); 60 | 61 | it('should return 403 if email was found', async () => { 62 | try { 63 | await chai 64 | .request(server) 65 | .post(signup) 66 | .send(preSave); 67 | } catch (error) { 68 | expect(error.status).to.equal(403); 69 | expect(error.response.text).to.equal('{"error":"Email is already in use"}'); 70 | } 71 | }); 72 | }); 73 | 74 | describe('secrete', () => { 75 | it('should return status 401', async () => { 76 | try { 77 | await chai.request(server).get(secret); 78 | } catch (error) { 79 | expect(error.status).to.equal(401); 80 | expect(error.response.text).to.equal('Unauthorized'); 81 | } 82 | }); 83 | 84 | it('should return status 200', async () => { 85 | try { 86 | const result = await chai 87 | .request(server) 88 | .get(secret) 89 | .set('Authorization', token); 90 | 91 | expect(result.status).to.equal(200); 92 | expect(result.body).to.deep.equal({ secret: 'resource' }); 93 | } catch (error) { 94 | throw new Error(error); 95 | } 96 | }); 97 | }); 98 | 99 | describe('signin', () => { 100 | it('should return error 400 if user email and password empty', async () => { 101 | let user = {}; 102 | try { 103 | const result = await chai 104 | .request(server) 105 | .post(signin) 106 | .send(user); 107 | } catch (error) { 108 | expect(error.status).to.be.equal(400); 109 | } 110 | }); 111 | 112 | it('should return 200 and our token', async () => { 113 | try { 114 | const result = await chai 115 | .request(server) 116 | .post(signin) 117 | .send(preSave); 118 | 119 | expect(result.status).to.be.equal(200); 120 | expect(result.body).not.to.be.empty; 121 | expect(result.body).to.have.property('token'); 122 | } catch (error) { 123 | throw new Error(error); 124 | } 125 | }); 126 | }); 127 | }); 128 | -------------------------------------------------------------------------------- /client/src/actions/index.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { 3 | AUTH_SIGN_UP, 4 | AUTH_SIGN_OUT, 5 | AUTH_SIGN_IN, 6 | AUTH_LINK_GOOGLE, 7 | AUTH_LINK_FACEBOOK, 8 | AUTH_UNLINK_GOOGLE, 9 | AUTH_UNLINK_FACEBOOK, 10 | AUTH_ERROR, 11 | DASHBOARD_GET_DATA } from './types'; 12 | 13 | export const oauthGoogle = data => { 14 | return async dispatch => { 15 | await axios.post('http://localhost:5000/users/oauth/google', { 16 | access_token: data 17 | }); 18 | 19 | dispatch({ 20 | type: AUTH_SIGN_UP 21 | }); 22 | }; 23 | } 24 | 25 | export const linkGoogle = data => { 26 | return async dispatch => { 27 | const res = await axios.post('http://localhost:5000/users/oauth/link/google', { 28 | access_token: data 29 | }); 30 | 31 | dispatch({ 32 | type: AUTH_LINK_GOOGLE, 33 | payload: res.data 34 | }); 35 | }; 36 | } 37 | 38 | export const unlinkGoogle = data => { 39 | return async dispatch => { 40 | const res = await axios.post('http://localhost:5000/users/oauth/unlink/google'); 41 | 42 | dispatch({ 43 | type: AUTH_UNLINK_GOOGLE, 44 | payload: res.data 45 | }); 46 | }; 47 | } 48 | 49 | export const linkFacebook = data => { 50 | return async dispatch => { 51 | const res = await axios.post('http://localhost:5000/users/oauth/link/facebook', { 52 | access_token: data 53 | }); 54 | 55 | dispatch({ 56 | type: AUTH_LINK_FACEBOOK, 57 | payload: res.data 58 | }); 59 | }; 60 | } 61 | 62 | export const unlinkFacebook = data => { 63 | return async dispatch => { 64 | const res = await axios.post('http://localhost:5000/users/oauth/unlink/facebook'); 65 | 66 | dispatch({ 67 | type: AUTH_UNLINK_FACEBOOK, 68 | payload: res.data 69 | }); 70 | }; 71 | } 72 | 73 | export const oauthFacebook = data => { 74 | return async dispatch => { 75 | await axios.post('http://localhost:5000/users/oauth/facebook', { 76 | access_token: data 77 | }); 78 | 79 | dispatch({ 80 | type: AUTH_SIGN_UP 81 | }); 82 | }; 83 | } 84 | 85 | export const signUp = data => { 86 | return async dispatch => { 87 | try { 88 | await axios.post('http://localhost:5000/users/signup', data); 89 | 90 | dispatch({ 91 | type: AUTH_SIGN_UP 92 | }); 93 | } catch(err) { 94 | dispatch({ 95 | type: AUTH_ERROR, 96 | payload: 'Email is already in use' 97 | }) 98 | } 99 | }; 100 | } 101 | 102 | export const signIn = data => { 103 | return async dispatch => { 104 | try { 105 | await axios.post('http://localhost:5000/users/signin', data); 106 | 107 | dispatch({ 108 | type: AUTH_SIGN_IN 109 | }); 110 | } catch(err) { 111 | dispatch({ 112 | type: AUTH_ERROR, 113 | payload: 'Email and password combination isn\'t valid' 114 | }) 115 | } 116 | }; 117 | } 118 | 119 | export const checkAuth = () => { 120 | return async dispatch => { 121 | try { 122 | await axios.get('http://localhost:5000/users/status'); 123 | 124 | dispatch({ 125 | type: AUTH_SIGN_IN 126 | }); 127 | 128 | console.log('user is auth-ed') 129 | } catch(err) { 130 | console.log('error', err) 131 | } 132 | }; 133 | } 134 | 135 | export const getDashboard = () => { 136 | return async dispatch => { 137 | try { 138 | const res = await axios.get('http://localhost:5000/users/dashboard') 139 | 140 | dispatch({ 141 | type: DASHBOARD_GET_DATA, 142 | payload: res.data 143 | }) 144 | 145 | } catch(err) { 146 | console.error('err', err) 147 | } 148 | } 149 | } 150 | 151 | export const signOut = () => { 152 | return async dispatch => { 153 | await axios.get('http://localhost:5000/users/signout'); 154 | 155 | dispatch({ 156 | type: AUTH_SIGN_OUT 157 | }) 158 | }; 159 | } 160 | -------------------------------------------------------------------------------- /client/src/registerServiceWorker.js: -------------------------------------------------------------------------------- 1 | // In production, we register a service worker to serve assets from local cache. 2 | 3 | // This lets the app load faster on subsequent visits in production, and gives 4 | // it offline capabilities. However, it also means that developers (and users) 5 | // will only see deployed updates on the "N+1" visit to a page, since previously 6 | // cached resources are updated in the background. 7 | 8 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy. 9 | // This link also includes instructions on opting out of this behavior. 10 | 11 | const isLocalhost = Boolean( 12 | window.location.hostname === 'localhost' || 13 | // [::1] is the IPv6 localhost address. 14 | window.location.hostname === '[::1]' || 15 | // 127.0.0.1/8 is considered localhost for IPv4. 16 | window.location.hostname.match( 17 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 18 | ) 19 | ); 20 | 21 | export default function register() { 22 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 23 | // The URL constructor is available in all browsers that support SW. 24 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location); 25 | if (publicUrl.origin !== window.location.origin) { 26 | // Our service worker won't work if PUBLIC_URL is on a different origin 27 | // from what our page is served on. This might happen if a CDN is used to 28 | // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374 29 | return; 30 | } 31 | 32 | window.addEventListener('load', () => { 33 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 34 | 35 | if (isLocalhost) { 36 | // This is running on localhost. Lets check if a service worker still exists or not. 37 | checkValidServiceWorker(swUrl); 38 | } else { 39 | // Is not local host. Just register service worker 40 | registerValidSW(swUrl); 41 | } 42 | }); 43 | } 44 | } 45 | 46 | function registerValidSW(swUrl) { 47 | navigator.serviceWorker 48 | .register(swUrl) 49 | .then(registration => { 50 | registration.onupdatefound = () => { 51 | const installingWorker = registration.installing; 52 | installingWorker.onstatechange = () => { 53 | if (installingWorker.state === 'installed') { 54 | if (navigator.serviceWorker.controller) { 55 | // At this point, the old content will have been purged and 56 | // the fresh content will have been added to the cache. 57 | // It's the perfect time to display a "New content is 58 | // available; please refresh." message in your web app. 59 | console.log('New content is available; please refresh.'); 60 | } else { 61 | // At this point, everything has been precached. 62 | // It's the perfect time to display a 63 | // "Content is cached for offline use." message. 64 | console.log('Content is cached for offline use.'); 65 | } 66 | } 67 | }; 68 | }; 69 | }) 70 | .catch(error => { 71 | console.error('Error during service worker registration:', error); 72 | }); 73 | } 74 | 75 | function checkValidServiceWorker(swUrl) { 76 | // Check if the service worker can be found. If it can't reload the page. 77 | fetch(swUrl) 78 | .then(response => { 79 | // Ensure service worker exists, and that we really are getting a JS file. 80 | if ( 81 | response.status === 404 || 82 | response.headers.get('content-type').indexOf('javascript') === -1 83 | ) { 84 | // No service worker found. Probably a different app. Reload the page. 85 | navigator.serviceWorker.ready.then(registration => { 86 | registration.unregister().then(() => { 87 | window.location.reload(); 88 | }); 89 | }); 90 | } else { 91 | // Service worker found. Proceed as normal. 92 | registerValidSW(swUrl); 93 | } 94 | }) 95 | .catch(() => { 96 | console.log( 97 | 'No internet connection found. App is running in offline mode.' 98 | ); 99 | }); 100 | } 101 | 102 | export function unregister() { 103 | if ('serviceWorker' in navigator) { 104 | navigator.serviceWorker.ready.then(registration => { 105 | registration.unregister(); 106 | }); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /__tests__/report/lcov-report/helpers/routeHelpers.js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Code coverage report for helpers/routeHelpers.js 5 | 6 | 7 | 8 | 9 | 14 | 15 | 16 |
17 |
18 |

19 | All files / helpers routeHelpers.js 20 |

21 |
22 |
23 | 100% 24 | Statements 25 | 10/10 26 |
27 |
28 | 75% 29 | Branches 30 | 3/4 31 |
32 |
33 | 100% 34 | Functions 35 | 2/2 36 |
37 |
38 | 100% 39 | Lines 40 | 9/9 41 |
42 |
43 |
44 |
45 |

 46 | 
113 | 
1 47 | 2 48 | 3 49 | 4 50 | 5 51 | 6 52 | 7 53 | 8 54 | 9 55 | 10 56 | 11 57 | 12 58 | 13 59 | 14 60 | 15 61 | 16 62 | 17 63 | 18 64 | 19 65 | 20 66 | 21 67 | 22 68 | 231x 69 |   70 | 1x 71 |   72 | 2x 73 | 5x 74 | 5x 75 | 1x 76 |   77 |   78 | 4x 79 | 4x 80 | 4x 81 |   82 |   83 |   84 |   85 |   86 |   87 |   88 |   89 |   90 |  
const Joi = require('joi');
 91 |  
 92 | module.exports = {
 93 |   validateBody: (schema) => {
 94 |     return (req, res, next) => {
 95 |       const result = Joi.validate(req.body, schema);
 96 |       if (result.error) {
 97 |         return res.status(400).json(result.error);
 98 |       }
 99 |  
100 |       Eif (!req.value) { req.value = {}; }
101 |       req.value['body'] = result.value;
102 |       next();
103 |     }
104 |   },
105 |  
106 |   schemas: {
107 |     authSchema: Joi.object().keys({
108 |       email: Joi.string().email().required(),
109 |       password: Joi.string().required()
110 |     })
111 |   }
112 | }
114 |
115 |
116 | 120 | 121 | 122 | 129 | 130 | 131 | 132 | -------------------------------------------------------------------------------- /__tests__/report/lcov-report/server/helpers/routeHelpers.js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Code coverage report for server/helpers/routeHelpers.js 5 | 6 | 7 | 8 | 9 | 14 | 15 | 16 |
17 |
18 |

19 | All files / server/helpers routeHelpers.js 20 |

21 |
22 |
23 | 100% 24 | Statements 25 | 10/10 26 |
27 |
28 | 75% 29 | Branches 30 | 3/4 31 |
32 |
33 | 100% 34 | Functions 35 | 2/2 36 |
37 |
38 | 100% 39 | Lines 40 | 9/9 41 |
42 |
43 |
44 |
45 |

 46 | 
113 | 
1 47 | 2 48 | 3 49 | 4 50 | 5 51 | 6 52 | 7 53 | 8 54 | 9 55 | 10 56 | 11 57 | 12 58 | 13 59 | 14 60 | 15 61 | 16 62 | 17 63 | 18 64 | 19 65 | 20 66 | 21 67 | 22 68 | 231x 69 |   70 | 1x 71 |   72 | 2x 73 | 5x 74 | 5x 75 | 1x 76 |   77 |   78 | 4x 79 | 4x 80 | 4x 81 |   82 |   83 |   84 |   85 |   86 |   87 |   88 |   89 |   90 |  
const Joi = require('joi');
 91 |  
 92 | module.exports = {
 93 |   validateBody: (schema) => {
 94 |     return (req, res, next) => {
 95 |       const result = Joi.validate(req.body, schema);
 96 |       if (result.error) {
 97 |         return res.status(400).json(result.error);
 98 |       }
 99 |  
100 |       Eif (!req.value) { req.value = {}; }
101 |       req.value['body'] = result.value;
102 |       next();
103 |     }
104 |   },
105 |  
106 |   schemas: {
107 |     authSchema: Joi.object().keys({
108 |       email: Joi.string().email().required(),
109 |       password: Joi.string().required()
110 |     })
111 |   }
112 | }
114 |
115 |
116 | 120 | 121 | 122 | 129 | 130 | 131 | 132 | -------------------------------------------------------------------------------- /__tests__/report/lcov-report/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Code coverage report for All files 5 | 6 | 7 | 8 | 9 | 14 | 15 | 16 |
17 |
18 |

19 | All files 20 |

21 |
22 |
23 | 100% 24 | Statements 25 | 36/36 26 |
27 |
28 | 100% 29 | Branches 30 | 2/2 31 |
32 |
33 | 100% 34 | Functions 35 | 6/6 36 |
37 |
38 | 100% 39 | Lines 40 | 36/36 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 |
FileStatementsBranchesFunctionsLines
controllers
100%22/22100%2/2100%6/6100%22/22
routes
100%14/14100%0/0100%0/0100%14/14
89 |
90 |
91 | 95 | 96 | 97 | 104 | 105 | 106 | 107 | -------------------------------------------------------------------------------- /__tests__/report/lcov-report/server/app.js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Code coverage report for server/app.js 5 | 6 | 7 | 8 | 9 | 14 | 15 | 16 |
17 |
18 |

19 | All files / server app.js 20 |

21 |
22 |
23 | 85.71% 24 | Statements 25 | 12/14 26 |
27 |
28 | 50% 29 | Branches 30 | 2/4 31 |
32 |
33 | 100% 34 | Functions 35 | 0/0 36 |
37 |
38 | 85.71% 39 | Lines 40 | 12/14 41 |
42 |
43 |
44 |
45 |

 46 | 
122 | 
1 47 | 2 48 | 3 49 | 4 50 | 5 51 | 6 52 | 7 53 | 8 54 | 9 55 | 10 56 | 11 57 | 12 58 | 13 59 | 14 60 | 15 61 | 16 62 | 17 63 | 18 64 | 19 65 | 20 66 | 21 67 | 22 68 | 23 69 | 24 70 | 25 71 | 261x 72 | 1x 73 | 1x 74 | 1x 75 |   76 | 1x 77 | 1x 78 | 1x 79 |   80 |   81 |   82 |   83 | 1x 84 |   85 |   86 | 1x 87 |   88 |   89 |   90 | 1x 91 |   92 |   93 | 1x 94 |   95 | 1x 96 |  
const express = require('express');
 97 | const morgan = require('morgan');
 98 | const bodyParser = require('body-parser');
 99 | const mongoose = require('mongoose');
100 |  
101 | mongoose.Promise = global.Promise;
102 | Eif (process.env.NODE_ENV === 'test') {
103 |   mongoose.connect('mongodb://localhost/APIAuthenticationTEST', { useMongoClient: true });
104 | } else {
105 |   mongoose.connect('mongodb://localhost/APIAuthentication', { useMongoClient: true });
106 | }
107 |  
108 | const app = express();
109 |  
110 | // Middlewares moved morgan into if for clear tests
111 | Iif (!process.env.NODE_ENV === 'test') {
112 |   app.use(morgan('dev'));
113 | }
114 |  
115 | app.use(bodyParser.json());
116 |  
117 | // Routes
118 | app.use('/users', require('./routes/users'));
119 |  
120 | module.exports = app;
121 |  
123 |
124 |
125 | 129 | 130 | 131 | 138 | 139 | 140 | 141 | -------------------------------------------------------------------------------- /__tests__/report/lcov-report/routes/users.js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Code coverage report for routes/users.js 5 | 6 | 7 | 8 | 9 | 14 | 15 | 16 |
17 |
18 |

19 | All files / routes users.js 20 |

21 |
22 |
23 | 100% 24 | Statements 25 | 14/14 26 |
27 |
28 | 100% 29 | Branches 30 | 0/0 31 |
32 |
33 | 100% 34 | Functions 35 | 0/0 36 |
37 |
38 | 100% 39 | Lines 40 | 14/14 41 |
42 |
43 |
44 |
45 |

 46 | 
122 | 
1 47 | 2 48 | 3 49 | 4 50 | 5 51 | 6 52 | 7 53 | 8 54 | 9 55 | 10 56 | 11 57 | 12 58 | 13 59 | 14 60 | 15 61 | 16 62 | 17 63 | 18 64 | 19 65 | 20 66 | 21 67 | 22 68 | 23 69 | 24 70 | 25 71 | 261x 72 | 1x 73 | 1x 74 | 1x 75 |   76 | 1x 77 | 1x 78 | 1x 79 | 1x 80 |   81 | 1x 82 |   83 |   84 | 1x 85 |   86 |   87 | 1x 88 |   89 |   90 | 1x 91 |   92 |   93 | 1x 94 |   95 |   96 | 1x
const express = require('express');
 97 | const router = require('express-promise-router')();
 98 | const passport = require('passport');
 99 | const passportConf = require('../passport');
100 |  
101 | const { validateBody, schemas } = require('../helpers/routeHelpers');
102 | const UsersController = require('../controllers/users');
103 | const passportSignIn = passport.authenticate('local', { session: false });
104 | const passportJWT = passport.authenticate('jwt', { session: false });
105 |  
106 | router.route('/signup')
107 |   .post(validateBody(schemas.authSchema), UsersController.signUp);
108 |  
109 | router.route('/signin')
110 |   .post(validateBody(schemas.authSchema), passportSignIn, UsersController.signIn);
111 |  
112 | router.route('/oauth/google')
113 |   .post(passport.authenticate('googleToken', { session: false }), UsersController.googleOAuth);
114 |  
115 | router.route('/oauth/facebook')
116 |   .post(passport.authenticate('facebookToken', { session: false }), UsersController.facebookOAuth);
117 |  
118 | router.route('/secret')
119 |   .get(passportJWT, UsersController.secret);
120 |  
121 | module.exports = router;
123 |
124 |
125 | 129 | 130 | 131 | 138 | 139 | 140 | 141 | -------------------------------------------------------------------------------- /__tests__/report/lcov-report/configuration/index.js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Code coverage report for configuration/index.js 5 | 6 | 7 | 8 | 9 | 14 | 15 | 16 |
17 |
18 |

19 | All files / configuration index.js 20 |

21 |
22 |
23 | 66.67% 24 | Statements 25 | 2/3 26 |
27 |
28 | 50% 29 | Branches 30 | 1/2 31 |
32 |
33 | 100% 34 | Functions 35 | 0/0 36 |
37 |
38 | 66.67% 39 | Lines 40 | 2/3 41 |
42 |
43 |
44 |
45 |

 46 | 
134 | 
1 47 | 2 48 | 3 49 | 4 50 | 5 51 | 6 52 | 7 53 | 8 54 | 9 55 | 10 56 | 11 57 | 12 58 | 13 59 | 14 60 | 15 61 | 16 62 | 17 63 | 18 64 | 19 65 | 20 66 | 21 67 | 22 68 | 23 69 | 24 70 | 25 71 | 26 72 | 27 73 | 28 74 | 29 75 | 301x 76 | 1x 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 |  
Eif (process.env.NODE_ENV === 'test') {
105 |   module.exports = {
106 |     JWT_SECRET: 'codeworkrauthentication',
107 |     oauth: {
108 |       google: {
109 |         clientID: 'number',
110 |         clientSecret: 'string',
111 |       },
112 |       facebook: {
113 |         clientID: 'number',
114 |         clientSecret: 'string',
115 |       },
116 |     },
117 |   };
118 | } else {
119 |   module.exports = {
120 |     JWT_SECRET: '',
121 |     oauth: {
122 |       google: {
123 |         clientID: '',
124 |         clientSecret: '',
125 |       },
126 |       facebook: {
127 |         clientID: '',
128 |         clientSecret: '',
129 |       },
130 |     },
131 |   };
132 | }
133 |  
135 |
136 |
137 | 141 | 142 | 143 | 150 | 151 | 152 | 153 | -------------------------------------------------------------------------------- /__tests__/report/lcov-report/server/routes/users.js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Code coverage report for server/routes/users.js 5 | 6 | 7 | 8 | 9 | 14 | 15 | 16 |
17 |
18 |

19 | All files / server/routes users.js 20 |

21 |
22 |
23 | 100% 24 | Statements 25 | 14/14 26 |
27 |
28 | 100% 29 | Branches 30 | 0/0 31 |
32 |
33 | 100% 34 | Functions 35 | 0/0 36 |
37 |
38 | 100% 39 | Lines 40 | 14/14 41 |
42 |
43 |
44 |
45 |

 46 | 
122 | 
1 47 | 2 48 | 3 49 | 4 50 | 5 51 | 6 52 | 7 53 | 8 54 | 9 55 | 10 56 | 11 57 | 12 58 | 13 59 | 14 60 | 15 61 | 16 62 | 17 63 | 18 64 | 19 65 | 20 66 | 21 67 | 22 68 | 23 69 | 24 70 | 25 71 | 261x 72 | 1x 73 | 1x 74 | 1x 75 |   76 | 1x 77 | 1x 78 | 1x 79 | 1x 80 |   81 | 1x 82 |   83 |   84 | 1x 85 |   86 |   87 | 1x 88 |   89 |   90 | 1x 91 |   92 |   93 | 1x 94 |   95 |   96 | 1x
const express = require('express');
 97 | const router = require('express-promise-router')();
 98 | const passport = require('passport');
 99 | const passportConf = require('../passport');
100 |  
101 | const { validateBody, schemas } = require('../helpers/routeHelpers');
102 | const UsersController = require('../controllers/users');
103 | const passportSignIn = passport.authenticate('local', { session: false });
104 | const passportJWT = passport.authenticate('jwt', { session: false });
105 |  
106 | router.route('/signup')
107 |   .post(validateBody(schemas.authSchema), UsersController.signUp);
108 |  
109 | router.route('/signin')
110 |   .post(validateBody(schemas.authSchema), passportSignIn, UsersController.signIn);
111 |  
112 | router.route('/oauth/google')
113 |   .post(passport.authenticate('googleToken', { session: false }), UsersController.googleOAuth);
114 |  
115 | router.route('/oauth/facebook')
116 |   .post(passport.authenticate('facebookToken', { session: false }), UsersController.facebookOAuth);
117 |  
118 | router.route('/secret')
119 |   .get(passportJWT, UsersController.secret);
120 |  
121 | module.exports = router;
123 |
124 |
125 | 129 | 130 | 131 | 138 | 139 | 140 | 141 | -------------------------------------------------------------------------------- /server/controllers/users.js: -------------------------------------------------------------------------------- 1 | const JWT = require('jsonwebtoken'); 2 | const User = require('../models/user'); 3 | const { JWT_SECRET } = require('../configuration'); 4 | 5 | signToken = user => { 6 | return JWT.sign({ 7 | iss: 'CodeWorkr', 8 | sub: user.id, 9 | iat: new Date().getTime(), // current time 10 | exp: new Date().setDate(new Date().getDate() + 1) // current time + 1 day ahead 11 | }, JWT_SECRET); 12 | } 13 | 14 | module.exports = { 15 | signUp: async (req, res, next) => { 16 | const { email, password } = req.value.body; 17 | 18 | // Check if there is a user with the same email 19 | let foundUser = await User.findOne({ "local.email": email }); 20 | if (foundUser) { 21 | return res.status(403).json({ error: 'Email is already in use'}); 22 | } 23 | 24 | // Is there a Google account with the same email? 25 | foundUser = await User.findOne({ 26 | $or: [ 27 | { "google.email": email }, 28 | { "facebook.email": email }, 29 | ] 30 | }); 31 | if (foundUser) { 32 | // Let's merge them? 33 | foundUser.methods.push('local') 34 | foundUser.local = { 35 | email: email, 36 | password: password 37 | } 38 | await foundUser.save() 39 | // Generate the token 40 | const token = signToken(foundUser); 41 | // Respond with token 42 | res.cookie('access_token', token, { 43 | httpOnly: true 44 | }); 45 | res.status(200).json({ success: true }); 46 | } 47 | 48 | // Is there a Google account with the same email? 49 | // foundUser = await User.findOne({ "facebook.email": email }); 50 | // if (foundUser) { 51 | // // Let's merge them? 52 | // foundUser.methods.push('local') 53 | // foundUser.local = { 54 | // email: email, 55 | // password: password 56 | // } 57 | // await foundUser.save() 58 | // // Generate the token 59 | // const token = signToken(foundUser); 60 | // // Respond with token 61 | // res.status(200).json({ token }); 62 | // } 63 | 64 | // Create a new user 65 | const newUser = new User({ 66 | methods: ['local'], 67 | local: { 68 | email: email, 69 | password: password 70 | } 71 | }); 72 | 73 | await newUser.save(); 74 | 75 | // Generate the token 76 | const token = signToken(newUser); 77 | // Send a cookie containing JWT 78 | res.cookie('access_token', token, { 79 | httpOnly: true 80 | }); 81 | res.status(200).json({ success: true }); 82 | }, 83 | 84 | signIn: async (req, res, next) => { 85 | // Generate token 86 | const token = signToken(req.user); 87 | res.cookie('access_token', token, { 88 | httpOnly: true 89 | }); 90 | res.status(200).json({ success: true }); 91 | }, 92 | 93 | signOut: async (req, res, next) => { 94 | res.clearCookie('access_token'); 95 | // console.log('I managed to get here!'); 96 | res.json({ success: true }); 97 | }, 98 | 99 | googleOAuth: async (req, res, next) => { 100 | // Generate token 101 | const token = signToken(req.user); 102 | res.cookie('access_token', token, { 103 | httpOnly: true 104 | }); 105 | res.status(200).json({ success: true }); 106 | }, 107 | 108 | linkGoogle: async (req, res, next) => { 109 | res.json({ 110 | success: true, 111 | methods: req.user.methods, 112 | message: 'Successfully linked account with Google' 113 | }); 114 | }, 115 | 116 | unlinkGoogle: async (req, res, next) => { 117 | // Delete Google sub-object 118 | if (req.user.google) { 119 | req.user.google = undefined 120 | } 121 | // Remove 'google' from methods array 122 | const googleStrPos = req.user.methods.indexOf('google') 123 | if (googleStrPos >= 0) { 124 | req.user.methods.splice(googleStrPos, 1) 125 | } 126 | await req.user.save() 127 | 128 | // Return something? 129 | res.json({ 130 | success: true, 131 | methods: req.user.methods, 132 | message: 'Successfully unlinked account from Google' 133 | }); 134 | }, 135 | 136 | facebookOAuth: async (req, res, next) => { 137 | // Generate token 138 | const token = signToken(req.user); 139 | res.cookie('access_token', token, { 140 | httpOnly: true 141 | }); 142 | res.status(200).json({ success: true }); 143 | }, 144 | 145 | linkFacebook: async (req, res, next) => { 146 | res.json({ 147 | success: true, 148 | methods: req.user.methods, 149 | message: 'Successfully linked account with Facebook' 150 | }); 151 | }, 152 | 153 | unlinkFacebook: async (req, res, next) => { 154 | // Delete Facebook sub-object 155 | if (req.user.facebook) { 156 | req.user.facebook = undefined 157 | } 158 | // Remove 'facebook' from methods array 159 | const facebookStrPos = req.user.methods.indexOf('facebook') 160 | if (facebookStrPos >= 0) { 161 | req.user.methods.splice(facebookStrPos, 1) 162 | } 163 | await req.user.save() 164 | 165 | // Return something? 166 | res.json({ 167 | success: true, 168 | methods: req.user.methods, 169 | message: 'Successfully unlinked account from Facebook' 170 | }); 171 | }, 172 | 173 | dashboard: async (req, res, next) => { 174 | console.log('I managed to get here!'); 175 | res.json({ 176 | secret: "resource", 177 | methods: req.user.methods 178 | }); 179 | }, 180 | 181 | checkAuth: async (req, res, next) => { 182 | console.log('I managed to get here!'); 183 | res.json({ success: true }); 184 | } 185 | } -------------------------------------------------------------------------------- /__tests__/report/lcov-report/sorter.js: -------------------------------------------------------------------------------- 1 | var addSorting = (function () { 2 | "use strict"; 3 | var cols, 4 | currentSort = { 5 | index: 0, 6 | desc: false 7 | }; 8 | 9 | // returns the summary table element 10 | function getTable() { return document.querySelector('.coverage-summary'); } 11 | // returns the thead element of the summary table 12 | function getTableHeader() { return getTable().querySelector('thead tr'); } 13 | // returns the tbody element of the summary table 14 | function getTableBody() { return getTable().querySelector('tbody'); } 15 | // returns the th element for nth column 16 | function getNthColumn(n) { return getTableHeader().querySelectorAll('th')[n]; } 17 | 18 | // loads all columns 19 | function loadColumns() { 20 | var colNodes = getTableHeader().querySelectorAll('th'), 21 | colNode, 22 | cols = [], 23 | col, 24 | i; 25 | 26 | for (i = 0; i < colNodes.length; i += 1) { 27 | colNode = colNodes[i]; 28 | col = { 29 | key: colNode.getAttribute('data-col'), 30 | sortable: !colNode.getAttribute('data-nosort'), 31 | type: colNode.getAttribute('data-type') || 'string' 32 | }; 33 | cols.push(col); 34 | if (col.sortable) { 35 | col.defaultDescSort = col.type === 'number'; 36 | colNode.innerHTML = colNode.innerHTML + ''; 37 | } 38 | } 39 | return cols; 40 | } 41 | // attaches a data attribute to every tr element with an object 42 | // of data values keyed by column name 43 | function loadRowData(tableRow) { 44 | var tableCols = tableRow.querySelectorAll('td'), 45 | colNode, 46 | col, 47 | data = {}, 48 | i, 49 | val; 50 | for (i = 0; i < tableCols.length; i += 1) { 51 | colNode = tableCols[i]; 52 | col = cols[i]; 53 | val = colNode.getAttribute('data-value'); 54 | if (col.type === 'number') { 55 | val = Number(val); 56 | } 57 | data[col.key] = val; 58 | } 59 | return data; 60 | } 61 | // loads all row data 62 | function loadData() { 63 | var rows = getTableBody().querySelectorAll('tr'), 64 | i; 65 | 66 | for (i = 0; i < rows.length; i += 1) { 67 | rows[i].data = loadRowData(rows[i]); 68 | } 69 | } 70 | // sorts the table using the data for the ith column 71 | function sortByIndex(index, desc) { 72 | var key = cols[index].key, 73 | sorter = function (a, b) { 74 | a = a.data[key]; 75 | b = b.data[key]; 76 | return a < b ? -1 : a > b ? 1 : 0; 77 | }, 78 | finalSorter = sorter, 79 | tableBody = document.querySelector('.coverage-summary tbody'), 80 | rowNodes = tableBody.querySelectorAll('tr'), 81 | rows = [], 82 | i; 83 | 84 | if (desc) { 85 | finalSorter = function (a, b) { 86 | return -1 * sorter(a, b); 87 | }; 88 | } 89 | 90 | for (i = 0; i < rowNodes.length; i += 1) { 91 | rows.push(rowNodes[i]); 92 | tableBody.removeChild(rowNodes[i]); 93 | } 94 | 95 | rows.sort(finalSorter); 96 | 97 | for (i = 0; i < rows.length; i += 1) { 98 | tableBody.appendChild(rows[i]); 99 | } 100 | } 101 | // removes sort indicators for current column being sorted 102 | function removeSortIndicators() { 103 | var col = getNthColumn(currentSort.index), 104 | cls = col.className; 105 | 106 | cls = cls.replace(/ sorted$/, '').replace(/ sorted-desc$/, ''); 107 | col.className = cls; 108 | } 109 | // adds sort indicators for current column being sorted 110 | function addSortIndicators() { 111 | getNthColumn(currentSort.index).className += currentSort.desc ? ' sorted-desc' : ' sorted'; 112 | } 113 | // adds event listeners for all sorter widgets 114 | function enableUI() { 115 | var i, 116 | el, 117 | ithSorter = function ithSorter(i) { 118 | var col = cols[i]; 119 | 120 | return function () { 121 | var desc = col.defaultDescSort; 122 | 123 | if (currentSort.index === i) { 124 | desc = !currentSort.desc; 125 | } 126 | sortByIndex(i, desc); 127 | removeSortIndicators(); 128 | currentSort.index = i; 129 | currentSort.desc = desc; 130 | addSortIndicators(); 131 | }; 132 | }; 133 | for (i =0 ; i < cols.length; i += 1) { 134 | if (cols[i].sortable) { 135 | // add the click event handler on the th so users 136 | // dont have to click on those tiny arrows 137 | el = getNthColumn(i).querySelector('.sorter').parentElement; 138 | if (el.addEventListener) { 139 | el.addEventListener('click', ithSorter(i)); 140 | } else { 141 | el.attachEvent('onclick', ithSorter(i)); 142 | } 143 | } 144 | } 145 | } 146 | // adds sorting functionality to the UI 147 | return function () { 148 | if (!getTable()) { 149 | return; 150 | } 151 | cols = loadColumns(); 152 | loadData(cols); 153 | addSortIndicators(); 154 | enableUI(); 155 | }; 156 | })(); 157 | 158 | window.addEventListener('load', addSorting); 159 | -------------------------------------------------------------------------------- /__tests__/report/lcov-report/base.css: -------------------------------------------------------------------------------- 1 | body, html { 2 | margin:0; padding: 0; 3 | height: 100%; 4 | } 5 | body { 6 | font-family: Helvetica Neue, Helvetica, Arial; 7 | font-size: 14px; 8 | color:#333; 9 | } 10 | .small { font-size: 12px; } 11 | *, *:after, *:before { 12 | -webkit-box-sizing:border-box; 13 | -moz-box-sizing:border-box; 14 | box-sizing:border-box; 15 | } 16 | h1 { font-size: 20px; margin: 0;} 17 | h2 { font-size: 14px; } 18 | pre { 19 | font: 12px/1.4 Consolas, "Liberation Mono", Menlo, Courier, monospace; 20 | margin: 0; 21 | padding: 0; 22 | -moz-tab-size: 2; 23 | -o-tab-size: 2; 24 | tab-size: 2; 25 | } 26 | a { color:#0074D9; text-decoration:none; } 27 | a:hover { text-decoration:underline; } 28 | .strong { font-weight: bold; } 29 | .space-top1 { padding: 10px 0 0 0; } 30 | .pad2y { padding: 20px 0; } 31 | .pad1y { padding: 10px 0; } 32 | .pad2x { padding: 0 20px; } 33 | .pad2 { padding: 20px; } 34 | .pad1 { padding: 10px; } 35 | .space-left2 { padding-left:55px; } 36 | .space-right2 { padding-right:20px; } 37 | .center { text-align:center; } 38 | .clearfix { display:block; } 39 | .clearfix:after { 40 | content:''; 41 | display:block; 42 | height:0; 43 | clear:both; 44 | visibility:hidden; 45 | } 46 | .fl { float: left; } 47 | @media only screen and (max-width:640px) { 48 | .col3 { width:100%; max-width:100%; } 49 | .hide-mobile { display:none!important; } 50 | } 51 | 52 | .quiet { 53 | color: #7f7f7f; 54 | color: rgba(0,0,0,0.5); 55 | } 56 | .quiet a { opacity: 0.7; } 57 | 58 | .fraction { 59 | font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace; 60 | font-size: 10px; 61 | color: #555; 62 | background: #E8E8E8; 63 | padding: 4px 5px; 64 | border-radius: 3px; 65 | vertical-align: middle; 66 | } 67 | 68 | div.path a:link, div.path a:visited { color: #333; } 69 | table.coverage { 70 | border-collapse: collapse; 71 | margin: 10px 0 0 0; 72 | padding: 0; 73 | } 74 | 75 | table.coverage td { 76 | margin: 0; 77 | padding: 0; 78 | vertical-align: top; 79 | } 80 | table.coverage td.line-count { 81 | text-align: right; 82 | padding: 0 5px 0 20px; 83 | } 84 | table.coverage td.line-coverage { 85 | text-align: right; 86 | padding-right: 10px; 87 | min-width:20px; 88 | } 89 | 90 | table.coverage td span.cline-any { 91 | display: inline-block; 92 | padding: 0 5px; 93 | width: 100%; 94 | } 95 | .missing-if-branch { 96 | display: inline-block; 97 | margin-right: 5px; 98 | border-radius: 3px; 99 | position: relative; 100 | padding: 0 4px; 101 | background: #333; 102 | color: yellow; 103 | } 104 | 105 | .skip-if-branch { 106 | display: none; 107 | margin-right: 10px; 108 | position: relative; 109 | padding: 0 4px; 110 | background: #ccc; 111 | color: white; 112 | } 113 | .missing-if-branch .typ, .skip-if-branch .typ { 114 | color: inherit !important; 115 | } 116 | .coverage-summary { 117 | border-collapse: collapse; 118 | width: 100%; 119 | } 120 | .coverage-summary tr { border-bottom: 1px solid #bbb; } 121 | .keyline-all { border: 1px solid #ddd; } 122 | .coverage-summary td, .coverage-summary th { padding: 10px; } 123 | .coverage-summary tbody { border: 1px solid #bbb; } 124 | .coverage-summary td { border-right: 1px solid #bbb; } 125 | .coverage-summary td:last-child { border-right: none; } 126 | .coverage-summary th { 127 | text-align: left; 128 | font-weight: normal; 129 | white-space: nowrap; 130 | } 131 | .coverage-summary th.file { border-right: none !important; } 132 | .coverage-summary th.pct { } 133 | .coverage-summary th.pic, 134 | .coverage-summary th.abs, 135 | .coverage-summary td.pct, 136 | .coverage-summary td.abs { text-align: right; } 137 | .coverage-summary td.file { white-space: nowrap; } 138 | .coverage-summary td.pic { min-width: 120px !important; } 139 | .coverage-summary tfoot td { } 140 | 141 | .coverage-summary .sorter { 142 | height: 10px; 143 | width: 7px; 144 | display: inline-block; 145 | margin-left: 0.5em; 146 | background: url(sort-arrow-sprite.png) no-repeat scroll 0 0 transparent; 147 | } 148 | .coverage-summary .sorted .sorter { 149 | background-position: 0 -20px; 150 | } 151 | .coverage-summary .sorted-desc .sorter { 152 | background-position: 0 -10px; 153 | } 154 | .status-line { height: 10px; } 155 | /* dark red */ 156 | .red.solid, .status-line.low, .low .cover-fill { background:#C21F39 } 157 | .low .chart { border:1px solid #C21F39 } 158 | /* medium red */ 159 | .cstat-no, .fstat-no, .cbranch-no, .cbranch-no { background:#F6C6CE } 160 | /* light red */ 161 | .low, .cline-no { background:#FCE1E5 } 162 | /* light green */ 163 | .high, .cline-yes { background:rgb(230,245,208) } 164 | /* medium green */ 165 | .cstat-yes { background:rgb(161,215,106) } 166 | /* dark green */ 167 | .status-line.high, .high .cover-fill { background:rgb(77,146,33) } 168 | .high .chart { border:1px solid rgb(77,146,33) } 169 | 170 | 171 | .medium .chart { border:1px solid #666; } 172 | .medium .cover-fill { background: #666; } 173 | 174 | .cbranch-no { background: yellow !important; color: #111; } 175 | 176 | .cstat-skip { background: #ddd; color: #111; } 177 | .fstat-skip { background: #ddd; color: #111 !important; } 178 | .cbranch-skip { background: #ddd !important; color: #111; } 179 | 180 | span.cline-neutral { background: #eaeaea; } 181 | .medium { background: #eaeaea; } 182 | 183 | .cover-fill, .cover-empty { 184 | display:inline-block; 185 | height: 12px; 186 | } 187 | .chart { 188 | line-height: 0; 189 | } 190 | .cover-empty { 191 | background: white; 192 | } 193 | .cover-full { 194 | border-right: none !important; 195 | } 196 | pre.prettyprint { 197 | border: none !important; 198 | padding: 0 !important; 199 | margin: 0 !important; 200 | } 201 | .com { color: #999 !important; } 202 | .ignore-none { color: #999; font-weight: normal; } 203 | 204 | .wrapper { 205 | min-height: 100%; 206 | height: auto !important; 207 | height: 100%; 208 | margin: 0 auto -48px; 209 | } 210 | .footer, .push { 211 | height: 48px; 212 | } 213 | -------------------------------------------------------------------------------- /server/passport.js: -------------------------------------------------------------------------------- 1 | const passport = require('passport'); 2 | const JwtStrategy = require('passport-jwt').Strategy; 3 | const { ExtractJwt } = require('passport-jwt'); 4 | const LocalStrategy = require('passport-local').Strategy; 5 | const GooglePlusTokenStrategy = require('passport-google-plus-token'); 6 | const FacebookTokenStrategy = require('passport-facebook-token'); 7 | const config = require('./configuration'); 8 | const User = require('./models/user'); 9 | 10 | const cookieExtractor = req => { 11 | let token = null; 12 | if (req && req.cookies) { 13 | token = req.cookies['access_token']; 14 | } 15 | return token; 16 | } 17 | 18 | // JSON WEB TOKENS STRATEGY 19 | passport.use(new JwtStrategy({ 20 | jwtFromRequest: cookieExtractor, 21 | secretOrKey: config.JWT_SECRET, 22 | passReqToCallback: true 23 | }, async (req, payload, done) => { 24 | try { 25 | // Find the user specified in token 26 | const user = await User.findById(payload.sub); 27 | 28 | // If user doesn't exists, handle it 29 | if (!user) { 30 | return done(null, false); 31 | } 32 | 33 | // Otherwise, return the user 34 | req.user = user; 35 | done(null, user); 36 | } catch(error) { 37 | done(error, false); 38 | } 39 | })); 40 | 41 | // Google OAuth Strategy 42 | passport.use('googleToken', new GooglePlusTokenStrategy({ 43 | clientID: config.oauth.google.clientID, 44 | clientSecret: config.oauth.google.clientSecret, 45 | passReqToCallback: true, 46 | }, async (req, accessToken, refreshToken, profile, done) => { 47 | try { 48 | // Could get accessed in two ways: 49 | // 1) When registering for the first time 50 | // 2) When linking account to the existing one 51 | 52 | // Should have full user profile over here 53 | console.log('profile', profile); 54 | console.log('accessToken', accessToken); 55 | console.log('refreshToken', refreshToken); 56 | 57 | if (req.user) { 58 | // We're already logged in, time for linking account! 59 | // Add Google's data to an existing account 60 | req.user.methods.push('google') 61 | req.user.google = { 62 | id: profile.id, 63 | email: profile.emails[0].value 64 | } 65 | await req.user.save() 66 | return done(null, req.user); 67 | } else { 68 | // We're in the account creation process 69 | let existingUser = await User.findOne({ "google.id": profile.id }); 70 | if (existingUser) { 71 | return done(null, existingUser); 72 | } 73 | 74 | // Check if we have someone with the same email 75 | existingUser = await User.findOne({ "local.email": profile.emails[0].value }) 76 | if (existingUser) { 77 | // We want to merge google's data with local auth 78 | existingUser.methods.push('google') 79 | existingUser.google = { 80 | id: profile.id, 81 | email: profile.emails[0].value 82 | } 83 | await existingUser.save() 84 | return done(null, existingUser); 85 | } 86 | 87 | const newUser = new User({ 88 | methods: ['google'], 89 | google: { 90 | id: profile.id, 91 | email: profile.emails[0].value 92 | } 93 | }); 94 | 95 | await newUser.save(); 96 | done(null, newUser); 97 | } 98 | } catch(error) { 99 | done(error, false, error.message); 100 | } 101 | })); 102 | 103 | passport.use('facebookToken', new FacebookTokenStrategy({ 104 | clientID: config.oauth.facebook.clientID, 105 | clientSecret: config.oauth.facebook.clientSecret, 106 | passReqToCallback: true 107 | }, async (req, accessToken, refreshToken, profile, done) => { 108 | try { 109 | console.log('profile', profile); 110 | console.log('accessToken', accessToken); 111 | console.log('refreshToken', refreshToken); 112 | 113 | if (req.user) { 114 | // We're already logged in, time for linking account! 115 | // Add Facebook's data to an existing account 116 | req.user.methods.push('facebook') 117 | req.user.facebook = { 118 | id: profile.id, 119 | email: profile.emails[0].value 120 | } 121 | await req.user.save(); 122 | return done(null, req.user); 123 | } else { 124 | // We're in the account creation process 125 | let existingUser = await User.findOne({ "facebook.id": profile.id }); 126 | if (existingUser) { 127 | return done(null, existingUser); 128 | } 129 | 130 | // Check if we have someone with the same email 131 | existingUser = await User.findOne({ "local.email": profile.emails[0].value }) 132 | if (existingUser) { 133 | // We want to merge facebook's data with local auth 134 | existingUser.methods.push('facebook') 135 | existingUser.facebook = { 136 | id: profile.id, 137 | email: profile.emails[0].value 138 | } 139 | await existingUser.save() 140 | return done(null, existingUser); 141 | } 142 | 143 | const newUser = new User({ 144 | methods: ['facebook'], 145 | facebook: { 146 | id: profile.id, 147 | email: profile.emails[0].value 148 | } 149 | }); 150 | 151 | await newUser.save(); 152 | done(null, newUser); 153 | } 154 | } catch(error) { 155 | done(error, false, error.message); 156 | } 157 | })); 158 | 159 | // LOCAL STRATEGY 160 | passport.use(new LocalStrategy({ 161 | usernameField: 'email' 162 | }, async (email, password, done) => { 163 | try { 164 | // Find the user given the email 165 | const user = await User.findOne({ "local.email": email }); 166 | 167 | // If not, handle it 168 | if (!user) { 169 | return done(null, false); 170 | } 171 | 172 | // Check if the password is correct 173 | const isMatch = await user.isValidPassword(password); 174 | 175 | // If not, handle it 176 | if (!isMatch) { 177 | return done(null, false); 178 | } 179 | 180 | // Otherwise, return the user 181 | done(null, user); 182 | } catch(error) { 183 | done(error, false); 184 | } 185 | })); -------------------------------------------------------------------------------- /__tests__/report/lcov-report/server/models/user.js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Code coverage report for server/models/user.js 5 | 6 | 7 | 8 | 9 | 14 | 15 | 16 |
17 |
18 |

19 | All files / server/models user.js 20 |

21 |
22 |
23 | 88.24% 24 | Statements 25 | 15/17 26 |
27 |
28 | 100% 29 | Branches 30 | 0/0 31 |
32 |
33 | 100% 34 | Functions 35 | 2/2 36 |
37 |
38 | 88.24% 39 | Lines 40 | 15/17 41 |
42 |
43 |
44 |
45 |

 46 | 
179 | 
1 47 | 2 48 | 3 49 | 4 50 | 5 51 | 6 52 | 7 53 | 8 54 | 9 55 | 10 56 | 11 57 | 12 58 | 13 59 | 14 60 | 15 61 | 16 62 | 17 63 | 18 64 | 19 65 | 20 66 | 21 67 | 22 68 | 23 69 | 24 70 | 25 71 | 26 72 | 27 73 | 28 74 | 29 75 | 30 76 | 31 77 | 32 78 | 33 79 | 34 80 | 35 81 | 36 82 | 37 83 | 38 84 | 39 85 | 40 86 | 41 87 | 42 88 | 43 89 | 44 90 | 451x 91 | 1x 92 | 1x 93 |   94 |   95 | 1x 96 |   97 |   98 |   99 |   100 |   101 |   102 |   103 |   104 |   105 |   106 |   107 |   108 | 1x 109 | 2x 110 |   111 | 2x 112 |   113 | 2x 114 |   115 | 2x 116 | 2x 117 |   118 |   119 |   120 |   121 |   122 | 1x 123 | 1x 124 | 1x 125 |   126 |   127 |   128 |   129 |   130 |   131 | 1x 132 |   133 |   134 | 1x
const mongoose = require('mongoose');
135 | const bcrypt = require('bcryptjs');
136 | const Schema = mongoose.Schema;
137 |  
138 | // Create a schema
139 | const userSchema = new Schema({
140 |   email: {
141 |     type: String,
142 |     required: true,
143 |     unique: true,
144 |     lowercase: true
145 |   },
146 |   password: { 
147 |     type: String,
148 |     required: true
149 |   }
150 | });
151 |  
152 | userSchema.pre('save', async function(next) {
153 |   try {
154 |     // Generate a salt
155 |     const salt = await bcrypt.genSalt(10);
156 |     // Generate a password hash (salt + hash)
157 |     const passwordHash = await bcrypt.hash(this.password, salt);
158 |     // Re-assign hashed version over original, plain text password
159 |     this.password = passwordHash;
160 |     next();
161 |   } catch(error) {
162 |     next(error);
163 |   }
164 | });
165 |  
166 | userSchema.methods.isValidPassword = async function(newPassword) {
167 |   try {
168 |     return await bcrypt.compare(newPassword, this.password);
169 |   } catch(error) {
170 |     throw new Error(error);
171 |   }
172 | }
173 |  
174 | // Create a model
175 | const User = mongoose.model('user', userSchema);
176 |  
177 | // Export the model
178 | module.exports = User;
180 |
181 |
182 | 186 | 187 | 188 | 195 | 196 | 197 | 198 | -------------------------------------------------------------------------------- /__tests__/server/controllers/users.test.js: -------------------------------------------------------------------------------- 1 | // will work for linux for windows we are going to user cross-env in package json 2 | //process.env.NODE_ENV = 'test'; 3 | 4 | const chai = require('chai'); 5 | const faker = require('faker'); 6 | const sinon = require('sinon'); 7 | const sinonChai = require('sinon-chai'); 8 | const rewire = require('rewire'); 9 | const { expect } = chai; 10 | 11 | const User = require('../../../server/models/user'); 12 | const userController = rewire('../../../server/controllers/users.js'); 13 | 14 | chai.use(sinonChai); 15 | 16 | let sandbox = null; 17 | 18 | describe('Users controller', () => { 19 | let req = { 20 | user: { 21 | id: faker.random.number(), 22 | }, 23 | value: { 24 | body: { 25 | email: faker.internet.email(), 26 | password: faker.internet.password(), 27 | }, 28 | }, 29 | }; 30 | let res = { 31 | json: function() { 32 | return this; 33 | }, 34 | status: function() { 35 | return this; 36 | }, 37 | }; 38 | 39 | beforeEach(() => { 40 | sandbox = sinon.sandbox.create(); 41 | }); 42 | 43 | afterEach(() => { 44 | sandbox.restore(); 45 | }); 46 | 47 | describe('secret', () => { 48 | it('should return resource when called', async () => { 49 | sandbox.spy(console, 'log'); 50 | sandbox.spy(res, 'json'); 51 | 52 | try { 53 | await userController.secret(req, res); 54 | 55 | expect(console.log).to.have.been.called; 56 | expect( 57 | res.json.calledWith({ 58 | secret: 'resource', 59 | }), 60 | ).to.be.ok; 61 | expect(res.json).to.have.been.calledWith({ 62 | secret: 'resource', 63 | }); 64 | } catch (error) { 65 | throw new Error(error); 66 | } 67 | }); 68 | }); 69 | 70 | describe('signIn', () => { 71 | it('should return token when signIn called', async () => { 72 | sandbox.spy(res, 'json'); 73 | sandbox.spy(res, 'status'); 74 | 75 | try { 76 | await userController.signIn(req, res); 77 | 78 | expect(res.status).to.have.been.calledWith(200); 79 | expect(res.json.callCount).to.equal(1); 80 | } catch (error) { 81 | throw new Error(error); 82 | } 83 | }); 84 | 85 | it('should return fake token using rewire', async () => { 86 | sandbox.spy(res, 'json'); 87 | sandbox.spy(res, 'status'); 88 | 89 | // fake jwt token with rewire 90 | let signToken = userController.__set__('signToken', user => 'fakeToken'); 91 | 92 | try { 93 | await userController.signIn(req, res); 94 | 95 | expect(res.json).to.have.been.calledWith({ 96 | token: 'fakeToken', 97 | }); 98 | signToken(); 99 | } catch (error) { 100 | throw new Error(error); 101 | } 102 | }); 103 | }); 104 | 105 | describe('signUp', () => { 106 | it('should return 403 if the user is already save in the db.', async () => { 107 | sandbox.spy(res, 'json'); 108 | sandbox.spy(res, 'status'); 109 | sandbox.stub(User, 'findOne').returns( 110 | Promise.resolve({ 111 | id: faker.random.number(), 112 | }), 113 | ); 114 | 115 | try { 116 | await userController.signUp(req, res); 117 | 118 | expect(res.status).to.have.been.calledWith(403); 119 | expect(res.json).to.have.been.calledWith({ 120 | error: 'Email is already in use', 121 | }); 122 | } catch (error) { 123 | throw new Error(error); 124 | } 125 | }); 126 | 127 | it('should return 200 if user is not in db and it was saved', async () => { 128 | sandbox.spy(res, 'json'); 129 | sandbox.spy(res, 'status'); 130 | sandbox.stub(User, 'findOne').returns(Promise.resolve(false)); 131 | sandbox.stub(User.prototype, 'save').returns( 132 | Promise.resolve({ 133 | id: faker.random.number(), 134 | }), 135 | ); 136 | 137 | try { 138 | await userController.signUp(req, res); 139 | 140 | expect(res.status).to.have.been.calledWith(200); 141 | expect(res.json.callCount).to.equal(1); 142 | } catch (error) { 143 | throw new Error(error); 144 | } 145 | }); 146 | 147 | it('should return 200 if user is not in db using callback done', async () => { 148 | sandbox.spy(res, 'json'); 149 | sandbox.spy(res, 'status'); 150 | sandbox.stub(User, 'findOne').returns(Promise.resolve(false)); 151 | sandbox.stub(User.prototype, 'save').returns( 152 | Promise.resolve({ 153 | id: faker.random.number(), 154 | }), 155 | ); 156 | 157 | try { 158 | await userController.signUp(req, res); 159 | 160 | expect(res.status).to.have.been.calledWith(200); 161 | expect(res.json.callCount).to.equal(1); 162 | } catch (error) { 163 | throw new Error(error); 164 | } 165 | }); 166 | 167 | it('should return fake token in res.json', async () => { 168 | sandbox.spy(res, 'json'); 169 | sandbox.spy(res, 'status'); 170 | sandbox.stub(User, 'findOne').returns(Promise.resolve(false)); 171 | sandbox.stub(User.prototype, 'save').returns( 172 | Promise.resolve({ 173 | id: faker.random.number(), 174 | }), 175 | ); 176 | 177 | let signToken = userController.__set__('signToken', user => 'fakeTokenNumberTwo'); 178 | 179 | try { 180 | await userController.signUp(req, res); 181 | 182 | expect(res.json).to.have.been.calledWith({ 183 | token: 'fakeTokenNumberTwo', 184 | }); 185 | signToken(); 186 | } catch (error) { 187 | throw new Error(error); 188 | } 189 | }); 190 | }); 191 | 192 | describe('googleOAuth', () => { 193 | it('should return token if user passed the passport google oauth', async () => { 194 | sandbox.spy(res, 'json'); 195 | sandbox.spy(res, 'status'); 196 | 197 | let signToken = userController.__set__('signToken', user => 'fakeTokenFromGoogleController'); 198 | 199 | try { 200 | await userController.googleOAuth(req, res); 201 | 202 | expect(res.status).to.have.been.calledWith(200); 203 | expect(res.json).to.have.been.calledWith({ 204 | token: 'fakeTokenFromGoogleController', 205 | }); 206 | signToken(); 207 | } catch (error) { 208 | throw new Error(error); 209 | } 210 | }); 211 | }); 212 | 213 | describe('facebookOAuth', () => { 214 | it('should return token if user passed the passport facebook oauth', async () => { 215 | sandbox.spy(res, 'json'); 216 | sandbox.spy(res, 'status'); 217 | 218 | let signToken = userController.__set__('signToken', user => 'fakeTokenFromFacebookController'); 219 | 220 | try { 221 | await userController.facebookOAuth(req, res); 222 | 223 | expect(res.status).to.have.been.calledWith(200); 224 | expect(res.json).to.have.been.calledWith({ 225 | token: 'fakeTokenFromFacebookController', 226 | }); 227 | signToken(); 228 | } catch (error) { 229 | throw new Error(error); 230 | } 231 | }); 232 | }); 233 | }); 234 | -------------------------------------------------------------------------------- /__tests__/report/lcov-report/controllers/users.js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Code coverage report for controllers/users.js 5 | 6 | 7 | 8 | 9 | 14 | 15 | 16 |
17 |
18 |

19 | All files / controllers users.js 20 |

21 |
22 |
23 | 100% 24 | Statements 25 | 22/22 26 |
27 |
28 | 100% 29 | Branches 30 | 2/2 31 |
32 |
33 | 100% 34 | Functions 35 | 6/6 36 |
37 |
38 | 100% 39 | Lines 40 | 22/22 41 |
42 |
43 |
44 |
45 |

 46 | 
233 | 
1 47 | 2 48 | 3 49 | 4 50 | 5 51 | 6 52 | 7 53 | 8 54 | 9 55 | 10 56 | 11 57 | 12 58 | 13 59 | 14 60 | 15 61 | 16 62 | 17 63 | 18 64 | 19 65 | 20 66 | 21 67 | 22 68 | 23 69 | 24 70 | 25 71 | 26 72 | 27 73 | 28 74 | 29 75 | 30 76 | 31 77 | 32 78 | 33 79 | 34 80 | 35 81 | 36 82 | 37 83 | 38 84 | 39 85 | 40 86 | 41 87 | 42 88 | 43 89 | 44 90 | 45 91 | 46 92 | 47 93 | 48 94 | 49 95 | 50 96 | 51 97 | 52 98 | 53 99 | 54 100 | 55 101 | 56 102 | 57 103 | 58 104 | 59 105 | 60 106 | 61 107 | 62 108 | 632x 109 | 2x 110 | 2x 111 |   112 | 2x 113 | 6x 114 |   115 |   116 |   117 |   118 |   119 |   120 |   121 | 2x 122 |   123 | 7x 124 |   125 |   126 | 7x 127 | 7x 128 | 2x 129 |   130 |   131 |   132 | 5x 133 |   134 |   135 |   136 |   137 |   138 |   139 |   140 | 5x 141 |   142 |   143 | 5x 144 |   145 | 5x 146 |   147 |   148 |   149 |   150 | 3x 151 | 3x 152 |   153 |   154 |   155 |   156 | 1x 157 | 1x 158 |   159 |   160 |   161 |   162 | 1x 163 | 1x 164 |   165 |   166 |   167 | 2x 168 | 2x 169 |   170 |  
const JWT = require('jsonwebtoken');
171 | const User = require('../models/user');
172 | const { JWT_SECRET } = require('../configuration');
173 |  
174 | signToken = user => {
175 |   return JWT.sign({
176 |     iss: 'CodeWorkr',
177 |     sub: user.id,
178 |     iat: new Date().getTime(), // current time
179 |     exp: new Date().setDate(new Date().getDate() + 1) // current time + 1 day ahead
180 |   }, JWT_SECRET);
181 | }
182 |  
183 | module.exports = {
184 |   signUp: async (req, res, next) => {
185 |     const { email, password } = req.value.body;
186 |  
187 |     // Check if there is a user with the same email
188 |     const foundUser = await User.findOne({ "local.email": email });
189 |     if (foundUser) { 
190 |       return res.status(403).json({ error: 'Email is already in use'});
191 |     }
192 |  
193 |     // Create a new user
194 |     const newUser = new User({ 
195 |       method: 'local',
196 |       local: {
197 |         email: email, 
198 |         password: password
199 |       }
200 |     });
201 |  
202 |     await newUser.save();
203 |  
204 |     // Generate the token
205 |     const token = signToken(newUser);
206 |     // Respond with token
207 |     res.status(200).json({ token });
208 |   },
209 |  
210 |   signIn: async (req, res, next) => {
211 |     // Generate token
212 |     const token = signToken(req.user);
213 |     res.status(200).json({ token });
214 |   },
215 |  
216 |   googleOAuth: async (req, res, next) => {
217 |     // Generate token
218 |     const token = signToken(req.user);
219 |     res.status(200).json({ token });
220 |   },
221 |  
222 |   facebookOAuth: async (req, res, next) => {
223 |     // Generate token
224 |     const token = signToken(req.user);
225 |     res.status(200).json({ token });
226 |   },
227 |  
228 |   secret: async (req, res, next) => {
229 |     console.log('I managed to get here!');
230 |     res.json({ secret: "resource" });
231 |   }
232 | }
234 |
235 |
236 | 240 | 241 | 242 | 249 | 250 | 251 | 252 | -------------------------------------------------------------------------------- /__tests__/report/lcov-report/server/controllers/users.js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Code coverage report for server/controllers/users.js 5 | 6 | 7 | 8 | 9 | 14 | 15 | 16 |
17 |
18 |

19 | All files / server/controllers users.js 20 |

21 |
22 |
23 | 100% 24 | Statements 25 | 22/22 26 |
27 |
28 | 100% 29 | Branches 30 | 2/2 31 |
32 |
33 | 100% 34 | Functions 35 | 6/6 36 |
37 |
38 | 100% 39 | Lines 40 | 22/22 41 |
42 |
43 |
44 |
45 |

 46 | 
233 | 
1 47 | 2 48 | 3 49 | 4 50 | 5 51 | 6 52 | 7 53 | 8 54 | 9 55 | 10 56 | 11 57 | 12 58 | 13 59 | 14 60 | 15 61 | 16 62 | 17 63 | 18 64 | 19 65 | 20 66 | 21 67 | 22 68 | 23 69 | 24 70 | 25 71 | 26 72 | 27 73 | 28 74 | 29 75 | 30 76 | 31 77 | 32 78 | 33 79 | 34 80 | 35 81 | 36 82 | 37 83 | 38 84 | 39 85 | 40 86 | 41 87 | 42 88 | 43 89 | 44 90 | 45 91 | 46 92 | 47 93 | 48 94 | 49 95 | 50 96 | 51 97 | 52 98 | 53 99 | 54 100 | 55 101 | 56 102 | 57 103 | 58 104 | 59 105 | 60 106 | 61 107 | 62 108 | 632x 109 | 2x 110 | 2x 111 |   112 | 2x 113 | 6x 114 |   115 |   116 |   117 |   118 |   119 |   120 |   121 | 2x 122 |   123 | 7x 124 |   125 |   126 | 7x 127 | 7x 128 | 2x 129 |   130 |   131 |   132 | 5x 133 |   134 |   135 |   136 |   137 |   138 |   139 |   140 | 5x 141 |   142 |   143 | 5x 144 |   145 | 5x 146 |   147 |   148 |   149 |   150 | 3x 151 | 3x 152 |   153 |   154 |   155 |   156 | 1x 157 | 1x 158 |   159 |   160 |   161 |   162 | 1x 163 | 1x 164 |   165 |   166 |   167 | 2x 168 | 2x 169 |   170 |  
const JWT = require('jsonwebtoken');
171 | const User = require('../models/user');
172 | const { JWT_SECRET } = require('../configuration');
173 |  
174 | signToken = user => {
175 |   return JWT.sign({
176 |     iss: 'CodeWorkr',
177 |     sub: user.id,
178 |     iat: new Date().getTime(), // current time
179 |     exp: new Date().setDate(new Date().getDate() + 1) // current time + 1 day ahead
180 |   }, JWT_SECRET);
181 | }
182 |  
183 | module.exports = {
184 |   signUp: async (req, res, next) => {
185 |     const { email, password } = req.value.body;
186 |  
187 |     // Check if there is a user with the same email
188 |     const foundUser = await User.findOne({ "local.email": email });
189 |     if (foundUser) { 
190 |       return res.status(403).json({ error: 'Email is already in use'});
191 |     }
192 |  
193 |     // Create a new user
194 |     const newUser = new User({ 
195 |       method: 'local',
196 |       local: {
197 |         email: email, 
198 |         password: password
199 |       }
200 |     });
201 |  
202 |     await newUser.save();
203 |  
204 |     // Generate the token
205 |     const token = signToken(newUser);
206 |     // Respond with token
207 |     res.status(200).json({ token });
208 |   },
209 |  
210 |   signIn: async (req, res, next) => {
211 |     // Generate token
212 |     const token = signToken(req.user);
213 |     res.status(200).json({ token });
214 |   },
215 |  
216 |   googleOAuth: async (req, res, next) => {
217 |     // Generate token
218 |     const token = signToken(req.user);
219 |     res.status(200).json({ token });
220 |   },
221 |  
222 |   facebookOAuth: async (req, res, next) => {
223 |     // Generate token
224 |     const token = signToken(req.user);
225 |     res.status(200).json({ token });
226 |   },
227 |  
228 |   secret: async (req, res, next) => {
229 |     console.log('I managed to get here!');
230 |     res.json({ secret: "resource" });
231 |   }
232 | }
234 |
235 |
236 | 240 | 241 | 242 | 249 | 250 | 251 | 252 | -------------------------------------------------------------------------------- /__tests__/report/lcov-report/server/passport.js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Code coverage report for server/passport.js 5 | 6 | 7 | 8 | 9 | 14 | 15 | 16 |
17 |
18 |

19 | All files / server passport.js 20 |

21 |
22 |
23 | 44.9% 24 | Statements 25 | 22/49 26 |
27 |
28 | 30% 29 | Branches 30 | 3/10 31 |
32 |
33 | 50% 34 | Functions 35 | 2/4 36 |
37 |
38 | 44.9% 39 | Lines 40 | 22/49 41 |
42 |
43 |
44 |
45 |

 46 | 
395 | 
1 47 | 2 48 | 3 49 | 4 50 | 5 51 | 6 52 | 7 53 | 8 54 | 9 55 | 10 56 | 11 57 | 12 58 | 13 59 | 14 60 | 15 61 | 16 62 | 17 63 | 18 64 | 19 65 | 20 66 | 21 67 | 22 68 | 23 69 | 24 70 | 25 71 | 26 72 | 27 73 | 28 74 | 29 75 | 30 76 | 31 77 | 32 78 | 33 79 | 34 80 | 35 81 | 36 82 | 37 83 | 38 84 | 39 85 | 40 86 | 41 87 | 42 88 | 43 89 | 44 90 | 45 91 | 46 92 | 47 93 | 48 94 | 49 95 | 50 96 | 51 97 | 52 98 | 53 99 | 54 100 | 55 101 | 56 102 | 57 103 | 58 104 | 59 105 | 60 106 | 61 107 | 62 108 | 63 109 | 64 110 | 65 111 | 66 112 | 67 113 | 68 114 | 69 115 | 70 116 | 71 117 | 72 118 | 73 119 | 74 120 | 75 121 | 76 122 | 77 123 | 78 124 | 79 125 | 80 126 | 81 127 | 82 128 | 83 129 | 84 130 | 85 131 | 86 132 | 87 133 | 88 134 | 89 135 | 90 136 | 91 137 | 92 138 | 93 139 | 94 140 | 95 141 | 96 142 | 97 143 | 98 144 | 99 145 | 100 146 | 101 147 | 102 148 | 103 149 | 104 150 | 105 151 | 106 152 | 107 153 | 108 154 | 109 155 | 110 156 | 111 157 | 112 158 | 113 159 | 114 160 | 115 161 | 116 162 | 1171x 163 | 1x 164 | 1x 165 | 1x 166 | 1x 167 | 1x 168 | 1x 169 | 1x 170 |   171 |   172 | 1x 173 |   174 |   175 |   176 | 1x 177 |   178 | 1x 179 |   180 |   181 | 1x 182 |   183 |   184 |   185 |   186 | 1x 187 |   188 |   189 |   190 |   191 |   192 |   193 | 1x 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 | 1x 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 | 1x 254 |   255 |   256 | 1x 257 |   258 | 1x 259 |   260 |   261 | 1x 262 |   263 |   264 |   265 |   266 | 1x 267 |   268 |   269 | 1x 270 |   271 |   272 |   273 |   274 | 1x 275 |   276 |   277 |   278 |  
const passport = require('passport');
279 | const JwtStrategy = require('passport-jwt').Strategy;
280 | const { ExtractJwt } = require('passport-jwt');
281 | const LocalStrategy = require('passport-local').Strategy;
282 | const GooglePlusTokenStrategy = require('passport-google-plus-token');
283 | const FacebookTokenStrategy = require('passport-facebook-token');
284 | const config = require('./configuration');
285 | const User = require('./models/user');
286 |  
287 | // JSON WEB TOKENS STRATEGY
288 | passport.use(new JwtStrategy({
289 |   jwtFromRequest: ExtractJwt.fromHeader('authorization'),
290 |   secretOrKey: config.JWT_SECRET
291 | }, async (payload, done) => {
292 |   try {
293 |     // Find the user specified in token
294 |     const user = await User.findById(payload.sub);
295 |  
296 |     // If user doesn't exists, handle it
297 |     Iif (!user) {
298 |       return done(null, false);
299 |     }
300 |  
301 |     // Otherwise, return the user
302 |     done(null, user);
303 |   } catch(error) {
304 |     done(error, false);
305 |   }
306 | }));
307 |  
308 | // Google OAuth Strategy
309 | passport.use('googleToken', new GooglePlusTokenStrategy({
310 |   clientID: config.oauth.google.clientID,
311 |   clientSecret: config.oauth.google.clientSecret
312 | }, async (accessToken, refreshToken, profile, done) => {
313 |   try {
314 |     // Should have full user profile over here
315 |     console.log('profile', profile);
316 |     console.log('accessToken', accessToken);
317 |     console.log('refreshToken', refreshToken);
318 |  
319 |     const existingUser = await User.findOne({ "google.id": profile.id });
320 |     if (existingUser) {
321 |       return done(null, existingUser);
322 |     }
323 |  
324 |     const newUser = new User({
325 |       method: 'google',
326 |       google: {
327 |         id: profile.id,
328 |         email: profile.emails[0].value
329 |       }
330 |     });
331 |  
332 |     await newUser.save();
333 |     done(null, newUser);
334 |   } catch(error) {
335 |     done(error, false, error.message);
336 |   }
337 | }));
338 |  
339 | passport.use('facebookToken', new FacebookTokenStrategy({
340 |   clientID: config.oauth.facebook.clientID,
341 |   clientSecret: config.oauth.facebook.clientSecret
342 | }, async (accessToken, refreshToken, profile, done) => {
343 |   try {
344 |     console.log('profile', profile);
345 |     console.log('accessToken', accessToken);
346 |     console.log('refreshToken', refreshToken);
347 |     
348 |     const existingUser = await User.findOne({ "facebook.id": profile.id });
349 |     if (existingUser) {
350 |       return done(null, existingUser);
351 |     }
352 |  
353 |     const newUser = new User({
354 |       method: 'facebook',
355 |       facebook: {
356 |         id: profile.id,
357 |         email: profile.emails[0].value
358 |       }
359 |     });
360 |  
361 |     await newUser.save();
362 |     done(null, newUser);
363 |   } catch(error) {
364 |     done(error, false, error.message);
365 |   }
366 | }));
367 |  
368 | // LOCAL STRATEGY
369 | passport.use(new LocalStrategy({
370 |   usernameField: 'email'
371 | }, async (email, password, done) => {
372 |   try {
373 |     // Find the user given the email
374 |     const user = await User.findOne({ "local.email": email });
375 |     
376 |     // If not, handle it
377 |     Iif (!user) {
378 |       return done(null, false);
379 |     }
380 |   
381 |     // Check if the password is correct
382 |     const isMatch = await user.isValidPassword(password);
383 |   
384 |     // If not, handle it
385 |     Iif (!isMatch) {
386 |       return done(null, false);
387 |     }
388 |   
389 |     // Otherwise, return the user
390 |     done(null, user);
391 |   } catch(error) {
392 |     done(error, false);
393 |   }
394 | }));
396 |
397 |
398 | 402 | 403 | 404 | 411 | 412 | 413 | 414 | -------------------------------------------------------------------------------- /__tests__/report/lcov-report/prettify.js: -------------------------------------------------------------------------------- 1 | window.PR_SHOULD_USE_CONTINUATION=true;(function(){var h=["break,continue,do,else,for,if,return,while"];var u=[h,"auto,case,char,const,default,double,enum,extern,float,goto,int,long,register,short,signed,sizeof,static,struct,switch,typedef,union,unsigned,void,volatile"];var p=[u,"catch,class,delete,false,import,new,operator,private,protected,public,this,throw,true,try,typeof"];var l=[p,"alignof,align_union,asm,axiom,bool,concept,concept_map,const_cast,constexpr,decltype,dynamic_cast,explicit,export,friend,inline,late_check,mutable,namespace,nullptr,reinterpret_cast,static_assert,static_cast,template,typeid,typename,using,virtual,where"];var x=[p,"abstract,boolean,byte,extends,final,finally,implements,import,instanceof,null,native,package,strictfp,super,synchronized,throws,transient"];var R=[x,"as,base,by,checked,decimal,delegate,descending,dynamic,event,fixed,foreach,from,group,implicit,in,interface,internal,into,is,lock,object,out,override,orderby,params,partial,readonly,ref,sbyte,sealed,stackalloc,string,select,uint,ulong,unchecked,unsafe,ushort,var"];var r="all,and,by,catch,class,else,extends,false,finally,for,if,in,is,isnt,loop,new,no,not,null,of,off,on,or,return,super,then,true,try,unless,until,when,while,yes";var w=[p,"debugger,eval,export,function,get,null,set,undefined,var,with,Infinity,NaN"];var s="caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END";var I=[h,"and,as,assert,class,def,del,elif,except,exec,finally,from,global,import,in,is,lambda,nonlocal,not,or,pass,print,raise,try,with,yield,False,True,None"];var f=[h,"alias,and,begin,case,class,def,defined,elsif,end,ensure,false,in,module,next,nil,not,or,redo,rescue,retry,self,super,then,true,undef,unless,until,when,yield,BEGIN,END"];var H=[h,"case,done,elif,esac,eval,fi,function,in,local,set,then,until"];var A=[l,R,w,s+I,f,H];var e=/^(DIR|FILE|vector|(de|priority_)?queue|list|stack|(const_)?iterator|(multi)?(set|map)|bitset|u?(int|float)\d*)/;var C="str";var z="kwd";var j="com";var O="typ";var G="lit";var L="pun";var F="pln";var m="tag";var E="dec";var J="src";var P="atn";var n="atv";var N="nocode";var M="(?:^^\\.?|[+-]|\\!|\\!=|\\!==|\\#|\\%|\\%=|&|&&|&&=|&=|\\(|\\*|\\*=|\\+=|\\,|\\-=|\\->|\\/|\\/=|:|::|\\;|<|<<|<<=|<=|=|==|===|>|>=|>>|>>=|>>>|>>>=|\\?|\\@|\\[|\\^|\\^=|\\^\\^|\\^\\^=|\\{|\\||\\|=|\\|\\||\\|\\|=|\\~|break|case|continue|delete|do|else|finally|instanceof|return|throw|try|typeof)\\s*";function k(Z){var ad=0;var S=false;var ac=false;for(var V=0,U=Z.length;V122)){if(!(al<65||ag>90)){af.push([Math.max(65,ag)|32,Math.min(al,90)|32])}if(!(al<97||ag>122)){af.push([Math.max(97,ag)&~32,Math.min(al,122)&~32])}}}}af.sort(function(av,au){return(av[0]-au[0])||(au[1]-av[1])});var ai=[];var ap=[NaN,NaN];for(var ar=0;arat[0]){if(at[1]+1>at[0]){an.push("-")}an.push(T(at[1]))}}an.push("]");return an.join("")}function W(al){var aj=al.source.match(new RegExp("(?:\\[(?:[^\\x5C\\x5D]|\\\\[\\s\\S])*\\]|\\\\u[A-Fa-f0-9]{4}|\\\\x[A-Fa-f0-9]{2}|\\\\[0-9]+|\\\\[^ux0-9]|\\(\\?[:!=]|[\\(\\)\\^]|[^\\x5B\\x5C\\(\\)\\^]+)","g"));var ah=aj.length;var an=[];for(var ak=0,am=0;ak=2&&ai==="["){aj[ak]=X(ag)}else{if(ai!=="\\"){aj[ak]=ag.replace(/[a-zA-Z]/g,function(ao){var ap=ao.charCodeAt(0);return"["+String.fromCharCode(ap&~32,ap|32)+"]"})}}}}return aj.join("")}var aa=[];for(var V=0,U=Z.length;V=0;){S[ac.charAt(ae)]=Y}}var af=Y[1];var aa=""+af;if(!ag.hasOwnProperty(aa)){ah.push(af);ag[aa]=null}}ah.push(/[\0-\uffff]/);V=k(ah)})();var X=T.length;var W=function(ah){var Z=ah.sourceCode,Y=ah.basePos;var ad=[Y,F];var af=0;var an=Z.match(V)||[];var aj={};for(var ae=0,aq=an.length;ae=5&&"lang-"===ap.substring(0,5);if(am&&!(ai&&typeof ai[1]==="string")){am=false;ap=J}if(!am){aj[ag]=ap}}var ab=af;af+=ag.length;if(!am){ad.push(Y+ab,ap)}else{var al=ai[1];var ak=ag.indexOf(al);var ac=ak+al.length;if(ai[2]){ac=ag.length-ai[2].length;ak=ac-al.length}var ar=ap.substring(5);B(Y+ab,ag.substring(0,ak),W,ad);B(Y+ab+ak,al,q(ar,al),ad);B(Y+ab+ac,ag.substring(ac),W,ad)}}ah.decorations=ad};return W}function i(T){var W=[],S=[];if(T.tripleQuotedStrings){W.push([C,/^(?:\'\'\'(?:[^\'\\]|\\[\s\S]|\'{1,2}(?=[^\']))*(?:\'\'\'|$)|\"\"\"(?:[^\"\\]|\\[\s\S]|\"{1,2}(?=[^\"]))*(?:\"\"\"|$)|\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$))/,null,"'\""])}else{if(T.multiLineStrings){W.push([C,/^(?:\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$)|\`(?:[^\\\`]|\\[\s\S])*(?:\`|$))/,null,"'\"`"])}else{W.push([C,/^(?:\'(?:[^\\\'\r\n]|\\.)*(?:\'|$)|\"(?:[^\\\"\r\n]|\\.)*(?:\"|$))/,null,"\"'"])}}if(T.verbatimStrings){S.push([C,/^@\"(?:[^\"]|\"\")*(?:\"|$)/,null])}var Y=T.hashComments;if(Y){if(T.cStyleComments){if(Y>1){W.push([j,/^#(?:##(?:[^#]|#(?!##))*(?:###|$)|.*)/,null,"#"])}else{W.push([j,/^#(?:(?:define|elif|else|endif|error|ifdef|include|ifndef|line|pragma|undef|warning)\b|[^\r\n]*)/,null,"#"])}S.push([C,/^<(?:(?:(?:\.\.\/)*|\/?)(?:[\w-]+(?:\/[\w-]+)+)?[\w-]+\.h|[a-z]\w*)>/,null])}else{W.push([j,/^#[^\r\n]*/,null,"#"])}}if(T.cStyleComments){S.push([j,/^\/\/[^\r\n]*/,null]);S.push([j,/^\/\*[\s\S]*?(?:\*\/|$)/,null])}if(T.regexLiterals){var X=("/(?=[^/*])(?:[^/\\x5B\\x5C]|\\x5C[\\s\\S]|\\x5B(?:[^\\x5C\\x5D]|\\x5C[\\s\\S])*(?:\\x5D|$))+/");S.push(["lang-regex",new RegExp("^"+M+"("+X+")")])}var V=T.types;if(V){S.push([O,V])}var U=(""+T.keywords).replace(/^ | $/g,"");if(U.length){S.push([z,new RegExp("^(?:"+U.replace(/[\s,]+/g,"|")+")\\b"),null])}W.push([F,/^\s+/,null," \r\n\t\xA0"]);S.push([G,/^@[a-z_$][a-z_$@0-9]*/i,null],[O,/^(?:[@_]?[A-Z]+[a-z][A-Za-z_$@0-9]*|\w+_t\b)/,null],[F,/^[a-z_$][a-z_$@0-9]*/i,null],[G,new RegExp("^(?:0x[a-f0-9]+|(?:\\d(?:_\\d+)*\\d*(?:\\.\\d*)?|\\.\\d\\+)(?:e[+\\-]?\\d+)?)[a-z]*","i"),null,"0123456789"],[F,/^\\[\s\S]?/,null],[L,/^.[^\s\w\.$@\'\"\`\/\#\\]*/,null]);return g(W,S)}var K=i({keywords:A,hashComments:true,cStyleComments:true,multiLineStrings:true,regexLiterals:true});function Q(V,ag){var U=/(?:^|\s)nocode(?:\s|$)/;var ab=/\r\n?|\n/;var ac=V.ownerDocument;var S;if(V.currentStyle){S=V.currentStyle.whiteSpace}else{if(window.getComputedStyle){S=ac.defaultView.getComputedStyle(V,null).getPropertyValue("white-space")}}var Z=S&&"pre"===S.substring(0,3);var af=ac.createElement("LI");while(V.firstChild){af.appendChild(V.firstChild)}var W=[af];function ae(al){switch(al.nodeType){case 1:if(U.test(al.className)){break}if("BR"===al.nodeName){ad(al);if(al.parentNode){al.parentNode.removeChild(al)}}else{for(var an=al.firstChild;an;an=an.nextSibling){ae(an)}}break;case 3:case 4:if(Z){var am=al.nodeValue;var aj=am.match(ab);if(aj){var ai=am.substring(0,aj.index);al.nodeValue=ai;var ah=am.substring(aj.index+aj[0].length);if(ah){var ak=al.parentNode;ak.insertBefore(ac.createTextNode(ah),al.nextSibling)}ad(al);if(!ai){al.parentNode.removeChild(al)}}}break}}function ad(ak){while(!ak.nextSibling){ak=ak.parentNode;if(!ak){return}}function ai(al,ar){var aq=ar?al.cloneNode(false):al;var ao=al.parentNode;if(ao){var ap=ai(ao,1);var an=al.nextSibling;ap.appendChild(aq);for(var am=an;am;am=an){an=am.nextSibling;ap.appendChild(am)}}return aq}var ah=ai(ak.nextSibling,0);for(var aj;(aj=ah.parentNode)&&aj.nodeType===1;){ah=aj}W.push(ah)}for(var Y=0;Y=S){ah+=2}if(V>=ap){Z+=2}}}var t={};function c(U,V){for(var S=V.length;--S>=0;){var T=V[S];if(!t.hasOwnProperty(T)){t[T]=U}else{if(window.console){console.warn("cannot override language handler %s",T)}}}}function q(T,S){if(!(T&&t.hasOwnProperty(T))){T=/^\s*]*(?:>|$)/],[j,/^<\!--[\s\S]*?(?:-\->|$)/],["lang-",/^<\?([\s\S]+?)(?:\?>|$)/],["lang-",/^<%([\s\S]+?)(?:%>|$)/],[L,/^(?:<[%?]|[%?]>)/],["lang-",/^]*>([\s\S]+?)<\/xmp\b[^>]*>/i],["lang-js",/^]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\s\S]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i]]),["default-markup","htm","html","mxml","xhtml","xml","xsl"]);c(g([[F,/^[\s]+/,null," \t\r\n"],[n,/^(?:\"[^\"]*\"?|\'[^\']*\'?)/,null,"\"'"]],[[m,/^^<\/?[a-z](?:[\w.:-]*\w)?|\/?>$/i],[P,/^(?!style[\s=]|on)[a-z](?:[\w:-]*\w)?/i],["lang-uq.val",/^=\s*([^>\'\"\s]*(?:[^>\'\"\s\/]|\/(?=\s)))/],[L,/^[=<>\/]+/],["lang-js",/^on\w+\s*=\s*\"([^\"]+)\"/i],["lang-js",/^on\w+\s*=\s*\'([^\']+)\'/i],["lang-js",/^on\w+\s*=\s*([^\"\'>\s]+)/i],["lang-css",/^style\s*=\s*\"([^\"]+)\"/i],["lang-css",/^style\s*=\s*\'([^\']+)\'/i],["lang-css",/^style\s*=\s*([^\"\'>\s]+)/i]]),["in.tag"]);c(g([],[[n,/^[\s\S]+/]]),["uq.val"]);c(i({keywords:l,hashComments:true,cStyleComments:true,types:e}),["c","cc","cpp","cxx","cyc","m"]);c(i({keywords:"null,true,false"}),["json"]);c(i({keywords:R,hashComments:true,cStyleComments:true,verbatimStrings:true,types:e}),["cs"]);c(i({keywords:x,cStyleComments:true}),["java"]);c(i({keywords:H,hashComments:true,multiLineStrings:true}),["bsh","csh","sh"]);c(i({keywords:I,hashComments:true,multiLineStrings:true,tripleQuotedStrings:true}),["cv","py"]);c(i({keywords:s,hashComments:true,multiLineStrings:true,regexLiterals:true}),["perl","pl","pm"]);c(i({keywords:f,hashComments:true,multiLineStrings:true,regexLiterals:true}),["rb"]);c(i({keywords:w,cStyleComments:true,regexLiterals:true}),["js"]);c(i({keywords:r,hashComments:3,cStyleComments:true,multilineStrings:true,tripleQuotedStrings:true,regexLiterals:true}),["coffee"]);c(g([],[[C,/^[\s\S]+/]]),["regex"]);function d(V){var U=V.langExtension;try{var S=a(V.sourceNode);var T=S.sourceCode;V.sourceCode=T;V.spans=S.spans;V.basePos=0;q(U,T)(V);D(V)}catch(W){if("console" in window){console.log(W&&W.stack?W.stack:W)}}}function y(W,V,U){var S=document.createElement("PRE");S.innerHTML=W;if(U){Q(S,U)}var T={langExtension:V,numberLines:U,sourceNode:S};d(T);return S.innerHTML}function b(ad){function Y(af){return document.getElementsByTagName(af)}var ac=[Y("pre"),Y("code"),Y("xmp")];var T=[];for(var aa=0;aa=0){var ah=ai.match(ab);var am;if(!ah&&(am=o(aj))&&"CODE"===am.tagName){ah=am.className.match(ab)}if(ah){ah=ah[1]}var al=false;for(var ak=aj.parentNode;ak;ak=ak.parentNode){if((ak.tagName==="pre"||ak.tagName==="code"||ak.tagName==="xmp")&&ak.className&&ak.className.indexOf("prettyprint")>=0){al=true;break}}if(!al){var af=aj.className.match(/\blinenums\b(?::(\d+))?/);af=af?af[1]&&af[1].length?+af[1]:true:false;if(af){Q(aj,af)}S={langExtension:ah,sourceNode:aj,numberLines:af};d(S)}}}if(X]*(?:>|$)/],[PR.PR_COMMENT,/^<\!--[\s\S]*?(?:-\->|$)/],[PR.PR_PUNCTUATION,/^(?:<[%?]|[%?]>)/],["lang-",/^<\?([\s\S]+?)(?:\?>|$)/],["lang-",/^<%([\s\S]+?)(?:%>|$)/],["lang-",/^]*>([\s\S]+?)<\/xmp\b[^>]*>/i],["lang-handlebars",/^]*type\s*=\s*['"]?text\/x-handlebars-template['"]?\b[^>]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-js",/^]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\s\S]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i],[PR.PR_DECLARATION,/^{{[#^>/]?\s*[\w.][^}]*}}/],[PR.PR_DECLARATION,/^{{&?\s*[\w.][^}]*}}/],[PR.PR_DECLARATION,/^{{{>?\s*[\w.][^}]*}}}/],[PR.PR_COMMENT,/^{{![^}]*}}/]]),["handlebars","hbs"]);PR.registerLangHandler(PR.createSimpleLexer([[PR.PR_PLAIN,/^[ \t\r\n\f]+/,null," \t\r\n\f"]],[[PR.PR_STRING,/^\"(?:[^\n\r\f\\\"]|\\(?:\r\n?|\n|\f)|\\[\s\S])*\"/,null],[PR.PR_STRING,/^\'(?:[^\n\r\f\\\']|\\(?:\r\n?|\n|\f)|\\[\s\S])*\'/,null],["lang-css-str",/^url\(([^\)\"\']*)\)/i],[PR.PR_KEYWORD,/^(?:url|rgb|\!important|@import|@page|@media|@charset|inherit)(?=[^\-\w]|$)/i,null],["lang-css-kw",/^(-?(?:[_a-z]|(?:\\[0-9a-f]+ ?))(?:[_a-z0-9\-]|\\(?:\\[0-9a-f]+ ?))*)\s*:/i],[PR.PR_COMMENT,/^\/\*[^*]*\*+(?:[^\/*][^*]*\*+)*\//],[PR.PR_COMMENT,/^(?:)/],[PR.PR_LITERAL,/^(?:\d+|\d*\.\d+)(?:%|[a-z]+)?/i],[PR.PR_LITERAL,/^#(?:[0-9a-f]{3}){1,2}/i],[PR.PR_PLAIN,/^-?(?:[_a-z]|(?:\\[\da-f]+ ?))(?:[_a-z\d\-]|\\(?:\\[\da-f]+ ?))*/i],[PR.PR_PUNCTUATION,/^[^\s\w\'\"]+/]]),["css"]);PR.registerLangHandler(PR.createSimpleLexer([],[[PR.PR_KEYWORD,/^-?(?:[_a-z]|(?:\\[\da-f]+ ?))(?:[_a-z\d\-]|\\(?:\\[\da-f]+ ?))*/i]]),["css-kw"]);PR.registerLangHandler(PR.createSimpleLexer([],[[PR.PR_STRING,/^[^\)\"\']+/]]),["css-str"]); 2 | --------------------------------------------------------------------------------