├── .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 | }
--------------------------------------------------------------------------------