├── .gitignore ├── .sample-env ├── LICENSE.md ├── README.md ├── config ├── env.js ├── jest │ ├── cssTransform.js │ └── fileTransform.js ├── paths.js ├── polyfills.js ├── webpack.config.dev.js ├── webpack.config.prod.js └── webpackDevServer.config.js ├── doc └── img │ └── crud.gif ├── env-setup.js ├── package-lock.json ├── package.json ├── postcss.config.js ├── public ├── favicon.ico ├── index.html └── manifest.json ├── scripts ├── build.js ├── start.js └── test.js ├── server ├── components │ └── users │ │ ├── UsersSchema.js │ │ ├── users.test.js │ │ ├── usersAPI.js │ │ ├── usersController.js │ │ └── usersPassport.js ├── index.js └── util │ └── index.js └── src ├── App.js ├── components └── Root │ ├── AppBar │ ├── index.js │ └── style.scss │ ├── Main │ └── index.js │ ├── SideNav │ ├── components │ │ └── NavigationList │ │ │ ├── index.js │ │ │ └── style.scss │ └── index.js │ └── index.js ├── index.js ├── index.scss ├── registerServiceWorker.js ├── routes.js ├── scenes ├── Home │ └── index.js ├── Login │ └── index.js ├── Profile │ └── index.js ├── Register │ └── index.js ├── Users │ └── index.js └── components │ ├── PrivateRoute.js │ └── hoc │ └── asyncComponent.js └── services ├── axios.js ├── errors ├── actionTypes.js └── reducer.js ├── reducers.js ├── store.js └── users ├── actionTypes.js ├── actions.js └── reducer.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next -------------------------------------------------------------------------------- /.sample-env: -------------------------------------------------------------------------------- 1 | API_PORT=8001 2 | 3 | CORS_WHITELIST_DOMAINS="*" 4 | 5 | SESSION_EXPIRES_IN=1h 6 | 7 | DATABASE= 8 | 9 | AUTH_SECRET_OR_KEY= 10 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2018 Jefferson Ribeiro 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Aplicação fullstack utilizando MongoDB, NodeJs, React e Redux 2 |

3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 |

11 | 12 | #### Backend features 13 | O banco de dados Mongo foi hospedado no MLab por conveniência. 14 | Foi implementado gravatar para foto de perfil, se o email do usuário tiver um avatar 15 | no wordpress ela automaticamente é salva no banco. 16 | Rotas sensíveis foram protegidas por meio de JWT em conjunto com passport. 17 | 18 | 19 | #### Usando 20 | - Nodejs 21 | * Express 22 | * Nodemon - Para restartar o server sempre que houver uma alteração 23 | * Passport - Para proteger rotas privadas 24 | * Jwt - Para proteger rotas privadas 25 | * Bcrypt - Para Cryptografar as senhas de usuários antes de salvar no banco 26 | - MongoDB 27 | * Mongoose 28 | 29 | #### Frontend features 30 | Foi usado React em conjunto com Redux e React-router para construir a SPA. 31 | Rotas protegidas redirecionam para a home e só são acessíveis por meio de auth. 32 | Localstorage foi utilizado para persistir o state de usuário no recarregamento das páginas. 33 | 34 | #### Usando 35 | - React 36 | * Redux - Para gerenciar o state da aplicação 37 | * asyncRoutes - As rotas carregam em chunks, dessa forma evita da aplicação ficar pesada num primeiro carregamento 38 | - Axios - Para fazer as requisições HTTP 39 | - Local storage nativo - Para persistir o state e o auth nas rotas privadas 40 | - MaterialUI components 41 | 42 | ## Como iniciar a aplicação 43 | 44 | #### Requerimentos 45 | 46 | - Node.js 47 | - NPM 48 | 49 | ### Instalando os pacotes 50 | 51 | Execute o comando abaixo para instalar as dependências: 52 | ``` bash 53 | npm install 54 | ``` 55 | 56 | ### Iniciando o servidor 57 | 58 | Execute o comando abaixo para iniciar o Nodejs e conectar ao banco de dados MongoDB: 59 | ``` bash 60 | npm run server 61 | ``` 62 | 63 | Aguarde a execução e a API estará rodando na Url `http://localhost:8001/api/` 64 | 65 | Os endpoints disponíveis são: 66 | - Post - Login [more](https://documenter.getpostman.com/view/4374482/teste-fullstack/RW87p9Mq#0e46cf7d-edf9-416c-bfab-84022d8a346e) 67 | - Post - Register [more](https://documenter.getpostman.com/view/4374482/teste-fullstack/RW87p9Mq#db625518-ec7d-41c7-9894-189322033ac6) 68 | - Put - Update Profile [more](https://documenter.getpostman.com/view/4374482/teste-fullstack/RW87p9Mq#ee34ae20-fe46-46f5-8666-7ed784448d65) 69 | - Del - Delete Account [more](https://documenter.getpostman.com/view/4374482/teste-fullstack/RW87p9Mq#1481a07f-160a-4b9c-ba95-7ceb20266b53) 70 | - Get - List Users [more](https://documenter.getpostman.com/view/4374482/teste-fullstack/RW87p9Mq#5f812e40-7bf1-47e8-87bb-1390b2fdf70b) 71 | 72 | [A documentação completa pode ser encontrada no Postman](https://documenter.getpostman.com/view/4374482/teste-fullstack/RW87p9Mq) 73 | 74 | Deixe o servidor rodando em um terminal, abra outro e siga para o próximo passo: 75 | 76 | ### Iniciando a SPA React 77 | 78 | Para isso basta executar o comando abaixo, e pronto! :D 79 | ``` bash 80 | npm start 81 | ``` 82 | 83 | A aplicação irá iniciar automaticamente no browser na Url `http://localhost:3000` 84 | 85 |
86 | 87 | ### Copyright and license 88 | The MIT License (MIT). Please see License File for more information. 89 | 90 |
91 |
92 | 93 |

94 |

95 | A little project by Jefferson Ribeiro 96 |

