├── .gitignore ├── .vscode └── settings.json ├── src ├── index.js └── components │ ├── WithDataLoading.jsx │ ├── Main.jsx │ ├── Home.jsx │ ├── Users.jsx │ └── Login.jsx ├── server ├── models │ └── User.js ├── controllers │ └── Users.js └── index.js ├── public └── index.html ├── README.md └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "git.ignoreLimitWarning": true 3 | } -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import { Main } from './components/Main' 4 | 5 | ReactDOM.render( 6 |
, 7 | document.getElementById('app') 8 | ) 9 | -------------------------------------------------------------------------------- /src/components/WithDataLoading.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export function WithDataLoading(Component) { 4 | return function WihLoadingComponent({ isLoading, ...props }) { 5 | if (!isLoading) return ; 6 | return ( 7 |

8 | Hold on, fetching data may take some time :) 9 |

10 | ) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/components/Main.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { BrowserRouter, Route, Routes } from 'react-router-dom' 3 | import { Login } from './Login' 4 | import { Home } from './Home' 5 | import { Users } from './Users' 6 | 7 | export const Main = () => ( 8 | 9 | 10 | 11 | 12 | 13 | 14 | ) -------------------------------------------------------------------------------- /server/models/User.js: -------------------------------------------------------------------------------- 1 | import _sequelize from 'sequelize' 2 | const { Sequelize, Model, DataTypes } = _sequelize 3 | const sequelize = new Sequelize('sqlite::memory:', { logging: false }) 4 | 5 | export const User = sequelize.define('User', { 6 | email: { 7 | type: DataTypes.STRING, 8 | allowNull: false, 9 | primaryKey: true 10 | }, 11 | password: { 12 | type: DataTypes.STRING, 13 | allowNull: false 14 | }, 15 | token: { 16 | type: DataTypes.UUID 17 | } 18 | }, { 19 | sequelize, 20 | modelName: 'User' 21 | }) 22 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | An Application 6 | 7 | 11 | 15 | 16 | 17 |
18 | 19 | -------------------------------------------------------------------------------- /src/components/Home.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react' 2 | import { WithDataLoading } from './WithDataLoading' 3 | import { Users } from './Users' 4 | 5 | export const Home = () => { 6 | const Data = WithDataLoading(Users); 7 | const [appState, setAppState] = useState({ 8 | loading: false, 9 | users: null, 10 | }) 11 | 12 | useEffect(async () => { 13 | setAppState({ loading: true }) 14 | const response = await fetch('http://localhost:7777/users') 15 | const { users } = await response.json() 16 | 17 | setAppState({ loading: false, users }) 18 | }, [setAppState]) 19 | return 20 | } -------------------------------------------------------------------------------- /server/controllers/Users.js: -------------------------------------------------------------------------------- 1 | import faker from 'faker' 2 | import { User } from '../models/User.js' 3 | 4 | export async function createUsers (count) { 5 | await User.sync() 6 | for (let i = 0; i < count; i++) { 7 | await User.create({ 8 | email: faker.internet.email(), 9 | // TODO: se debe encriptar 10 | password: faker.internet.password() 11 | }) 12 | } 13 | } 14 | 15 | export async function getAll (page = 1, limit = 10) { 16 | console.log('[getAll]', page, limit) 17 | const users = await User.findAll() 18 | if (page < 1) { 19 | page = 1 20 | } 21 | if (page > users.length / limit) { 22 | page = Math.ceil(users.length / limit) 23 | } 24 | return { 25 | page, 26 | users: users.slice((page - 1) * limit, page * limit) 27 | } 28 | } 29 | 30 | export function get (email) { 31 | console.log('[get]', email) 32 | return User.findByPk(email) 33 | } 34 | 35 | export async function update (email, newUser) { 36 | console.log('[update]', email, newUser) 37 | await User.update(newUser, { where: { email} }) 38 | } 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TODO 2 | 3 | ## Backend 4 | - [x] Usar express 5 | - [x] Usar sequelize con SQlite 6 | - [x] Implementar endpoint `/login` 7 | - [x] Recibe email/contraseña 8 | - [x] Verifica password con tabla User 9 | - [x] Crea token aleatorio si coincide password 10 | - [x] Guarda token en tabla User 11 | - [x] Retorna JSON con token 12 | - [x] Crear fixture de 100 usuarios 13 | - [x] Enpoint `/users` 14 | - [x] devuelve lista de usuarios 15 | - [x] página lista de usuarios 16 | 17 | ## Forntend 18 | - [x] Usar [material-ui](https://mui.com/) 19 | - [x] Consumir endpoint `/login` 20 | - [ ] Usar react-redux (opcional) 21 | - [x] Si login Ok, mostrar lista de usuarios 22 | - [x] Fronend responsivo 23 | 24 | ## Uso 25 | 26 | 1. Descargar el repositorio 27 | 28 | ```bash 29 | $ git clone https://github.com/YerkoPalma/challenge-capta-hydro.git 30 | ``` 31 | 32 | 2. Instalar dependencias 33 | 34 | ```bash 35 | $ npm install 36 | ``` 37 | 38 | 3. Ejecutar en modo de desarrollo 39 | 40 | ```bash 41 | $ npm run dev 42 | ``` 43 | 44 | 4. Ejecutar en producción 45 | ```bash 46 | $ npm start 47 | ``` -------------------------------------------------------------------------------- /src/components/Users.jsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { DataGrid } from '@mui/x-data-grid' 3 | 4 | const columns = [ 5 | { field: 'id', headerName: 'ID' }, 6 | { field: 'email', headerName: 'Email', width: 250 }, 7 | { field: 'password', headerName: 'Password', width: 150 } 8 | ] 9 | 10 | export const Users = () => { 11 | const [loading, setLoading] = React.useState(false) 12 | const [rows, setRows] = React.useState([]) 13 | const [page, setPage] = React.useState(1) 14 | 15 | React.useEffect(async () => { 16 | setLoading(true) 17 | const response = await fetch(`http://localhost:7777/users?page=${page}`) 18 | const { users } = await response.json() 19 | setRows(users.map( 20 | (user, i) => { 21 | return { id: (page - 1) * 10 + i + 1, email: user.email, password: user.password } 22 | }) 23 | ) 24 | setLoading(false) 25 | }, [page]) 26 | 27 | const handlePageChange = (newPage) => { 28 | console.log('[handlePageChange]', newPage + 1) 29 | setPage(newPage + 1) 30 | } 31 | 32 | return ( 33 | 43 | ) 44 | } 45 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "challenge-capta-hydro", 3 | "version": "1.0.0", 4 | "description": "", 5 | "type": "module", 6 | "main": "index.js", 7 | "scripts": { 8 | "build": "react-scripts build", 9 | "start": "npm run build && NODE_ENV=production node server", 10 | "dev": "run-p start:dev start:server", 11 | "start:dev": "react-scripts start", 12 | "start:server": "node server" 13 | }, 14 | "author": "Yerko Palma", 15 | "license": "MIT", 16 | "dependencies": { 17 | "@emotion/react": "^11.7.0", 18 | "@emotion/styled": "^11.6.0", 19 | "@mui/icons-material": "^5.2.1", 20 | "@mui/material": "^5.2.3", 21 | "@mui/x-data-grid": "^5.2.0", 22 | "body-parser": "^1.19.0", 23 | "cors": "^2.8.5", 24 | "express": "^4.17.1", 25 | "react": "^17.0.2", 26 | "react-dom": "^17.0.2", 27 | "react-redux": "^7.2.6", 28 | "react-router": "^6.0.2", 29 | "react-router-dom": "^6.0.2", 30 | "redux": "^4.1.2", 31 | "sequelize": "^6.12.0-beta.1", 32 | "sqlite3": "^5.0.2" 33 | }, 34 | "devDependencies": { 35 | "faker": "^5.5.3", 36 | "npm-run-all": "^4.1.5", 37 | "react-scripts": "^4.0.3" 38 | }, 39 | "browserslist": { 40 | "production": [ 41 | ">0.2%", 42 | "not dead", 43 | "not op_mini all" 44 | ], 45 | "development": [ 46 | "last 1 chrome version", 47 | "last 1 firefox version", 48 | "last 1 safari version" 49 | ] 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /server/index.js: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | import cors from 'cors' 3 | import bodyParser from 'body-parser' 4 | import { randomUUID } from 'crypto' 5 | import { getAll, get, update, createUsers } from '../server/controllers/Users.js' 6 | import { fileURLToPath } from 'url' 7 | import path from 'path' 8 | 9 | const __filename = fileURLToPath(import.meta.url) 10 | const __dirname = path.dirname(__filename) 11 | 12 | let port = process.env.PORT || 7777 13 | let development = process.env.NODE_ENV !== 'production' 14 | let app = express() 15 | 16 | app.use( 17 | cors(), 18 | bodyParser.urlencoded({ extended: true }), 19 | bodyParser.json() 20 | ) 21 | app.listen(port, async () => { 22 | console.info('Server running, listening on port ', port) 23 | // genera usuarios falsos 24 | if (development) { 25 | await createUsers(100) 26 | console.info('Usuarios agregados') 27 | } 28 | }) 29 | 30 | if (process.env.NODE_ENV === `production`) { 31 | app.use(express.static(path.resolve(__dirname,'../build'))) 32 | app.get('/*',(req,res)=>{ 33 | res.sendFile(path.resolve(__dirname,'../build', 'index.html')) 34 | }) 35 | } 36 | 37 | app.post('/login', async (req, res) => { 38 | // recibe email/contraseña 39 | const { email, password } = req.body 40 | // valida en base de datos 41 | const user = await get(email) 42 | console.log(user) 43 | // error 404 en caso de no encontrar correo 44 | if (!user) { 45 | res.status(404).send() 46 | // error 401 en caso de encontrar correo y no coincidir contraseña 47 | } else if (user.password !== password) { 48 | res.status(401).send() 49 | // devuelve un token en caso de exito 50 | } else { 51 | const token = randomUUID() 52 | await update(email, { token }) 53 | res.status(200).send(token) 54 | } 55 | }) 56 | 57 | app.get('/users', async (req,res) => { 58 | // TODO: recibe token de usuario en Header de autenticación 59 | // TODO: si token esta correcto retorna lista de usuarios paginada 60 | const { page, limit } = req.query 61 | res.send(await getAll(page, limit)) 62 | // TODO: si esta incorrecto error 403 63 | }) 64 | -------------------------------------------------------------------------------- /src/components/Login.jsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { Navigate } from 'react-router-dom' 3 | import Button from '@mui/material/Button' 4 | import Container from '@mui/material/Container' 5 | import Box from '@mui/material/Box' 6 | import TextField from '@mui/material/TextField' 7 | import Stack from '@mui/material/Stack' 8 | import FormControl from '@mui/material/FormControl' 9 | import InputLabel from '@mui/material/InputLabel' 10 | import OutlinedInput from '@mui/material/OutlinedInput' 11 | import Snackbar from '@mui/material/Snackbar' 12 | import MuiAlert from '@mui/material/Alert' 13 | import InputAdornment from '@mui/material/InputAdornment' 14 | import IconButton from '@mui/material/IconButton' 15 | import Visibility from '@mui/icons-material/Visibility' 16 | import VisibilityOff from '@mui/icons-material/VisibilityOff' 17 | 18 | const Alert = React.forwardRef(function Alert(props, ref) { 19 | return 20 | }) 21 | 22 | export const Login = () => { 23 | const [open, setOpen] = React.useState(false) 24 | 25 | const handleClose = (event, reason) => { 26 | if (reason === 'clickaway') { 27 | return 28 | } 29 | 30 | setOpen(false) 31 | } 32 | 33 | const [values, setValues] = React.useState({ 34 | password: '', 35 | email: '', 36 | showPassword: false, 37 | redirect: '' 38 | }) 39 | 40 | const handleChange = (prop) => (event) => { 41 | setValues({ ...values, [prop]: event.target.value }) 42 | } 43 | 44 | const handleClickShowPassword = () => { 45 | setValues({ 46 | ...values, 47 | showPassword: !values.showPassword, 48 | }) 49 | } 50 | 51 | const handleMouseDownPassword = (event) => { 52 | event.preventDefault() 53 | } 54 | 55 | const handleSubmit = async (event) => { 56 | event.preventDefault() 57 | // React.useEffect(async () => { 58 | const response = await fetch('http://localhost:7777/login', { 59 | method: 'POST', 60 | headers: {'Content-Type': 'application/json'}, 61 | body: JSON.stringify({ 62 | email: values.email, 63 | password: values.password 64 | }) 65 | }) 66 | let token 67 | if (response.ok) { 68 | token = await response.text() 69 | // redirect to users page 70 | console.log(token) 71 | setValues({ 72 | ...values, 73 | redirect: '/home' 74 | }) 75 | } else { 76 | setOpen(true) 77 | } 78 | // }) 79 | } 80 | 81 | if (values.redirect) { 82 | return 83 | } 84 | return ( 85 | 86 | 87 | 88 | 89 | 90 | Password 91 | 98 | 104 | {values.showPassword ? : } 105 | 106 | 107 | } 108 | label="Password" 109 | /> 110 | 111 | 112 | 113 | 114 | 115 | 116 | Usuario y contraseña incorrectas 117 | 118 | 119 | 120 | ) 121 | } --------------------------------------------------------------------------------