97 | 98 | -------------------------------------------------------------------------------- /config/env.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const paths = require('./paths'); 6 | 7 | // Make sure that including paths.js after env.js will read .env variables. 8 | delete require.cache[require.resolve('./paths')]; 9 | 10 | const NODE_ENV = process.env.NODE_ENV; 11 | if (!NODE_ENV) { 12 | throw new Error( 13 | 'The NODE_ENV environment variable is required but was not specified.' 14 | ); 15 | } 16 | 17 | // https://github.com/bkeepers/dotenv#what-other-env-files-can-i-use 18 | var dotenvFiles = [ 19 | `${paths.dotenv}.${NODE_ENV}.local`, 20 | `${paths.dotenv}.${NODE_ENV}`, 21 | // Don't include `.env.local` for `test` environment 22 | // since normally you expect tests to produce the same 23 | // results for everyone 24 | NODE_ENV !== 'test' && `${paths.dotenv}.local`, 25 | paths.dotenv, 26 | ].filter(Boolean); 27 | 28 | // Load environment variables from .env* files. Suppress warnings using silent 29 | // if this file is missing. dotenv will never modify any environment variables 30 | // that have already been set. Variable expansion is supported in .env files. 31 | // https://github.com/motdotla/dotenv 32 | // https://github.com/motdotla/dotenv-expand 33 | dotenvFiles.forEach(dotenvFile => { 34 | if (fs.existsSync(dotenvFile)) { 35 | require('dotenv-expand')( 36 | require('dotenv').config({ 37 | path: dotenvFile, 38 | }) 39 | ); 40 | } 41 | }); 42 | 43 | // We support resolving modules according to `NODE_PATH`. 44 | // This lets you use absolute paths in imports inside large monorepos: 45 | // https://github.com/facebookincubator/create-react-app/issues/253. 46 | // It works similar to `NODE_PATH` in Node itself: 47 | // https://nodejs.org/api/modules.html#modules_loading_from_the_global_folders 48 | // Note that unlike in Node, only *relative* paths from `NODE_PATH` are honored. 49 | // Otherwise, we risk importing Node.js core modules into an app instead of Webpack shims. 50 | // https://github.com/facebookincubator/create-react-app/issues/1023#issuecomment-265344421 51 | // We also resolve them to make sure all tools using them work consistently. 52 | const appDirectory = fs.realpathSync(process.cwd()); 53 | process.env.NODE_PATH = (process.env.NODE_PATH || '') 54 | .split(path.delimiter) 55 | .filter(folder => folder && !path.isAbsolute(folder)) 56 | .map(folder => path.resolve(appDirectory, folder)) 57 | .join(path.delimiter); 58 | 59 | // Grab NODE_ENV and REACT_APP_* environment variables and prepare them to be 60 | // injected into the application via DefinePlugin in Webpack configuration. 61 | const REACT_APP = /^REACT_APP_/i; 62 | 63 | function getClientEnvironment(publicUrl) { 64 | const raw = Object.keys(process.env) 65 | .filter(key => REACT_APP.test(key)) 66 | .reduce( 67 | (env, key) => { 68 | env[key] = process.env[key]; 69 | return env; 70 | }, 71 | { 72 | // Useful for determining whether we’re running in production mode. 73 | // Most importantly, it switches React into the correct mode. 74 | NODE_ENV: process.env.NODE_ENV || 'development', 75 | // Useful for resolving the correct path to static assets in `public`. 76 | // For example, . 77 | // This should only be used as an escape hatch. Normally you would put 78 | // images into the `src` and `import` them in code to get their paths. 79 | PUBLIC_URL: publicUrl, 80 | } 81 | ); 82 | // Stringify all values so we can feed into Webpack DefinePlugin 83 | const stringified = { 84 | 'process.env': Object.keys(raw).reduce((env, key) => { 85 | env[key] = JSON.stringify(raw[key]); 86 | return env; 87 | }, {}), 88 | }; 89 | 90 | return { raw, stringified }; 91 | } 92 | 93 | module.exports = getClientEnvironment; 94 | -------------------------------------------------------------------------------- /config/jest/cssTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // This is a custom Jest transformer turning style imports into empty objects. 4 | // http://facebook.github.io/jest/docs/en/webpack.html 5 | 6 | module.exports = { 7 | process() { 8 | return 'module.exports = {};'; 9 | }, 10 | getCacheKey() { 11 | // The output is always the same. 12 | return 'cssTransform'; 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /config/jest/fileTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | 5 | // This is a custom Jest transformer turning file imports into filenames. 6 | // http://facebook.github.io/jest/docs/en/webpack.html 7 | 8 | module.exports = { 9 | process(src, filename) { 10 | return `module.exports = ${JSON.stringify(path.basename(filename))};`; 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /config/paths.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const fs = require('fs'); 5 | const url = require('url'); 6 | 7 | // Make sure any symlinks in the project folder are resolved: 8 | // https://github.com/facebookincubator/create-react-app/issues/637 9 | const appDirectory = fs.realpathSync(process.cwd()); 10 | const resolveApp = relativePath => path.resolve(appDirectory, relativePath); 11 | 12 | const envPublicUrl = process.env.PUBLIC_URL; 13 | 14 | function ensureSlash(path, needsSlash) { 15 | const hasSlash = path.endsWith('/'); 16 | if (hasSlash && !needsSlash) { 17 | return path.substr(path, path.length - 1); 18 | } else if (!hasSlash && needsSlash) { 19 | return `${path}/`; 20 | } else { 21 | return path; 22 | } 23 | } 24 | 25 | const getPublicUrl = appPackageJson => 26 | envPublicUrl || require(appPackageJson).homepage; 27 | 28 | // We use `PUBLIC_URL` environment variable or "homepage" field to infer 29 | // "public path" at which the app is served. 30 | // Webpack needs to know it to put the right 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /scripts/build.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Do this as the first thing so that any code reading it knows the right env. 4 | process.env.BABEL_ENV = 'production'; 5 | process.env.NODE_ENV = 'production'; 6 | 7 | // Makes the script crash on unhandled rejections instead of silently 8 | // ignoring them. In the future, promise rejections that are not handled will 9 | // terminate the Node.js process with a non-zero exit code. 10 | process.on('unhandledRejection', err => { 11 | throw err; 12 | }); 13 | 14 | // Ensure environment variables are read. 15 | require('../config/env'); 16 | 17 | const path = require('path'); 18 | const chalk = require('chalk'); 19 | const fs = require('fs-extra'); 20 | const webpack = require('webpack'); 21 | const config = require('../config/webpack.config.prod'); 22 | const paths = require('../config/paths'); 23 | const checkRequiredFiles = require('react-dev-utils/checkRequiredFiles'); 24 | const formatWebpackMessages = require('react-dev-utils/formatWebpackMessages'); 25 | const printHostingInstructions = require('react-dev-utils/printHostingInstructions'); 26 | const FileSizeReporter = require('react-dev-utils/FileSizeReporter'); 27 | const printBuildError = require('react-dev-utils/printBuildError'); 28 | 29 | const measureFileSizesBeforeBuild = 30 | FileSizeReporter.measureFileSizesBeforeBuild; 31 | const printFileSizesAfterBuild = FileSizeReporter.printFileSizesAfterBuild; 32 | const useYarn = fs.existsSync(paths.yarnLockFile); 33 | 34 | // These sizes are pretty large. We'll warn for bundles exceeding them. 35 | const WARN_AFTER_BUNDLE_GZIP_SIZE = 512 * 1024; 36 | const WARN_AFTER_CHUNK_GZIP_SIZE = 1024 * 1024; 37 | 38 | // Warn and crash if required files are missing 39 | if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) { 40 | process.exit(1); 41 | } 42 | 43 | // First, read the current file sizes in build directory. 44 | // This lets us display how much they changed later. 45 | measureFileSizesBeforeBuild(paths.appBuild) 46 | .then(previousFileSizes => { 47 | // Remove all content but keep the directory so that 48 | // if you're in it, you don't end up in Trash 49 | fs.emptyDirSync(paths.appBuild); 50 | // Merge with the public folder 51 | copyPublicFolder(); 52 | // Start the webpack build 53 | return build(previousFileSizes); 54 | }) 55 | .then( 56 | ({ stats, previousFileSizes, warnings }) => { 57 | if (warnings.length) { 58 | console.log(chalk.yellow('Compiled with warnings.\n')); 59 | console.log(warnings.join('\n\n')); 60 | console.log( 61 | '\nSearch for the ' + 62 | chalk.underline(chalk.yellow('keywords')) + 63 | ' to learn more about each warning.' 64 | ); 65 | console.log( 66 | 'To ignore, add ' + 67 | chalk.cyan('// eslint-disable-next-line') + 68 | ' to the line before.\n' 69 | ); 70 | } else { 71 | console.log(chalk.green('Compiled successfully.\n')); 72 | } 73 | 74 | console.log('File sizes after gzip:\n'); 75 | printFileSizesAfterBuild( 76 | stats, 77 | previousFileSizes, 78 | paths.appBuild, 79 | WARN_AFTER_BUNDLE_GZIP_SIZE, 80 | WARN_AFTER_CHUNK_GZIP_SIZE 81 | ); 82 | console.log(); 83 | 84 | const appPackage = require(paths.appPackageJson); 85 | const publicUrl = paths.publicUrl; 86 | const publicPath = config.output.publicPath; 87 | const buildFolder = path.relative(process.cwd(), paths.appBuild); 88 | printHostingInstructions( 89 | appPackage, 90 | publicUrl, 91 | publicPath, 92 | buildFolder, 93 | useYarn 94 | ); 95 | }, 96 | err => { 97 | console.log(chalk.red('Failed to compile.\n')); 98 | printBuildError(err); 99 | process.exit(1); 100 | } 101 | ); 102 | 103 | // Create the production build and print the deployment instructions. 104 | function build(previousFileSizes) { 105 | console.log('Creating an optimized production build...'); 106 | 107 | let compiler = webpack(config); 108 | return new Promise((resolve, reject) => { 109 | compiler.run((err, stats) => { 110 | if (err) { 111 | return reject(err); 112 | } 113 | const messages = formatWebpackMessages(stats.toJson({}, true)); 114 | if (messages.errors.length) { 115 | // Only keep the first error. Others are often indicative 116 | // of the same problem, but confuse the reader with noise. 117 | if (messages.errors.length > 1) { 118 | messages.errors.length = 1; 119 | } 120 | return reject(new Error(messages.errors.join('\n\n'))); 121 | } 122 | if ( 123 | process.env.CI && 124 | (typeof process.env.CI !== 'string' || 125 | process.env.CI.toLowerCase() !== 'false') && 126 | messages.warnings.length 127 | ) { 128 | console.log( 129 | chalk.yellow( 130 | '\nTreating warnings as errors because process.env.CI = true.\n' + 131 | 'Most CI servers set it automatically.\n' 132 | ) 133 | ); 134 | return reject(new Error(messages.warnings.join('\n\n'))); 135 | } 136 | return resolve({ 137 | stats, 138 | previousFileSizes, 139 | warnings: messages.warnings, 140 | }); 141 | }); 142 | }); 143 | } 144 | 145 | function copyPublicFolder() { 146 | fs.copySync(paths.appPublic, paths.appBuild, { 147 | dereference: true, 148 | filter: file => file !== paths.appHtml, 149 | }); 150 | } 151 | -------------------------------------------------------------------------------- /scripts/start.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Do this as the first thing so that any code reading it knows the right env. 4 | process.env.BABEL_ENV = 'development'; 5 | process.env.NODE_ENV = 'development'; 6 | 7 | // Makes the script crash on unhandled rejections instead of silently 8 | // ignoring them. In the future, promise rejections that are not handled will 9 | // terminate the Node.js process with a non-zero exit code. 10 | process.on('unhandledRejection', err => { 11 | throw err; 12 | }); 13 | 14 | // Ensure environment variables are read. 15 | require('../config/env'); 16 | 17 | const fs = require('fs'); 18 | const chalk = require('chalk'); 19 | const webpack = require('webpack'); 20 | const WebpackDevServer = require('webpack-dev-server'); 21 | const clearConsole = require('react-dev-utils/clearConsole'); 22 | const checkRequiredFiles = require('react-dev-utils/checkRequiredFiles'); 23 | const { 24 | choosePort, 25 | createCompiler, 26 | prepareProxy, 27 | prepareUrls, 28 | } = require('react-dev-utils/WebpackDevServerUtils'); 29 | const openBrowser = require('react-dev-utils/openBrowser'); 30 | const paths = require('../config/paths'); 31 | const config = require('../config/webpack.config.dev'); 32 | const createDevServerConfig = require('../config/webpackDevServer.config'); 33 | 34 | const useYarn = fs.existsSync(paths.yarnLockFile); 35 | const isInteractive = process.stdout.isTTY; 36 | 37 | // Warn and crash if required files are missing 38 | if (!checkRequiredFiles([paths.appHtml, paths.appIndexJs])) { 39 | process.exit(1); 40 | } 41 | 42 | // Tools like Cloud9 rely on this. 43 | const DEFAULT_PORT = parseInt(process.env.PORT, 10) || 3000; 44 | const HOST = process.env.HOST || '0.0.0.0'; 45 | 46 | if (process.env.HOST) { 47 | console.log( 48 | chalk.cyan( 49 | `Attempting to bind to HOST environment variable: ${chalk.yellow( 50 | chalk.bold(process.env.HOST) 51 | )}` 52 | ) 53 | ); 54 | console.log( 55 | `If this was unintentional, check that you haven't mistakenly set it in your shell.` 56 | ); 57 | console.log(`Learn more here: ${chalk.yellow('http://bit.ly/2mwWSwH')}`); 58 | console.log(); 59 | } 60 | 61 | // We attempt to use the default port but if it is busy, we offer the user to 62 | // run on a different port. `choosePort()` Promise resolves to the next free port. 63 | choosePort(HOST, DEFAULT_PORT) 64 | .then(port => { 65 | if (port == null) { 66 | // We have not found a port. 67 | return; 68 | } 69 | const protocol = process.env.HTTPS === 'true' ? 'https' : 'http'; 70 | const appName = require(paths.appPackageJson).name; 71 | const urls = prepareUrls(protocol, HOST, port); 72 | // Create a webpack compiler that is configured with custom messages. 73 | const compiler = createCompiler(webpack, config, appName, urls, useYarn); 74 | // Load proxy config 75 | const proxySetting = require(paths.appPackageJson).proxy; 76 | const proxyConfig = prepareProxy(proxySetting, paths.appPublic); 77 | // Serve webpack assets generated by the compiler over a web sever. 78 | const serverConfig = createDevServerConfig( 79 | proxyConfig, 80 | urls.lanUrlForConfig 81 | ); 82 | const devServer = new WebpackDevServer(compiler, serverConfig); 83 | // Launch WebpackDevServer. 84 | devServer.listen(port, HOST, err => { 85 | if (err) { 86 | return console.log(err); 87 | } 88 | if (isInteractive) { 89 | clearConsole(); 90 | } 91 | console.log(chalk.cyan('Starting the development server...\n')); 92 | openBrowser(urls.localUrlForBrowser); 93 | }); 94 | 95 | ['SIGINT', 'SIGTERM'].forEach(function(sig) { 96 | process.on(sig, function() { 97 | devServer.close(); 98 | process.exit(); 99 | }); 100 | }); 101 | }) 102 | .catch(err => { 103 | if (err && err.message) { 104 | console.log(err.message); 105 | } 106 | process.exit(1); 107 | }); 108 | -------------------------------------------------------------------------------- /scripts/test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Do this as the first thing so that any code reading it knows the right env. 4 | process.env.BABEL_ENV = 'test'; 5 | process.env.NODE_ENV = 'test'; 6 | process.env.PUBLIC_URL = ''; 7 | 8 | // Makes the script crash on unhandled rejections instead of silently 9 | // ignoring them. In the future, promise rejections that are not handled will 10 | // terminate the Node.js process with a non-zero exit code. 11 | process.on('unhandledRejection', err => { 12 | throw err; 13 | }); 14 | 15 | // Ensure environment variables are read. 16 | require('../config/env'); 17 | 18 | const jest = require('jest'); 19 | let argv = process.argv.slice(2); 20 | 21 | // Watch unless on CI or in coverage mode 22 | if (!process.env.CI && argv.indexOf('--coverage') < 0) { 23 | argv.push('--watch'); 24 | } 25 | 26 | 27 | jest.run(argv); 28 | -------------------------------------------------------------------------------- /server/components/users/UsersSchema.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const Schema = mongoose.Schema; 3 | 4 | const UserSchema = new Schema({ 5 | name: { 6 | type: String, 7 | required: true, 8 | }, 9 | email: { 10 | type: String, 11 | required: true, 12 | }, 13 | password: { 14 | type: String, 15 | required: true, 16 | }, 17 | profile: { 18 | type: String 19 | }, 20 | birthDate: { 21 | type: Date, 22 | default: Date.now, 23 | }, 24 | createdDate: { 25 | type: Date, 26 | default: Date.now, 27 | }, 28 | updatedDate: { 29 | type: Date, 30 | default: Date.now, 31 | }, 32 | }); 33 | 34 | module.exports = mongoose.model('users', UserSchema); -------------------------------------------------------------------------------- /server/components/users/users.test.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeffersonRibeiro/react-nodejs-mongodb-crud/27d8baf1ae4a0907d2af363dce4d70284bcc97a9/server/components/users/users.test.js -------------------------------------------------------------------------------- /server/components/users/usersAPI.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const passport = require('passport'); 3 | 4 | const usersController = require('./usersController'); 5 | 6 | const router = express.Router(); 7 | 8 | /* 9 | @route /api/users/register 10 | @desc Register users 11 | @access public 12 | */ 13 | router.post('/register', usersController.register); 14 | 15 | /* 16 | @route /api/users/login 17 | @desc Login users 18 | @access public 19 | */ 20 | router.post('/login', usersController.login); 21 | 22 | /* 23 | @route /api/users/update 24 | @desc Update profile information 25 | @access private 26 | */ 27 | router.put('/update', 28 | passport.authenticate('jwt', { session: false }), 29 | usersController.update, 30 | ); 31 | 32 | /* 33 | @route /api/users/delete 34 | @desc Delete account 35 | @access private 36 | */ 37 | router.delete('/delete', 38 | passport.authenticate('jwt', { session: false }), 39 | usersController.userDelete, 40 | ); 41 | 42 | /* 43 | @route /api/users/all 44 | @desc Get all users 45 | @access private 46 | */ 47 | router.get('/all', 48 | passport.authenticate('jwt', { session: false }), 49 | usersController.getUserList, 50 | ); 51 | 52 | module.exports = router; -------------------------------------------------------------------------------- /server/components/users/usersController.js: -------------------------------------------------------------------------------- 1 | const bcrypt = require('bcryptjs'); 2 | const gravatar = require('gravatar'); 3 | const moment = require('moment'); 4 | 5 | 6 | const { bcryptHash, jwtSign } = require('../../util'); 7 | const User = require('./UsersSchema'); 8 | 9 | function register(req, res) { 10 | User.findOne({ email: req.body.email }) 11 | .then(user => { 12 | if(user) { 13 | return res.json({ 14 | status: 'EMAIL_ALREADY_EXISTS', 15 | message: 'Email já existe', 16 | }); 17 | } 18 | 19 | const profile = gravatar.url(req.body.email, { 20 | s: 200, // size 21 | r: 'pg', // ratings 22 | d: 'mm', // default 23 | }) 24 | 25 | const newUser = User({ 26 | name: req.body.name, 27 | email: req.body.email, 28 | birthDate: moment(req.body.birthDate, 'DD/MM/YYYY'), 29 | password: req.body.password, 30 | profile, 31 | }); 32 | 33 | bcryptHash(newUser.password) 34 | .then(hash => { 35 | newUser.password = hash; 36 | return newUser.save(); 37 | }) 38 | .then(() => { 39 | res.json({ 40 | status: true, 41 | message: 'Usuário criado com sucesso!', 42 | }); 43 | }) 44 | .catch(err => { 45 | throw new Error(err); 46 | }); 47 | 48 | }); 49 | } 50 | 51 | function login(req, res) { 52 | const email = req.body.email; 53 | const password = req.body.password; 54 | 55 | User.findOne({email}) 56 | .then(user => { 57 | /* Email not found */ 58 | if(!user) { 59 | return res.json({ 60 | status: 'USER_NOT_FOUND', 61 | message: 'Usuário não encontrado', 62 | }); 63 | } 64 | 65 | /* Check if password is correct */ 66 | bcrypt.compare(password, user.password).then(isMatch => { 67 | if(!isMatch) { 68 | return res.json({ 69 | status: 'PASSWORD_INCORRECT', 70 | message: 'Senha incorreta', 71 | }); 72 | } 73 | 74 | const payload = { 75 | id: user.id, 76 | name: user.name, 77 | email: user.email, 78 | profile: user.profile, 79 | birthDate: user.birthDate, 80 | createdDate: user.createdDate, 81 | updatedDate: user.updatedDate, 82 | } 83 | 84 | jwtSign(payload) 85 | .then(token => { 86 | res.json({ 87 | status: true, 88 | message: 'Login efeito com sucesso!', 89 | ...payload, 90 | token: `Bearer ${token}`, 91 | }); 92 | }) 93 | .catch(err => { 94 | throw new Error(err); 95 | }) 96 | }); 97 | }); 98 | } 99 | 100 | function update(req, res) { 101 | const { email } = req.user; 102 | const { authorization } = req.headers; 103 | const formData = { 104 | name: req.body.name, 105 | birthDate: moment(req.body.birthDate, 'DD/MM/YYYY'), 106 | updatedDate: req.body.updatedDate, 107 | } 108 | 109 | const opts = { 110 | new: true, 111 | } 112 | 113 | User.findOneAndUpdate({ email }, { $set: formData }, opts) 114 | .then(user => { 115 | const { name, email, profile, birthDate, createdDate, updatedDate } = user; 116 | const payload = { 117 | name, 118 | email, 119 | profile, 120 | birthDate, 121 | createdDate, 122 | updatedDate, 123 | } 124 | 125 | res.json({ 126 | ...payload, 127 | token: authorization, 128 | }); 129 | }); 130 | } 131 | 132 | function userDelete(req, res) { 133 | const { email } = req.user; 134 | 135 | User.findOneAndRemove({ email }) 136 | .then(() => { 137 | res.json({ 138 | status: 'ACCOUNT_DELETED', 139 | message: 'Conta deletada com sucesso!', 140 | }) 141 | }); 142 | } 143 | 144 | const getUserList = (req, res) => { 145 | User.find({}, (err, users) => { 146 | res.send(users); 147 | }); 148 | } 149 | 150 | module.exports = { 151 | register, 152 | login, 153 | update, 154 | userDelete, 155 | getUserList 156 | } -------------------------------------------------------------------------------- /server/components/users/usersPassport.js: -------------------------------------------------------------------------------- 1 | const JwtStrategy = require('passport-jwt').Strategy; 2 | const ExtractJwt = require('passport-jwt').ExtractJwt; 3 | const mongoose = require('mongoose'); 4 | const User = mongoose.model('users'); 5 | const secretOrKey = process.env.AUTH_SECRET_OR_KEY; 6 | 7 | const opts = { 8 | jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), 9 | secretOrKey, 10 | }; 11 | 12 | module.exports = passport => { 13 | passport.use(new JwtStrategy(opts, (jwt_payload, done) => { 14 | User.findById(jwt_payload.id) 15 | .then(user => { 16 | if(user){ 17 | return done(null, user); 18 | } 19 | return done(null, false); 20 | }) 21 | .catch(err => { 22 | throw new Error(err) 23 | }); 24 | })); 25 | } -------------------------------------------------------------------------------- /server/index.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | const express = require('express'); 3 | const mongoose = require('mongoose'); 4 | const bodyParser = require('body-parser'); 5 | const passport = require('passport'); 6 | const cors = require('cors'); 7 | 8 | const usersAPI = require('./components/users/usersAPI'); 9 | 10 | const app = express(); 11 | app.use(bodyParser.urlencoded({ extended: false })); 12 | app.use(bodyParser.json()); 13 | app.use(cors(process.env.CORS_WHITELIST_DOMAINS.split(','))); 14 | 15 | const db = process.env.DATABASE; 16 | 17 | mongoose.connect(db) 18 | .then(() => console.log('Connected to Database')) 19 | .catch(err => { 20 | throw new Error(err) 21 | }); 22 | 23 | app.use(passport.initialize()); 24 | 25 | require('./components/users/usersPassport')(passport); 26 | 27 | app.use('/api/users', usersAPI); 28 | 29 | app.listen(process.env.API_PORT, () => { 30 | console.log(`Server running on port ${process.env.API_PORT}`); 31 | }); -------------------------------------------------------------------------------- /server/util/index.js: -------------------------------------------------------------------------------- 1 | const jwt = require('jsonwebtoken'); 2 | const bcrypt = require('bcryptjs'); 3 | 4 | 5 | exports.jwtSign = (payload) => { 6 | const { 7 | SESSION_EXPIRES_IN, 8 | AUTH_SECRET_OR_KEY 9 | } = process.env; 10 | 11 | return new Promise((resolve, reject) => { 12 | jwt.sign(payload, AUTH_SECRET_OR_KEY, { expiresIn: SESSION_EXPIRES_IN }, (err, token) => { 13 | if(err) { 14 | reject(err); 15 | } else { 16 | resolve(token); 17 | } 18 | }); 19 | }) 20 | } 21 | 22 | exports.bcryptHash = (str) => { 23 | const salt = 10; 24 | 25 | return new Promise((resolve, reject) => { 26 | bcrypt.hash(str, salt, (err, hash) => { 27 | if(err) { 28 | reject(err); 29 | } else { 30 | resolve(hash); 31 | } 32 | }); 33 | }); 34 | } -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Provider } from 'react-redux'; 3 | import { BrowserRouter } from 'react-router-dom'; 4 | 5 | import Root from './components/Root'; 6 | import store from './services/store'; 7 | 8 | class App extends Component { 9 | render() { 10 | return ( 11 | 12 | 13 | 14 | 15 | 16 | ); 17 | } 18 | } 19 | 20 | export default App; 21 | -------------------------------------------------------------------------------- /src/components/Root/AppBar/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { compose } from 'redux'; 3 | import { connect } from 'react-redux'; 4 | import PropTypes from 'prop-types'; 5 | import { NavLink } from 'react-router-dom'; 6 | import { withStyles } from 'material-ui/styles'; 7 | import AppBar from 'material-ui/AppBar'; 8 | import Toolbar from 'material-ui/Toolbar'; 9 | import Typography from 'material-ui/Typography'; 10 | import IconButton from 'material-ui/IconButton'; 11 | import Menu, { MenuItem } from 'material-ui/Menu'; 12 | import { ListItemIcon, ListItemText } from 'material-ui/List'; 13 | import LogoutIcon from '@material-ui/icons/Input'; 14 | import Avatar from '@material-ui/core/Avatar'; 15 | import Dialog from '@material-ui/core/Dialog'; 16 | import DialogActions from '@material-ui/core/DialogActions'; 17 | import DialogContent from '@material-ui/core/DialogContent'; 18 | import DialogContentText from '@material-ui/core/DialogContentText'; 19 | import DialogTitle from '@material-ui/core/DialogTitle'; 20 | import Button from '@material-ui/core/Button'; 21 | 22 | import { logout } from '../../../services/users/actions'; 23 | 24 | const styles = theme => ({ 25 | appBar: { 26 | zIndex: theme.zIndex.drawer + 1, 27 | position: 'fixed', 28 | }, 29 | flex: { 30 | flex: 1, 31 | }, 32 | }); 33 | 34 | class _AppBar extends Component { 35 | state = { 36 | anchorEl: null, 37 | dialogOpen: false, 38 | }; 39 | 40 | handleLogout = () => { 41 | this.handleMenuClose(); 42 | this.props.logout(); 43 | } 44 | 45 | handleMenu = event => { 46 | this.setState({ anchorEl: event.currentTarget }); 47 | }; 48 | 49 | handleMenuClose = () => { 50 | this.setState({ anchorEl: null }); 51 | }; 52 | 53 | handleToggleDialog = () => { 54 | this.setState(prevState => ({ 55 | dialogOpen: !prevState.dialogOpen, 56 | }) 57 | ) 58 | } 59 | 60 | render() { 61 | const { classes, user } = this.props; 62 | const { anchorEl } = this.state; 63 | const open = !!anchorEl; 64 | 65 | return ( 66 | 67 | 68 | 69 | CRUD 70 | 71 |
72 | 79 | 80 | 81 | 95 | 96 | Perfil 97 | 98 | { 99 | this.handleMenuClose(); 100 | this.handleToggleDialog(); 101 | }}> 102 | 103 | 104 | 105 | 106 | 107 | 108 |
109 |
110 | 117 | Tem certeza de que deseja sair? 118 | 119 | 120 | Continue mais um tempo com a gente, somos tão legais :) 121 | 122 | 123 | 124 | 127 | 130 | 131 | 132 |
133 | ); 134 | } 135 | } 136 | 137 | _AppBar.propTypes = { 138 | classes: PropTypes.object.isRequired, 139 | }; 140 | 141 | const mapStateToProps = state => ({ 142 | user: state.user.data, 143 | }) 144 | 145 | export default compose( 146 | withStyles(styles), 147 | connect(mapStateToProps, { logout }), 148 | )(_AppBar); -------------------------------------------------------------------------------- /src/components/Root/AppBar/style.scss: -------------------------------------------------------------------------------- 1 | .navbar-fixed { 2 | nav { 3 | padding-right: 15px; 4 | padding-left: 15px; 5 | // background-color: #ffc107; 6 | } 7 | 8 | .brand-logo { 9 | font-size: 18px; 10 | img { 11 | max-height: 30px; 12 | margin-right: 5px; 13 | vertical-align: middle; 14 | } 15 | 16 | span { 17 | vertical-align: middle; 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /src/components/Root/Main/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { compose } from 'redux'; 3 | import { connect } from 'react-redux'; 4 | import { withRouter } from 'react-router-dom'; 5 | import PropTypes from 'prop-types'; 6 | import { withStyles } from 'material-ui/styles'; 7 | 8 | const styles = theme => ({ 9 | content: { 10 | flexGrow: 1, 11 | backgroundColor: theme.palette.background.default, 12 | padding: theme.spacing.unit * 3, 13 | minWidth: 0, 14 | }, 15 | toolbar: theme.mixins.toolbar, 16 | logged: { 17 | paddingLeft: 264, 18 | }, 19 | notLogged: { 20 | maxWidth: '400px', 21 | margin: '0 auto', 22 | } 23 | }); 24 | 25 | class Main extends Component { 26 | render() { 27 | const { classes, children, user } = this.props; 28 | 29 | return ( 30 |
31 |
32 | {children} 33 |
34 | ); 35 | } 36 | } 37 | 38 | Main.prototypes = { 39 | classes: PropTypes.object.isRequired, 40 | } 41 | 42 | const mapStateToProps = state => ({ 43 | user: state.user.data, 44 | }); 45 | 46 | export default withRouter( 47 | compose( 48 | withStyles(styles), 49 | connect(mapStateToProps, {}), 50 | )(Main) 51 | ); -------------------------------------------------------------------------------- /src/components/Root/SideNav/components/NavigationList/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { NavLink } from 'react-router-dom'; 3 | import HomeIcon from '@material-ui/icons/Home'; 4 | import ListIcon from '@material-ui/icons/List'; 5 | import List, { ListSubheader, ListItem, ListItemIcon, ListItemText } from 'material-ui/List'; 6 | 7 | import './style.scss'; 8 | 9 | const NavigationList = props => { 10 | return ( 11 | Menu} 15 | > 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | ); 30 | } 31 | 32 | export default NavigationList; -------------------------------------------------------------------------------- /src/components/Root/SideNav/components/NavigationList/style.scss: -------------------------------------------------------------------------------- 1 | .nav-link { 2 | .active { 3 | h3 { 4 | color: #3f51b5; 5 | } 6 | } 7 | } -------------------------------------------------------------------------------- /src/components/Root/SideNav/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { withStyles } from 'material-ui/styles'; 4 | import Drawer from 'material-ui/Drawer'; 5 | 6 | import NavigationList from './components/NavigationList'; 7 | 8 | 9 | const drawerWidth = 240; 10 | 11 | const styles = theme => ({ 12 | drawerPaper: { 13 | position: 'fixed', 14 | width: drawerWidth, 15 | }, 16 | toolbar: theme.mixins.toolbar, 17 | }); 18 | 19 | const SideNav = props => { 20 | const { classes } = props; 21 | 22 | return ( 23 | 30 |
31 | 32 | 33 | 34 | 35 | ); 36 | } 37 | 38 | SideNav.propTypes = { 39 | classes: PropTypes.object.isRequired, 40 | }; 41 | 42 | export default withStyles(styles)(SideNav); -------------------------------------------------------------------------------- /src/components/Root/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { withRouter } from 'react-router-dom'; 4 | import { compose } from 'redux'; 5 | import { connect } from 'react-redux'; 6 | 7 | import getRoutes from '../../routes'; 8 | import { withStyles } from 'material-ui/styles'; 9 | import Main from './Main'; 10 | import SideNav from './SideNav'; 11 | import AppBar from './AppBar'; 12 | 13 | const styles = theme => ({ 14 | root: { 15 | flexGrow: 1, 16 | zIndex: 1, 17 | overflow: 'hidden', 18 | position: 'relative', 19 | display: 'flex', 20 | }, 21 | }); 22 | 23 | class Root extends Component { 24 | 25 | render() { 26 | const { classes, user } = this.props; 27 | 28 | return ( 29 |
30 | {user.auth && 31 | 32 | 33 | 34 | 35 | } 36 |
37 | { getRoutes() } 38 |
39 |
40 | ); 41 | } 42 | } 43 | 44 | Root.proptypes = { 45 | classes: PropTypes.object.isRequired, 46 | users: PropTypes.object.isRequired, 47 | } 48 | 49 | const mapStateToProps = state => ({ 50 | user: state.user.data, 51 | }); 52 | 53 | export default withRouter( 54 | compose( 55 | withStyles(styles), 56 | connect(mapStateToProps, {}), 57 | )(Root) 58 | ); -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import CssBaseline from 'material-ui/CssBaseline'; 4 | import App from './App'; 5 | import registerServiceWorker from './registerServiceWorker'; 6 | 7 | 8 | import './index.scss'; 9 | 10 | ReactDOM.render( 11 | 12 | 13 | , 14 | document.getElementById('root') 15 | ); 16 | 17 | registerServiceWorker(); 18 | -------------------------------------------------------------------------------- /src/index.scss: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: "Roboto", "Helvetica", "Arial", sans-serif; 3 | } -------------------------------------------------------------------------------- /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 | 39 | // Add some additional logging to localhost, pointing developers to the 40 | // service worker/PWA documentation. 41 | navigator.serviceWorker.ready.then(() => { 42 | console.log( 43 | 'This web app is being served cache-first by a service ' + 44 | 'worker. To learn more, visit https://goo.gl/SC7cgQ' 45 | ); 46 | }); 47 | } else { 48 | // Is not local host. Just register service worker 49 | registerValidSW(swUrl); 50 | } 51 | }); 52 | } 53 | } 54 | 55 | function registerValidSW(swUrl) { 56 | navigator.serviceWorker 57 | .register(swUrl) 58 | .then(registration => { 59 | registration.onupdatefound = () => { 60 | const installingWorker = registration.installing; 61 | installingWorker.onstatechange = () => { 62 | if (installingWorker.state === 'installed') { 63 | if (navigator.serviceWorker.controller) { 64 | // At this point, the old content will have been purged and 65 | // the fresh content will have been added to the cache. 66 | // It's the perfect time to display a "New content is 67 | // available; please refresh." message in your web app. 68 | console.log('New content is available; please refresh.'); 69 | } else { 70 | // At this point, everything has been precached. 71 | // It's the perfect time to display a 72 | // "Content is cached for offline use." message. 73 | console.log('Content is cached for offline use.'); 74 | } 75 | } 76 | }; 77 | }; 78 | }) 79 | .catch(error => { 80 | console.error('Error during service worker registration:', error); 81 | }); 82 | } 83 | 84 | function checkValidServiceWorker(swUrl) { 85 | // Check if the service worker can be found. If it can't reload the page. 86 | fetch(swUrl) 87 | .then(response => { 88 | // Ensure service worker exists, and that we really are getting a JS file. 89 | if ( 90 | response.status === 404 || 91 | response.headers.get('content-type').indexOf('javascript') === -1 92 | ) { 93 | // No service worker found. Probably a different app. Reload the page. 94 | navigator.serviceWorker.ready.then(registration => { 95 | registration.unregister().then(() => { 96 | window.location.reload(); 97 | }); 98 | }); 99 | } else { 100 | // Service worker found. Proceed as normal. 101 | registerValidSW(swUrl); 102 | } 103 | }) 104 | .catch(() => { 105 | console.log( 106 | 'No internet connection found. App is running in offline mode.' 107 | ); 108 | }); 109 | } 110 | 111 | export function unregister() { 112 | if ('serviceWorker' in navigator) { 113 | navigator.serviceWorker.ready.then(registration => { 114 | registration.unregister(); 115 | }); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/routes.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Route, Switch, Redirect } from 'react-router-dom'; 3 | 4 | import PrivateRoute from './scenes/components/PrivateRoute'; 5 | import asyncComponent from './scenes/components/hoc/asyncComponent'; 6 | 7 | const Home = from('./scenes/Home'); 8 | const Login = from('./scenes/Login'); 9 | const Register = from('./scenes/Register'); 10 | const Profile = from('./scenes/Profile'); 11 | const Users = from('./scenes/Users'); 12 | 13 | function from(path) { 14 | return asyncComponent(() => { 15 | return import(`${path}/index.js`); 16 | }); 17 | } 18 | 19 | export default () => ( 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | } /> 28 | 29 | ); -------------------------------------------------------------------------------- /src/scenes/Home/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Typography from 'material-ui/Typography'; 3 | 4 | const Home = () => ( 5 | 6 | Página Inicial 7 | 8 | ); 9 | 10 | export default Home; -------------------------------------------------------------------------------- /src/scenes/Login/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { compose } from 'redux'; 3 | import { connect } from 'react-redux'; 4 | import { Link, withRouter } from 'react-router-dom'; 5 | import { withStyles } from '@material-ui/core/styles'; 6 | import IconButton from '@material-ui/core/IconButton'; 7 | import Input from '@material-ui/core/Input'; 8 | import InputLabel from '@material-ui/core/InputLabel'; 9 | import InputAdornment from '@material-ui/core/InputAdornment'; 10 | import FormControl from '@material-ui/core/FormControl'; 11 | import Visibility from '@material-ui/icons/Visibility'; 12 | import VisibilityOff from '@material-ui/icons/VisibilityOff'; 13 | import Button from '@material-ui/core/Button'; 14 | import FormHelperText from '@material-ui/core/FormHelperText'; 15 | 16 | import { login } from '../../services/users/actions'; 17 | 18 | const styles = theme => ({ 19 | root: { 20 | display: 'flex', 21 | flexWrap: 'wrap', 22 | }, 23 | margin: { 24 | margin: theme.spacing.unit, 25 | }, 26 | fill: { 27 | flexBasis: '100%', 28 | }, 29 | }); 30 | 31 | 32 | class Login extends Component { 33 | state = { 34 | email: '', 35 | password: '', 36 | showPassword: false, 37 | }; 38 | 39 | componentDidMount() { 40 | this.redirectLogged(); 41 | } 42 | 43 | componentDidUpdate() { 44 | this.redirectLogged(); 45 | } 46 | 47 | redirectLogged() { 48 | const { user, history } = this.props; 49 | 50 | if(user.auth) { 51 | history.push('/'); 52 | } 53 | } 54 | 55 | handleLogin = event => { 56 | const { email, password } = event.target; 57 | const data = { 58 | email: email.value, 59 | password: password.value, 60 | } 61 | 62 | this.props.login(data); 63 | 64 | event.preventDefault(); 65 | } 66 | 67 | handleChange = prop => event => { 68 | this.setState({ [prop]: event.target.value }); 69 | } 70 | 71 | handleClickShowPassword = () => { 72 | this.setState(prevState => { 73 | return ({ 74 | showPassword: !prevState.showPassword, 75 | }); 76 | }) 77 | } 78 | 79 | render() { 80 | const { classes, error } = this.props; 81 | return ( 82 |
83 | {/* EMAIL */} 84 | 89 | Email 90 | 97 | {error.status === 'USER_NOT_FOUND' && 98 | {error.message} 99 | } 100 | 101 | {/* PASSWORD */} 102 | 107 | Password 108 | 116 | 120 | {this.state.showPassword ? : } 121 | 122 | 123 | } 124 | /> 125 | {error.status === 'PASSWORD_INCORRECT' && 126 | {error.message} 127 | } 128 | 129 | 132 | 135 |
136 | ); 137 | } 138 | } 139 | 140 | const mapStateToProps = state => ({ 141 | user: state.user.data, 142 | error: state.error, 143 | }) 144 | 145 | export default withRouter( 146 | compose( 147 | withStyles(styles), 148 | connect(mapStateToProps, { login }), 149 | )(Login) 150 | ); -------------------------------------------------------------------------------- /src/scenes/Profile/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { compose } from 'redux'; 3 | import { connect } from 'react-redux'; 4 | import Typography from 'material-ui/Typography'; 5 | import { withStyles } from '@material-ui/core/styles'; 6 | import Input from '@material-ui/core/Input'; 7 | import InputLabel from '@material-ui/core/InputLabel'; 8 | import FormControl from '@material-ui/core/FormControl'; 9 | import Button from '@material-ui/core/Button'; 10 | import DatePicker from 'material-ui-pickers/DatePicker'; 11 | import MomentUtils from 'material-ui-pickers/utils/moment-utils'; 12 | import MuiPickersUtilsProvider from 'material-ui-pickers/utils/MuiPickersUtilsProvider'; 13 | import Snackbar from '@material-ui/core/Snackbar'; 14 | import IconButton from '@material-ui/core/IconButton'; 15 | import CloseIcon from '@material-ui/icons/Close'; 16 | import moment from 'moment'; 17 | 18 | import { updateProfile } from '../../services/users/actions'; 19 | import { logout } from '../../services/users/actions'; 20 | import axios from '../../services/axios'; 21 | 22 | const styles = theme => ({ 23 | root: { 24 | display: 'flex', 25 | flexWrap: 'wrap', 26 | justifyContent: 'space-between', 27 | }, 28 | close: { 29 | width: theme.spacing.unit * 4, 30 | height: theme.spacing.unit * 4, 31 | }, 32 | margin: { 33 | margin: theme.spacing.unit, 34 | }, 35 | leftIcon: { 36 | marginRight: theme.spacing.unit, 37 | }, 38 | fill: { 39 | flexBasis: '100%', 40 | }, 41 | }); 42 | 43 | class Profile extends Component { 44 | constructor(props) { 45 | super(props); 46 | 47 | const { name, birthDate } = this.props.user; 48 | this.state = { 49 | name, 50 | birthDate, 51 | snackbarOpen: false, 52 | }; 53 | } 54 | 55 | componentDidUpdate(prevProps) { 56 | if(prevProps.user.updatedDate !== this.props.user.updatedDate){ 57 | this.setState({ snackbarOpen: true }); 58 | } 59 | } 60 | 61 | 62 | handleUpdate = event => { 63 | const { user, updateProfile } = this.props; 64 | const { name, birthDate } = event.target; 65 | const formData = { 66 | name: name.value, 67 | birthDate: birthDate.value, 68 | updatedDate: new Date(), 69 | } 70 | 71 | updateProfile(formData, user.token); 72 | 73 | event.preventDefault(); 74 | } 75 | 76 | handleDeleteAccount = () => { 77 | const { user, logout } = this.props; 78 | 79 | var config = { 80 | headers: { 81 | 'Accept': '', 82 | 'Authorization': user.token, 83 | } 84 | }; 85 | 86 | axios.delete('/users/delete', config) 87 | .then(res => { 88 | alert('Conta deletada com sucesso!'); 89 | logout(); 90 | }) 91 | .catch(err => console.log(err)); 92 | } 93 | 94 | handleChange = prop => event => { 95 | this.setState({ [prop]: event.target.value }); 96 | } 97 | 98 | handleBirthDateChange = date => { 99 | this.setState({ birthDate: date }); 100 | } 101 | 102 | handleSnackbarClose = (event, reason) => { 103 | if (reason === 'clickaway') { 104 | return; 105 | } 106 | 107 | this.setState({ snackbarOpen: false }); 108 | }; 109 | 110 | render() { 111 | const { classes, user } = this.props; 112 | 113 | return ( 114 | 115 | 116 | Meu Perfil 117 | 118 |
119 | {/* NAME */} 120 | 121 | Nome Completo 122 | 129 | 130 | {/* EMAIL */} 131 | 132 | Email 133 | 139 | 140 | {/* BIRTH DATE */} 141 | 142 | 143 | (value ? [/\d/, /\d/, '/', /\d/, /\d/, '/', /\d/, /\d/, /\d/, /\d/] : [])} 150 | value={this.state.birthDate} 151 | onChange={this.handleBirthDateChange} 152 | /> 153 | 154 | 155 | {/* CREATED DATE */} 156 | 157 | Criado em 158 | 164 | 165 | {/* UPDATED DATE */} 166 | 167 | Atualizado em 168 | 174 | 175 | 178 |
179 | 182 | Perfil Atualizado} 194 | action={[ 195 | 202 | 203 | , 204 | ]} 205 | /> 206 |
207 | ); 208 | } 209 | } 210 | 211 | const mapStateToProps = state => ({ 212 | user: state.user.data, 213 | }) 214 | 215 | export default compose( 216 | withStyles(styles), 217 | connect(mapStateToProps, { updateProfile, logout }), 218 | )(Profile); -------------------------------------------------------------------------------- /src/scenes/Register/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { compose } from 'redux'; 3 | import { connect } from 'react-redux'; 4 | import { Link } from 'react-router-dom'; 5 | import { withStyles } from '@material-ui/core/styles'; 6 | import IconButton from '@material-ui/core/IconButton'; 7 | import Input from '@material-ui/core/Input'; 8 | import InputLabel from '@material-ui/core/InputLabel'; 9 | import InputAdornment from '@material-ui/core/InputAdornment'; 10 | import FormControl from '@material-ui/core/FormControl'; 11 | import Visibility from '@material-ui/icons/Visibility'; 12 | import VisibilityOff from '@material-ui/icons/VisibilityOff'; 13 | import Button from '@material-ui/core/Button'; 14 | import ChevronLeft from '@material-ui/icons/ChevronLeft'; 15 | import FormHelperText from '@material-ui/core/FormHelperText'; 16 | import Dialog from '@material-ui/core/Dialog'; 17 | import DialogActions from '@material-ui/core/DialogActions'; 18 | import DialogContent from '@material-ui/core/DialogContent'; 19 | import DialogContentText from '@material-ui/core/DialogContentText'; 20 | import DialogTitle from '@material-ui/core/DialogTitle'; 21 | import DatePicker from 'material-ui-pickers/DatePicker'; 22 | import DateFnsUtils from 'material-ui-pickers/utils/date-fns-utils'; 23 | import MuiPickersUtilsProvider from 'material-ui-pickers/utils/MuiPickersUtilsProvider'; 24 | 25 | import { FORM_SUBMIT_FAIL } from '../../services/errors/actionTypes'; 26 | import axios from '../../services/axios'; 27 | 28 | const styles = theme => ({ 29 | root: { 30 | display: 'flex', 31 | flexWrap: 'wrap', 32 | justifyContent: 'space-between', 33 | }, 34 | margin: { 35 | margin: theme.spacing.unit, 36 | }, 37 | leftIcon: { 38 | marginRight: theme.spacing.unit, 39 | }, 40 | fill: { 41 | flexBasis: '100%', 42 | }, 43 | }); 44 | 45 | const formSubmitFail = payload => dispatch => { 46 | return dispatch({ 47 | type: FORM_SUBMIT_FAIL, 48 | payload, 49 | }); 50 | } 51 | 52 | 53 | class Login extends Component { 54 | state = { 55 | name: '', 56 | email: '', 57 | birthDate: null, 58 | password: '', 59 | showPassword: false, 60 | beforeSubmitError: false, 61 | dialog: { 62 | open: false, 63 | title: '', 64 | content: '', 65 | } 66 | }; 67 | 68 | handleRegister = event => { 69 | const { formSubmitFail } = this.props; 70 | const { name, email, birthDate, password } = event.target; 71 | const data = { 72 | name: name.value, 73 | email: email.value, 74 | birthDate: birthDate.value, 75 | password: password.value, 76 | } 77 | 78 | const blankInputs = Object.keys(data).filter(key => data[key] === ''); 79 | 80 | if( blankInputs.length > 0 ) { 81 | this.setState({ beforeSubmitError: true }); 82 | } else { 83 | this.setState({ beforeSubmitError: false }); 84 | axios.post('/users/register', data) 85 | .then(res => { 86 | const { status, message } = res.data; 87 | 88 | if(status !== true) { 89 | formSubmitFail({ 90 | status, 91 | message, 92 | }); 93 | } else { 94 | this.handleOpenDialog(message, 'Agora você já pode fazer login com seu email e senha na tela inicial :D'); 95 | } 96 | }) 97 | .catch(err => console.log(err)); 98 | } 99 | 100 | event.preventDefault(); 101 | } 102 | 103 | handleChange = prop => event => { 104 | this.setState({ [prop]: event.target.value }); 105 | } 106 | 107 | handleClickShowPassword = () => { 108 | this.setState(prevState => { 109 | return ({ 110 | showPassword: !prevState.showPassword, 111 | }); 112 | }) 113 | } 114 | 115 | handleOpenDialog = (title, content) => { 116 | const dialog = { 117 | title, 118 | content, 119 | open: true, 120 | } 121 | this.setState({ dialog }); 122 | } 123 | 124 | handleCloseDialog = () => { 125 | const dialog = { 126 | title: '', 127 | content: '', 128 | open: false, 129 | } 130 | this.setState({ dialog }) 131 | } 132 | 133 | handleBirthDateChange = date => { 134 | this.setState({ birthDate: date }); 135 | } 136 | 137 | render() { 138 | const { classes, error } = this.props; 139 | return ( 140 | 141 |
142 | {/* NAME */} 143 | 148 | Nome Completo 149 | 156 | {(this.state.beforeSubmitError && this.state.name === '') && 157 | Preencha o nome 158 | } 159 | 160 | {/* EMAIL */} 161 | 168 | Email 169 | 177 | {((error.status === 'EMAIL_ALREADY_EXISTS') || (this.state.beforeSubmitError && this.state.name === '')) && 178 | {error.message || 'Preencha o email'} 179 | } 180 | 181 | {/* DATA NASCIMENTO */} 182 | 187 | 188 | (value ? [/\d/, /\d/, '/', /\d/, /\d/, '/', /\d/, /\d/, /\d/, /\d/] : [])} 195 | value={this.state.birthDate} 196 | onChange={this.handleBirthDateChange} 197 | /> 198 | 199 | {(this.state.beforeSubmitError && this.state.birthDate === '') && 200 | Preencha a data de nascimento 201 | } 202 | 203 | 204 | {/* PASSWORD */} 205 | 210 | Password 211 | 219 | 223 | {this.state.showPassword ? : } 224 | 225 | 226 | } 227 | /> 228 | {(this.state.beforeSubmitError && this.state.password === '') && 229 | Preencha a senha 230 | } 231 | 232 | 236 | 239 |
240 | 246 | {this.state.dialog.title} 247 | 248 | 249 | {this.state.dialog.content} 250 | 251 | 252 | 253 | 256 | 257 | 258 |
259 | ); 260 | } 261 | } 262 | 263 | const mapStateToProps = state => ({ 264 | error: state.error, 265 | }) 266 | 267 | export default compose( 268 | withStyles(styles), 269 | connect(mapStateToProps, { formSubmitFail }), 270 | )(Login); -------------------------------------------------------------------------------- /src/scenes/Users/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { compose } from 'redux'; 3 | import { connect } from 'react-redux'; 4 | import moment from 'moment'; 5 | import Typography from 'material-ui/Typography'; 6 | import { withStyles } from '@material-ui/core/styles'; 7 | import Table from '@material-ui/core/Table'; 8 | import TableBody from '@material-ui/core/TableBody'; 9 | import TableCell from '@material-ui/core/TableCell'; 10 | import TableHead from '@material-ui/core/TableHead'; 11 | import TableRow from '@material-ui/core/TableRow'; 12 | import Paper from '@material-ui/core/Paper'; 13 | import Avatar from '@material-ui/core/Avatar'; 14 | import LinearProgress from '@material-ui/core/LinearProgress'; 15 | 16 | import axios from '../../services/axios'; 17 | 18 | const styles = theme => ({ 19 | root: { 20 | width: '100%', 21 | marginTop: theme.spacing.unit * 3, 22 | overflowX: 'auto', 23 | }, 24 | table: { 25 | minWidth: 700, 26 | }, 27 | }); 28 | 29 | class Users extends Component { 30 | state = { 31 | userList: [], 32 | } 33 | 34 | componentDidMount() { 35 | const { user } = this.props; 36 | const config = { 37 | headers: { 38 | 'Authorization': user.token, 39 | } 40 | }; 41 | 42 | axios.get('/users/all', config) 43 | .then(res => { 44 | this.setState({ userList: res.data }); 45 | }); 46 | } 47 | 48 | render() { 49 | const { classes } = this.props; 50 | const { userList } = this.state; 51 | 52 | return ( 53 | 54 | Lista de Usuários 55 | 56 | 57 | 58 | 59 | 60 | {!userList.length && 61 | 62 | } 63 | 64 | Nome 65 | Email 66 | Criado em 67 | Data Nascimento 68 | 69 | 70 | 71 | {userList.map(u => { 72 | return ( 73 | 74 | 75 | 76 | 77 | 78 | {u.name} 79 | 80 | {u.email} 81 | 82 | {moment(u.createdDate).format('MMMM Do YYYY, h:mm:ss a')} 83 | {moment(u.birthDate).format('DD/MM/YYYY')} 84 | 85 | ); 86 | })} 87 | 88 |
89 |
90 |
91 | ); 92 | } 93 | } 94 | 95 | const mapStateToProps = state => ({ 96 | user: state.user.data, 97 | }); 98 | 99 | export default compose( 100 | withStyles(styles), 101 | connect(mapStateToProps, {}), 102 | )(Users); -------------------------------------------------------------------------------- /src/scenes/components/PrivateRoute.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { Route, Redirect, withRouter } from 'react-router-dom'; 4 | 5 | const PrivateRoute = ({ component: Component, user, ...rest }) => { 6 | return ( 7 | user.auth ? : } 10 | /> 11 | ); 12 | } 13 | 14 | const mapStateToProps = state => ({ 15 | user: state.user.data, 16 | }); 17 | 18 | export default withRouter(connect(mapStateToProps, {})(PrivateRoute)); -------------------------------------------------------------------------------- /src/scenes/components/hoc/asyncComponent.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | const asyncComponent = importComponent => { 4 | return class extends Component { 5 | state = { 6 | component: null, 7 | } 8 | 9 | componentDidMount() { 10 | importComponent().then(cmp => { 11 | this.setState({component: cmp.default}); 12 | }) 13 | } 14 | 15 | render() { 16 | const C = this.state.component; 17 | 18 | return C ? : null; 19 | } 20 | } 21 | } 22 | 23 | export default asyncComponent; 24 | -------------------------------------------------------------------------------- /src/services/axios.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | const instance = axios.create({ 4 | baseURL: 'http://localhost:8001/api/', 5 | }); 6 | 7 | export default instance; -------------------------------------------------------------------------------- /src/services/errors/actionTypes.js: -------------------------------------------------------------------------------- 1 | export const FORM_SUBMIT_FAIL = 'FORM_SUBMIT_FAIL'; -------------------------------------------------------------------------------- /src/services/errors/reducer.js: -------------------------------------------------------------------------------- 1 | import { FORM_SUBMIT_FAIL } from './actionTypes'; 2 | 3 | const initialState = { 4 | status: '', 5 | message: '', 6 | } 7 | 8 | export default function(state = initialState, action){ 9 | switch(action.type){ 10 | case FORM_SUBMIT_FAIL: 11 | return { 12 | ...state, 13 | ...action.payload, 14 | } 15 | default: 16 | return state; 17 | } 18 | } -------------------------------------------------------------------------------- /src/services/reducers.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import usersReducer from './users/reducer'; 3 | import errorsReducer from './errors/reducer'; 4 | 5 | 6 | export default combineReducers({ 7 | user: usersReducer, 8 | error: errorsReducer, 9 | }); -------------------------------------------------------------------------------- /src/services/store.js: -------------------------------------------------------------------------------- 1 | import { compose, createStore, applyMiddleware } from "redux"; 2 | import thunk from 'redux-thunk'; 3 | import rootReducer from './reducers'; 4 | 5 | const initialState = JSON.parse(window.localStorage.getItem('state')) || {}; 6 | const middleware = [thunk]; 7 | 8 | const store = createStore( 9 | rootReducer, 10 | initialState, 11 | compose( 12 | applyMiddleware(...middleware), 13 | window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__() 14 | ) 15 | ); 16 | 17 | store.subscribe(() => { 18 | const state = store.getState(); 19 | const persist = { 20 | user: state.user, 21 | } 22 | 23 | window.localStorage.setItem('state', JSON.stringify(persist)); 24 | }); 25 | 26 | export default store; -------------------------------------------------------------------------------- /src/services/users/actionTypes.js: -------------------------------------------------------------------------------- 1 | export const USER_LOGIN = 'USER_LOGIN'; 2 | export const USER_LOGOUT = 'USER_LOGOUT'; 3 | export const UPDATE_PROFILE = 'UPDATE_PROFILE'; -------------------------------------------------------------------------------- /src/services/users/actions.js: -------------------------------------------------------------------------------- 1 | import { USER_LOGIN, UPDATE_PROFILE, USER_LOGOUT } from './actionTypes'; 2 | import { FORM_SUBMIT_FAIL } from '../errors/actionTypes'; 3 | 4 | import axios from '../axios'; 5 | 6 | export const login = formData => dispatch => { 7 | axios.post('/users/login', formData) 8 | .then(res => { 9 | const { 10 | status, 11 | message, 12 | name, 13 | email, 14 | profile, 15 | birthDate, 16 | createdDate, 17 | updatedDate, 18 | token 19 | } = res.data; 20 | 21 | if(status !== true) { 22 | return dispatch({ 23 | type: FORM_SUBMIT_FAIL, 24 | payload: { 25 | status, 26 | message, 27 | }, 28 | }); 29 | } 30 | 31 | const payload = { 32 | name, 33 | email, 34 | profile, 35 | birthDate, 36 | createdDate, 37 | updatedDate, 38 | token, 39 | } 40 | 41 | return dispatch({ 42 | type: USER_LOGIN, 43 | payload, 44 | }); 45 | 46 | }) 47 | .catch(err => console.log(err)); 48 | } 49 | 50 | export const updateProfile = (formData, token) => dispatch => { 51 | const config = { 52 | headers: { 53 | 'Authorization': token, 54 | } 55 | }; 56 | 57 | axios.put('/users/update', formData, config) 58 | .then(res => { 59 | const { 60 | name, 61 | email, 62 | profile, 63 | birthDate, 64 | createdDate, 65 | updatedDate, 66 | token 67 | } = res.data; 68 | 69 | const payload = { 70 | name, 71 | email, 72 | profile, 73 | birthDate, 74 | createdDate, 75 | updatedDate, 76 | token, 77 | } 78 | 79 | return dispatch({ 80 | type: UPDATE_PROFILE, 81 | payload, 82 | }); 83 | 84 | }) 85 | .catch(err => console.log(err)); 86 | 87 | } 88 | 89 | export const logout = () => dispatch => { 90 | window.localStorage.removeItem('state'); 91 | return dispatch({ 92 | type: USER_LOGOUT, 93 | payload: {} 94 | }); 95 | } -------------------------------------------------------------------------------- /src/services/users/reducer.js: -------------------------------------------------------------------------------- 1 | import { USER_LOGIN, UPDATE_PROFILE, USER_LOGOUT } from './actionTypes'; 2 | 3 | const initialState = { 4 | data: { 5 | auth: false, 6 | name: '', 7 | email: '', 8 | profile: '', 9 | birthDate: '', 10 | createdDate: '', 11 | updatedDate: '', 12 | token: '', 13 | }, 14 | } 15 | 16 | export default function(state = initialState, action){ 17 | switch(action.type){ 18 | case USER_LOGIN: 19 | return { 20 | ...state, 21 | data: { 22 | auth: true, 23 | ...action.payload, 24 | } 25 | } 26 | case UPDATE_PROFILE: 27 | return { 28 | ...state, 29 | data: { 30 | auth: true, 31 | ...action.payload, 32 | } 33 | } 34 | case USER_LOGOUT: 35 | return { 36 | ...state, 37 | data: { 38 | auth: false, 39 | ...action.payload, 40 | } 41 | } 42 | default: 43 | return state; 44 | } 45 | } --------------------------------------------------------------------------------