Autenticação

Open in GitHub Open in Codespaces

Arquivos
invest-app-auth
├── package-lock.json
├── package.json
├── prisma
│   ├── dev.db
│   ├── migrations
│   │   ├── 20230826214323_init
│   │   │   └── migration.sql
│   │   ├── 20230902014613_create_category
│   │   │   └── migration.sql
│   │   ├── 20230902065351_create_user
│   │   │   └── migration.sql
│   │   ├── 20230902111235_create_investment_broker_interest_created_at
│   │   │   └── migration.sql
│   │   ├── 20230902131930_create_investment_user_cascade
│   │   │   └── migration.sql
│   │   └── migration_lock.toml
│   ├── schema.prisma
│   ├── seed.js
│   └── seeders.json
├── public
│   ├── css
│   │   └── style.css
│   ├── home.html
│   ├── js
│   │   ├── home.js
│   │   ├── lib
│   │   │   ├── auth.js
│   │   │   └── format.js
│   │   ├── services
│   │   │   └── api.js
│   │   ├── signin.js
│   │   └── signup.js
│   ├── signin.html
│   └── signup.html
├── requests.http
└── src
    ├── database
    │   └── database.js
    ├── index.js
    ├── middleware
    │   └── auth.js
    ├── models
    │   ├── Category.js
    │   ├── Investment.js
    │   └── User.js
    └── routes.js
Arquivos
invest-app-auth
├── package-lock.json
├── package.json
├── prisma
│   ├── dev.db
│   ├── migrations
│   │   ├── 20230826214323_init
│   │   │   └── migration.sql
│   │   ├── 20230902014613_create_category
│   │   │   └── migration.sql
│   │   ├── 20230902065351_create_user
│   │   │   └── migration.sql
│   │   ├── 20230902111235_create_investment_broker_interest_created_at
│   │   │   └── migration.sql
│   │   ├── 20230902131930_create_investment_user_cascade
│   │   │   └── migration.sql
│   │   └── migration_lock.toml
│   ├── schema.prisma
│   ├── seed.js
│   └── seeders.json
├── public
│   ├── css
│   │   └── style.css
│   ├── home.html
│   ├── js
│   │   ├── home.js
│   │   ├── lib
│   │   │   ├── auth.js
│   │   │   └── format.js
│   │   ├── services
│   │   │   └── api.js
│   │   ├── signin.js
│   │   └── signup.js
│   ├── signin.html
│   └── signup.html
├── requests.http
└── src
    ├── database
    │   └── database.js
    ├── index.js
    ├── middleware
    │   └── auth.js
    ├── models
    │   ├── Category.js
    │   ├── Investment.js
    │   └── User.js
    └── routes.js

JSON Web Token (JWT)

Fluxo de Requisição:

jsonwebtoken:

$ npm install jsonwebtoken
$ npm install jsonwebtoken

Encode: jwt.sign(payload , privateKey, options)

jwt.io debugger:

Encode JWT
jwt.sign(
  { userId: 1 },
  'secret'
);
//=> eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjEsImlhdCI6MTcyODc0NjY0M30.C1dDgnIRfWpJnqir_hyuVp_fnt7vpR6mKgHMdZoG4Rk
 
//=> Header:    eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
//=> Payload:   eyJ1c2VySWQiOjEsImlhdCI6MTcyODc0NjY0M30
//=> Signature: C1dDgnIRfWpJnqir_hyuVp_fnt7vpR6mKgHMdZoG4Rk
 
// base64 decoded
//=> Header:     {"alg":"HS256","typ":"JWT"}
//=> Payload:    {"userId":1,"iat":1728746643}
Encode JWT
jwt.sign(
  { userId: 1 },
  'secret'
);
//=> eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjEsImlhdCI6MTcyODc0NjY0M30.C1dDgnIRfWpJnqir_hyuVp_fnt7vpR6mKgHMdZoG4Rk
 
//=> Header:    eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
//=> Payload:   eyJ1c2VySWQiOjEsImlhdCI6MTcyODc0NjY0M30
//=> Signature: C1dDgnIRfWpJnqir_hyuVp_fnt7vpR6mKgHMdZoG4Rk
 
// base64 decoded
//=> Header:     {"alg":"HS256","typ":"JWT"}
//=> Payload:    {"userId":1,"iat":1728746643}

jwt.io debugger:

Encode JWT
jwt.sign(
  { userId: 1 },
  'secret',
  { expiresIn: 3600000 } // 1h
);
//=> eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjEsImlhdCI6MTcyODc0Njc4NiwiZXhwIjoxNzI4NzUwMzg2fQ.V1eHtVYWaI5Rji8wd4onYIGqdTGlm6NAmUeIiw6G7Gw
 
//=> Header:    eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
//=> Payload:   eyJ1c2VySWQiOjEsImlhdCI6MTcyODc0Njc4NiwiZXhwIjoxNzI4NzUwMzg2fQ
//=> Signature: V1eHtVYWaI5Rji8wd4onYIGqdTGlm6NAmUeIiw6G7Gw
 
// base64 decoded
//=> Header:     {"alg":"HS256","typ":"JWT"}
//=> Payload:    {"userId":1,"iat":1728746786,"exp":1728750386}
Encode JWT
jwt.sign(
  { userId: 1 },
  'secret',
  { expiresIn: 3600000 } // 1h
);
//=> eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjEsImlhdCI6MTcyODc0Njc4NiwiZXhwIjoxNzI4NzUwMzg2fQ.V1eHtVYWaI5Rji8wd4onYIGqdTGlm6NAmUeIiw6G7Gw
 
//=> Header:    eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
//=> Payload:   eyJ1c2VySWQiOjEsImlhdCI6MTcyODc0Njc4NiwiZXhwIjoxNzI4NzUwMzg2fQ
//=> Signature: V1eHtVYWaI5Rji8wd4onYIGqdTGlm6NAmUeIiw6G7Gw
 
// base64 decoded
//=> Header:     {"alg":"HS256","typ":"JWT"}
//=> Payload:    {"userId":1,"iat":1728746786,"exp":1728750386}

jwt.io debugger:

Encode JWT
jwt.sign(
  { userId: 1 },
  'abc',
  { expiresIn: '1h' }
);
//=> eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjEsImlhdCI6MTcyODc0ODQ5NywiZXhwIjoxNzI4NzUyMDk3fQ.0m_bFbC337WRu0bqlabUJ1hN-hNwOXVJUHKBbSdmz7s
 
//=> Header:    eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
//=> Payload:   eyJ1c2VySWQiOjEsImlhdCI6MTcyODc0ODQ5NywiZXhwIjoxNzI4NzUyMDk3fQ
//=> Signature: 0m_bFbC337WRu0bqlabUJ1hN-hNwOXVJUHKBbSdmz7s
 
// base64 decoded
//=> Header:     {"alg":"HS256","typ":"JWT"}
//=> Payload:    {"userId":1,"iat":1728748497,"exp":1728752097}
Encode JWT
jwt.sign(
  { userId: 1 },
  'abc',
  { expiresIn: '1h' }
);
//=> eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjEsImlhdCI6MTcyODc0ODQ5NywiZXhwIjoxNzI4NzUyMDk3fQ.0m_bFbC337WRu0bqlabUJ1hN-hNwOXVJUHKBbSdmz7s
 
//=> Header:    eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
//=> Payload:   eyJ1c2VySWQiOjEsImlhdCI6MTcyODc0ODQ5NywiZXhwIjoxNzI4NzUyMDk3fQ
//=> Signature: 0m_bFbC337WRu0bqlabUJ1hN-hNwOXVJUHKBbSdmz7s
 
// base64 decoded
//=> Header:     {"alg":"HS256","typ":"JWT"}
//=> Payload:    {"userId":1,"iat":1728748497,"exp":1728752097}

Observações:

Decode: jwt.verify(token, privateKey)

jwt.io - signature verified:

Decode JWT
jwt.verify(
  'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjEsImlhdCI6MTcyODc0NjY0M30.C1dDgnIRfWpJnqir_hyuVp_fnt7vpR6mKgHMdZoG4Rk',
  'secret'
);
//=> { userId: 1, iat: 1728746643 }
Decode JWT
jwt.verify(
  'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjEsImlhdCI6MTcyODc0NjY0M30.C1dDgnIRfWpJnqir_hyuVp_fnt7vpR6mKgHMdZoG4Rk',
  'secret'
);
//=> { userId: 1, iat: 1728746643 }

jwt.io - invalid signature:

Decode JWT
jwt.verify(
  'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjEsImlhdCI6MTcyODc0NjY0M30.C1dDgnIRfWpJnqir_hyuVp_fnt7vpR6mKgHMdZoG4Rk',
  'wrong'
);
//=> Uncaught JsonWebTokenError: invalid signature
Decode JWT
jwt.verify(
  'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjEsImlhdCI6MTcyODc0NjY0M30.C1dDgnIRfWpJnqir_hyuVp_fnt7vpR6mKgHMdZoG4Rk',
  'wrong'
);
//=> Uncaught JsonWebTokenError: invalid signature

jwt.io - signature verified:

Decode JWT
jwt.verify(
  'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjEsImlhdCI6MTcyODc0Njc4NiwiZXhwIjoxNzI4NzUwMzg2fQ.V1eHtVYWaI5Rji8wd4onYIGqdTGlm6NAmUeIiw6G7Gw',
  'secret'
);
//=> { userId: 1, iat: 1728746786, exp: 1728750386 }
Decode JWT
jwt.verify(
  'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjEsImlhdCI6MTcyODc0Njc4NiwiZXhwIjoxNzI4NzUwMzg2fQ.V1eHtVYWaI5Rji8wd4onYIGqdTGlm6NAmUeIiw6G7Gw',
  'secret'
);
//=> { userId: 1, iat: 1728746786, exp: 1728750386 }

Sabendo que o privateKey é secret, gere um novo token para o usuário de id igual a 2 com 1h de expiração.

Middleware

Express -> Middleware -> Router
Express -> Middleware -> Router
/codes/expressjs/invest-app-auth/src/middleware/auth.js
import jwt from 'jsonwebtoken';
 
function isAuthenticated(req, res, next) {
  try {
    const { authorization } = req.headers;
 
    const [, token] = authorization.split(' ');
 
    const { userId } = jwt.verify(token, process.env.JWT_SECRET);
 
    req.userId = userId;
 
    next();
  } catch (error) {
    res.status(401).send({ auth: false, message: 'Token invalid.' });
  }
}
 
export { isAuthenticated };
 
/codes/expressjs/invest-app-auth/src/middleware/auth.js
import jwt from 'jsonwebtoken';
 
function isAuthenticated(req, res, next) {
  try {
    const { authorization } = req.headers;
 
    const [, token] = authorization.split(' ');
 
    const { userId } = jwt.verify(token, process.env.JWT_SECRET);
 
    req.userId = userId;
 
    next();
  } catch (error) {
    res.status(401).send({ auth: false, message: 'Token invalid.' });
  }
}
 
export { isAuthenticated };
 
/codes/expressjs/invest-app-auth/.env.example
# Environment variables declared in this file are automatically made available to Prisma.
# See the documentation for more detail: https://pris.ly/d/prisma-schema#accessing-environment-variables-from-the-schema

# Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB.
# See the documentation for all the connection string options: https://pris.ly/d/connection-strings

DATABASE_URL="file:./dev.db"

# JWT
JWT_SECRET=abc

# BCRYPT
BCRYPT_SALT=10
/codes/expressjs/invest-app-auth/.env.example
# Environment variables declared in this file are automatically made available to Prisma.
# See the documentation for more detail: https://pris.ly/d/prisma-schema#accessing-environment-variables-from-the-schema

# Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB.
# See the documentation for all the connection string options: https://pris.ly/d/connection-strings

DATABASE_URL="file:./dev.db"

# JWT
JWT_SECRET=abc

# BCRYPT
BCRYPT_SALT=10
$ cp .env.example .env
$ cp .env.example .env

Router

/codes/expressjs/invest-app-auth/src/routes.js
import express from 'express';
import jwt from 'jsonwebtoken';
import bcrypt from 'bcrypt';
 
import { isAuthenticated } from './middleware/auth.js';
 
import Category from './models/Category.js';
import Investment from './models/Investment.js';
import User from './models/User.js';
 
class HTTPError extends Error {
  constructor(message, code) {
    super(message);
    this.code = code;
  }
}
 
const router = express.Router();
 
router.post('/investments', isAuthenticated, async (req, res) => {
  try {
    const investment = req.body;
 
    if (investment.createdAt) {
      investment.createdAt = new Date(
        investment.createdAt + 'T00:00:00-03:00'
      ).toISOString();
    }
 
    const userId = req.userId;
 
    const createdInvestment = await Investment.create({
      ...investment,
      userId,
    });
 
    return res.json(createdInvestment);
  } catch (error) {
    console.error(error.stack);
    throw new HTTPError('Unable to create investment', 400);
  }
});
 
router.get('/investments', isAuthenticated, async (req, res) => {
  try {
    const { name } = req.query;
 
    const userId = req.userId;
 
    let investments;
 
    if (name) {
      investments = await Investment.read({ name, userId });
    } else {
      investments = await Investment.read({ userId });
    }
 
    return res.json(investments);
  } catch (error) {
    throw new HTTPError('Unable to read investments', 400);
  }
});
 
router.get('/investments/:id', isAuthenticated, async (req, res) => {
  try {
    const id = req.params.id;
 
    const userId = req.userId;
 
    const investment = await Investment.readById(id, { userId });
 
    return res.json(investment);
  } catch (error) {
    throw new HTTPError('Unable to find investment', 400);
  }
});
 
router.put('/investments/:id', isAuthenticated, async (req, res) => {
  try {
    const investment = req.body;
 
    if (investment.createdAt) {
      investment.createdAt = new Date(
        investment.createdAt + 'T00:00:00-03:00'
      ).toISOString();
    }
 
    const id = req.params.id;
 
    const userId = req.userId;
 
    const updatedInvestment = await Investment.update({
      ...investment,
      id,
      userId,
    });
 
    return res.json(updatedInvestment);
  } catch (error) {
    console.error(error.stack);
    throw new HTTPError('Unable to update investment', 400);
  }
});
 
router.delete('/investments/:id', isAuthenticated, async (req, res) => {
  try {
    const id = req.params.id;
 
    if (await Investment.remove(id)) {
      return res.sendStatus(204);
    } else {
      throw new Error();
    }
  } catch (error) {
    throw new HTTPError('Unable to remove investment', 400);
  }
});
 
router.get('/categories', isAuthenticated, async (req, res) => {
  try {
    const { name } = req.query;
 
    let categories;
 
    if (name) {
      categories = await Category.read({ name });
    } else {
      categories = await Category.read();
    }
 
    return res.json(categories);
  } catch (error) {
    throw new HTTPError('Unable to read investments', 400);
  }
});
 
router.post('/users', async (req, res) => {
  try {
    const user = req.body;
 
    delete user.confirmationPassword;
 
    const newUser = await User.create(user);
 
    delete newUser.password;
 
    res.status(201).json(newUser);
  } catch (error) {
    throw new HTTPError('Unable to create user', 400);
  }
});
 
router.get('/users/me', isAuthenticated, async (req, res) => {
  try {
    const userId = req.userId;
 
    const user = await User.readById(userId);
 
    delete user.password;
 
    return res.json(user);
  } catch (error) {
    throw new HTTPError('Unable to find user', 400);
  }
});
 
router.post('/signin', async (req, res) => {
  try {
    const { email, password } = req.body;
 
    const { id: userId, password: hash } = await User.read({ email });
 
    const match = await bcrypt.compare(password, hash);
 
    if (match) {
      const token = jwt.sign(
        { userId },
        process.env.JWT_SECRET,
        { expiresIn: 3600000 } // 1h
      );
 
      return res.json({ auth: true, token });
    } else {
      throw new Error('User not found');
    }
  } catch (error) {
    res.status(401).json({ error: 'User not found' });
  }
});
 
// 404 handler
router.use((req, res, next) => {
  return res.status(404).json({ message: 'Content not found!' });
});
 
// Error handler
router.use((error, req, res, next) => {
  // console.error(error.stack);
  if (error instanceof HTTPError) {
    return res.status(error.code).json({ message: error.message });
  } else {
    return res.status(500).json({ message: 'Something broke!' });
  }
});
 
export default router;
 
/codes/expressjs/invest-app-auth/src/routes.js
import express from 'express';
import jwt from 'jsonwebtoken';
import bcrypt from 'bcrypt';
 
import { isAuthenticated } from './middleware/auth.js';
 
import Category from './models/Category.js';
import Investment from './models/Investment.js';
import User from './models/User.js';
 
class HTTPError extends Error {
  constructor(message, code) {
    super(message);
    this.code = code;
  }
}
 
const router = express.Router();
 
router.post('/investments', isAuthenticated, async (req, res) => {
  try {
    const investment = req.body;
 
    if (investment.createdAt) {
      investment.createdAt = new Date(
        investment.createdAt + 'T00:00:00-03:00'
      ).toISOString();
    }
 
    const userId = req.userId;
 
    const createdInvestment = await Investment.create({
      ...investment,
      userId,
    });
 
    return res.json(createdInvestment);
  } catch (error) {
    console.error(error.stack);
    throw new HTTPError('Unable to create investment', 400);
  }
});
 
router.get('/investments', isAuthenticated, async (req, res) => {
  try {
    const { name } = req.query;
 
    const userId = req.userId;
 
    let investments;
 
    if (name) {
      investments = await Investment.read({ name, userId });
    } else {
      investments = await Investment.read({ userId });
    }
 
    return res.json(investments);
  } catch (error) {
    throw new HTTPError('Unable to read investments', 400);
  }
});
 
router.get('/investments/:id', isAuthenticated, async (req, res) => {
  try {
    const id = req.params.id;
 
    const userId = req.userId;
 
    const investment = await Investment.readById(id, { userId });
 
    return res.json(investment);
  } catch (error) {
    throw new HTTPError('Unable to find investment', 400);
  }
});
 
router.put('/investments/:id', isAuthenticated, async (req, res) => {
  try {
    const investment = req.body;
 
    if (investment.createdAt) {
      investment.createdAt = new Date(
        investment.createdAt + 'T00:00:00-03:00'
      ).toISOString();
    }
 
    const id = req.params.id;
 
    const userId = req.userId;
 
    const updatedInvestment = await Investment.update({
      ...investment,
      id,
      userId,
    });
 
    return res.json(updatedInvestment);
  } catch (error) {
    console.error(error.stack);
    throw new HTTPError('Unable to update investment', 400);
  }
});
 
router.delete('/investments/:id', isAuthenticated, async (req, res) => {
  try {
    const id = req.params.id;
 
    if (await Investment.remove(id)) {
      return res.sendStatus(204);
    } else {
      throw new Error();
    }
  } catch (error) {
    throw new HTTPError('Unable to remove investment', 400);
  }
});
 
router.get('/categories', isAuthenticated, async (req, res) => {
  try {
    const { name } = req.query;
 
    let categories;
 
    if (name) {
      categories = await Category.read({ name });
    } else {
      categories = await Category.read();
    }
 
    return res.json(categories);
  } catch (error) {
    throw new HTTPError('Unable to read investments', 400);
  }
});
 
router.post('/users', async (req, res) => {
  try {
    const user = req.body;
 
    delete user.confirmationPassword;
 
    const newUser = await User.create(user);
 
    delete newUser.password;
 
    res.status(201).json(newUser);
  } catch (error) {
    throw new HTTPError('Unable to create user', 400);
  }
});
 
router.get('/users/me', isAuthenticated, async (req, res) => {
  try {
    const userId = req.userId;
 
    const user = await User.readById(userId);
 
    delete user.password;
 
    return res.json(user);
  } catch (error) {
    throw new HTTPError('Unable to find user', 400);
  }
});
 
router.post('/signin', async (req, res) => {
  try {
    const { email, password } = req.body;
 
    const { id: userId, password: hash } = await User.read({ email });
 
    const match = await bcrypt.compare(password, hash);
 
    if (match) {
      const token = jwt.sign(
        { userId },
        process.env.JWT_SECRET,
        { expiresIn: 3600000 } // 1h
      );
 
      return res.json({ auth: true, token });
    } else {
      throw new Error('User not found');
    }
  } catch (error) {
    res.status(401).json({ error: 'User not found' });
  }
});
 
// 404 handler
router.use((req, res, next) => {
  return res.status(404).json({ message: 'Content not found!' });
});
 
// Error handler
router.use((error, req, res, next) => {
  // console.error(error.stack);
  if (error instanceof HTTPError) {
    return res.status(error.code).json({ message: error.message });
  } else {
    return res.status(500).json({ message: 'Something broke!' });
  }
});
 
export default router;
 
/codes/expressjs/invest-app-auth/requests.http
@host = http://localhost:3000/api
@createdInvestmentId = {{createdInvestment.response.body.$.id}}
@token = {{signin.response.body.$.token}}
 
### Create User
 
# @name createdUser
POST {{host}}/users
Content-Type: application/json
 
{
  "name": "Luiz",
  "email": "luiz@email.com",
  "password": "12345678"
}
 
### Signin 401
 
# @name signin
POST {{host}}/signin
Content-Type: application/json
 
{
  "email": "luiz@email.com",
  "password": "321"
}
 
### Signin Ok
 
# @name signin
POST {{host}}/signin
Content-Type: application/json
 
{
  "email": "luiz@email.com",
  "password": "12345678"
}
 
### Read User (whitout token)
 
GET {{host}}/users/me
 
### Read User (with token)
 
GET {{host}}/users/me
Authorization: bearer {{token}}
 
### Read Categories
 
GET {{host}}/categories
Authorization: bearer {{token}}
 
### Read Category Pós
 
# @name categoryPos
GET {{host}}/categories?name=Pós
Authorization: bearer {{token}}
 
### Read Category IPCA
 
# @name categoryIpca
GET {{host}}/categories?name=IPCA
Authorization: bearer {{token}}
 
### Create Investment
 
@categoryIpcaId = {{categoryIpca.response.body.$.id}}
 
# @name createdInvestment
POST {{host}}/investments
Authorization: bearer {{token}}
Content-Type: application/json
 
{
  "name": "Tesouro Selic 2029",
  "value": 10000,
  "interest": "100% Selic",
  "createdAt": "2023-09-01",
  "broker": "Banco Inter",
  "categoryId": "{{categoryIpcaId}}"
}
 
### Read Investments
 
GET {{host}}/investments
Authorization: bearer {{token}}
 
### Read Investments by Name
 
GET {{host}}/investments?name=Tesouro
Authorization: bearer {{token}}
 
### Read Investments by Id
 
GET {{host}}/investments/{{createdInvestmentId}}
Authorization: bearer {{token}}
 
### Update Investment
 
@categoryPosId = {{categoryPos.response.body.$.id}}
 
PUT {{host}}/investments/{{createdInvestmentId}}
Authorization: bearer {{token}}
Content-Type: application/json
 
{
  "name": "Tesouro Selic 2029",
  "value": 20000,
  "interest": "100% Selic",
  "createdAt": "2023-09-01",
  "broker": "Banco Inter",
  "categoryId": "{{categoryPosId}}"
}
 
### Delete Investment
 
DELETE {{host}}/investments/{{createdInvestmentId}}
Authorization: bearer {{token}}
 
 
/codes/expressjs/invest-app-auth/requests.http
@host = http://localhost:3000/api
@createdInvestmentId = {{createdInvestment.response.body.$.id}}
@token = {{signin.response.body.$.token}}
 
### Create User
 
# @name createdUser
POST {{host}}/users
Content-Type: application/json
 
{
  "name": "Luiz",
  "email": "luiz@email.com",
  "password": "12345678"
}
 
### Signin 401
 
# @name signin
POST {{host}}/signin
Content-Type: application/json
 
{
  "email": "luiz@email.com",
  "password": "321"
}
 
### Signin Ok
 
# @name signin
POST {{host}}/signin
Content-Type: application/json
 
{
  "email": "luiz@email.com",
  "password": "12345678"
}
 
### Read User (whitout token)
 
GET {{host}}/users/me
 
### Read User (with token)
 
GET {{host}}/users/me
Authorization: bearer {{token}}
 
### Read Categories
 
GET {{host}}/categories
Authorization: bearer {{token}}
 
### Read Category Pós
 
# @name categoryPos
GET {{host}}/categories?name=Pós
Authorization: bearer {{token}}
 
### Read Category IPCA
 
# @name categoryIpca
GET {{host}}/categories?name=IPCA
Authorization: bearer {{token}}
 
### Create Investment
 
@categoryIpcaId = {{categoryIpca.response.body.$.id}}
 
# @name createdInvestment
POST {{host}}/investments
Authorization: bearer {{token}}
Content-Type: application/json
 
{
  "name": "Tesouro Selic 2029",
  "value": 10000,
  "interest": "100% Selic",
  "createdAt": "2023-09-01",
  "broker": "Banco Inter",
  "categoryId": "{{categoryIpcaId}}"
}
 
### Read Investments
 
GET {{host}}/investments
Authorization: bearer {{token}}
 
### Read Investments by Name
 
GET {{host}}/investments?name=Tesouro
Authorization: bearer {{token}}
 
### Read Investments by Id
 
GET {{host}}/investments/{{createdInvestmentId}}
Authorization: bearer {{token}}
 
### Update Investment
 
@categoryPosId = {{categoryPos.response.body.$.id}}
 
PUT {{host}}/investments/{{createdInvestmentId}}
Authorization: bearer {{token}}
Content-Type: application/json
 
{
  "name": "Tesouro Selic 2029",
  "value": 20000,
  "interest": "100% Selic",
  "createdAt": "2023-09-01",
  "broker": "Banco Inter",
  "categoryId": "{{categoryPosId}}"
}
 
### Delete Investment
 
DELETE {{host}}/investments/{{createdInvestmentId}}
Authorization: bearer {{token}}
 
 

View

Login:

/codes/expressjs/invest-app-auth/public/signin.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>Cadastro de Usuário</title>
    <link
      href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css"
      rel="stylesheet"
      integrity="sha384-GLhlTQ8iRABdZLl6O3oVMWSktQOp6b7In1Zl3/Jr59b6EGGoI1aFkw7cmDA6j6gD"
      crossorigin="anonymous"
    />
  </head>
 
  <body>
    <nav class="navbar navbar-expand-lg bg-light">
      <div class="container">
        <a class="navbar-brand" href="#">Invest App</a>
        <button
          class="navbar-toggler"
          type="button"
          data-bs-toggle="collapse"
          data-bs-target="#navbarSupportedContent"
          aria-controls="navbarSupportedContent"
          aria-expanded="false"
          aria-label="Toggle navigation"
        >
          <span class="navbar-toggler-icon"></span>
        </button>
        <div class="collapse navbar-collapse" id="navbarSupportedContent">
          <ul class="navbar-nav me-auto mb-2 mb-lg-0">
            <li class="nav-item">
              <a class="nav-link active" aria-current="page" href="/signin.html"
                >Entrar</a
              >
            </li>
            <li class="nav-item">
              <a class="nav-link" href="/signup.html">Cadastrar</a>
            </li>
          </ul>
        </div>
      </div>
    </nav>
    <div class="container pb-5">
      <h1 class="text-center my-5 fw-bold">Entrar</h1>
 
      <div class="row justify-content-center">
        <form
          class="col-6 needs-validation"
          onsubmit="handleSubmit(event)"
          novalidate
        >
          <div class="mb-3">
            <label for="email" class="form-label">Email</label>
            <input
              type="email"
              class="form-control"
              id="email"
              name="email"
              required
            />
            <div class="invalid-feedback">Informe o email do usuário.</div>
          </div>
          <div class="mb-3">
            <label for="password" class="form-label">Senha</label>
            <!-- pattern="(?=.*[A-Z])(?=.*[!@#$&*])(?=.*[0-9])(?=.*[a-z]).{8}" -->
            <input
              type="password"
              class="form-control"
              id="password"
              name="password"
              minlength="8"
              required
            />
            <div class="invalid-feedback">
              Informe a senha com 8 caracteres.
            </div>
          </div>
          <div class="mb-3">
            <input type="submit" class="btn btn-primary" value="Entrar" />
          </div>
        </form>
      </div>
    </div>
 
    <script
      src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.bundle.min.js"
      integrity="sha384-w76AqPfDkMBDXo30jS1Sgez6pr3x5MlQ1ZAGC+nuZB+EYdgRZgiwxhTBTkF7CXvN"
      crossorigin="anonymous"
    ></script>
    <script src="js/signin.js" type="module"></script>
  </body>
</html>
 
/codes/expressjs/invest-app-auth/public/signin.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>Cadastro de Usuário</title>
    <link
      href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css"
      rel="stylesheet"
      integrity="sha384-GLhlTQ8iRABdZLl6O3oVMWSktQOp6b7In1Zl3/Jr59b6EGGoI1aFkw7cmDA6j6gD"
      crossorigin="anonymous"
    />
  </head>
 
  <body>
    <nav class="navbar navbar-expand-lg bg-light">
      <div class="container">
        <a class="navbar-brand" href="#">Invest App</a>
        <button
          class="navbar-toggler"
          type="button"
          data-bs-toggle="collapse"
          data-bs-target="#navbarSupportedContent"
          aria-controls="navbarSupportedContent"
          aria-expanded="false"
          aria-label="Toggle navigation"
        >
          <span class="navbar-toggler-icon"></span>
        </button>
        <div class="collapse navbar-collapse" id="navbarSupportedContent">
          <ul class="navbar-nav me-auto mb-2 mb-lg-0">
            <li class="nav-item">
              <a class="nav-link active" aria-current="page" href="/signin.html"
                >Entrar</a
              >
            </li>
            <li class="nav-item">
              <a class="nav-link" href="/signup.html">Cadastrar</a>
            </li>
          </ul>
        </div>
      </div>
    </nav>
    <div class="container pb-5">
      <h1 class="text-center my-5 fw-bold">Entrar</h1>
 
      <div class="row justify-content-center">
        <form
          class="col-6 needs-validation"
          onsubmit="handleSubmit(event)"
          novalidate
        >
          <div class="mb-3">
            <label for="email" class="form-label">Email</label>
            <input
              type="email"
              class="form-control"
              id="email"
              name="email"
              required
            />
            <div class="invalid-feedback">Informe o email do usuário.</div>
          </div>
          <div class="mb-3">
            <label for="password" class="form-label">Senha</label>
            <!-- pattern="(?=.*[A-Z])(?=.*[!@#$&*])(?=.*[0-9])(?=.*[a-z]).{8}" -->
            <input
              type="password"
              class="form-control"
              id="password"
              name="password"
              minlength="8"
              required
            />
            <div class="invalid-feedback">
              Informe a senha com 8 caracteres.
            </div>
          </div>
          <div class="mb-3">
            <input type="submit" class="btn btn-primary" value="Entrar" />
          </div>
        </form>
      </div>
    </div>
 
    <script
      src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.bundle.min.js"
      integrity="sha384-w76AqPfDkMBDXo30jS1Sgez6pr3x5MlQ1ZAGC+nuZB+EYdgRZgiwxhTBTkF7CXvN"
      crossorigin="anonymous"
    ></script>
    <script src="js/signin.js" type="module"></script>
  </body>
</html>
 
/codes/expressjs/invest-app-auth/public/js/signin.js
import API from './services/api.js';
import Auth from './lib/auth.js';
 
const form = document.querySelector('form');
 
window.handleSubmit = handleSubmit;
 
async function handleSubmit(event) {
  event.preventDefault();
 
  const user = Object.fromEntries(new FormData(form));
 
  const { auth, token } = await API.create('/signin', user, false);
 
  if (auth) {
    Auth.signin(token);
  } else {
    console.log('Error no login');
  }
}
 
/codes/expressjs/invest-app-auth/public/js/signin.js
import API from './services/api.js';
import Auth from './lib/auth.js';
 
const form = document.querySelector('form');
 
window.handleSubmit = handleSubmit;
 
async function handleSubmit(event) {
  event.preventDefault();
 
  const user = Object.fromEntries(new FormData(form));
 
  const { auth, token } = await API.create('/signin', user, false);
 
  if (auth) {
    Auth.signin(token);
  } else {
    console.log('Error no login');
  }
}
 
/codes/expressjs/invest-app-auth/public/js/services/api.js
import Auth from '../lib/auth.js';
 
const domain = '/api';
 
async function create(resource, data, auth = true) {
  const url = `${domain}${resource}`;
 
  const config = {
    method: 'POST',
    mode: 'cors',
    body: JSON.stringify(data),
    headers: {
      'Content-Type': 'application/json; charset=UTF-8',
    },
  };
 
  if (auth) {
    config.headers.Authorization = `Bearer ${Auth.getToken()}`;
  }
 
  const res = await fetch(url, config);
 
  if (res.status === 401) {
    Auth.signout();
  }
 
  return await res.json();
}
 
async function read(resource) {
  const url = `${domain}${resource}`;
 
  const config = {
    method: 'get',
    headers: {
      Authorization: `Bearer ${Auth.getToken()}`,
    },
  };
 
  const res = await fetch(url, config);
 
  if (res.status === 401) {
    Auth.signout();
  }
 
  return await res.json();
}
 
async function update(resource, data) {
  const url = `${domain}${resource}`;
 
  const config = {
    method: 'PUT',
    mode: 'cors',
    body: JSON.stringify(data),
    headers: {
      'Content-Type': 'application/json; charset=UTF-8',
      Authorization: `Bearer ${Auth.getToken()}`,
    },
  };
 
  const res = await fetch(url, config);
 
  if (res.status === 401) {
    Auth.signout();
  }
 
  return await res.json();
}
 
async function remove(resource) {
  const url = `${domain}${resource}`;
 
  const config = {
    method: 'DELETE',
    mode: 'cors',
    headers: {
      Authorization: `Bearer ${Auth.getToken()}`,
    },
  };
 
  const res = await fetch(url, config);
 
  if (res.status === 401) {
    Auth.signout();
  }
 
  return true;
}
 
export default { create, read, update, remove };
 
/codes/expressjs/invest-app-auth/public/js/services/api.js
import Auth from '../lib/auth.js';
 
const domain = '/api';
 
async function create(resource, data, auth = true) {
  const url = `${domain}${resource}`;
 
  const config = {
    method: 'POST',
    mode: 'cors',
    body: JSON.stringify(data),
    headers: {
      'Content-Type': 'application/json; charset=UTF-8',
    },
  };
 
  if (auth) {
    config.headers.Authorization = `Bearer ${Auth.getToken()}`;
  }
 
  const res = await fetch(url, config);
 
  if (res.status === 401) {
    Auth.signout();
  }
 
  return await res.json();
}
 
async function read(resource) {
  const url = `${domain}${resource}`;
 
  const config = {
    method: 'get',
    headers: {
      Authorization: `Bearer ${Auth.getToken()}`,
    },
  };
 
  const res = await fetch(url, config);
 
  if (res.status === 401) {
    Auth.signout();
  }
 
  return await res.json();
}
 
async function update(resource, data) {
  const url = `${domain}${resource}`;
 
  const config = {
    method: 'PUT',
    mode: 'cors',
    body: JSON.stringify(data),
    headers: {
      'Content-Type': 'application/json; charset=UTF-8',
      Authorization: `Bearer ${Auth.getToken()}`,
    },
  };
 
  const res = await fetch(url, config);
 
  if (res.status === 401) {
    Auth.signout();
  }
 
  return await res.json();
}
 
async function remove(resource) {
  const url = `${domain}${resource}`;
 
  const config = {
    method: 'DELETE',
    mode: 'cors',
    headers: {
      Authorization: `Bearer ${Auth.getToken()}`,
    },
  };
 
  const res = await fetch(url, config);
 
  if (res.status === 401) {
    Auth.signout();
  }
 
  return true;
}
 
export default { create, read, update, remove };
 
/codes/expressjs/invest-app-auth/public/js/lib/auth.js
function isAuthenticated() {
  if (!getToken()) {
    window.location.href = '/signin.html';
  } else {
    return true;
  }
}
 
function getToken() {
  return localStorage.getItem('@invest-app:token');
}
 
function signin(token) {
  localStorage.setItem('@invest-app:token', token);
 
  window.location.href = '/home.html';
}
 
function signout() {
  localStorage.removeItem('@invest-app:token');
 
  window.location.href = '/signin.html';
}
 
export default { isAuthenticated, getToken, signin, signout };
 
/codes/expressjs/invest-app-auth/public/js/lib/auth.js
function isAuthenticated() {
  if (!getToken()) {
    window.location.href = '/signin.html';
  } else {
    return true;
  }
}
 
function getToken() {
  return localStorage.getItem('@invest-app:token');
}
 
function signin(token) {
  localStorage.setItem('@invest-app:token', token);
 
  window.location.href = '/home.html';
}
 
function signout() {
  localStorage.removeItem('@invest-app:token');
 
  window.location.href = '/signin.html';
}
 
export default { isAuthenticated, getToken, signin, signout };
 

Home:

/codes/expressjs/invest-app-auth/public/home.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>Invest App</title>
    <link
      href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css"
      rel="stylesheet"
      integrity="sha384-GLhlTQ8iRABdZLl6O3oVMWSktQOp6b7In1Zl3/Jr59b6EGGoI1aFkw7cmDA6j6gD"
      crossorigin="anonymous"
    />
    <link rel="stylesheet" href="css/style.css" />
    <script src="https://code.iconify.design/3/3.1.0/iconify.min.js"></script>
  </head>
  <body class="pb-5">
    <nav class="navbar navbar-expand-lg bg-body-tertiary">
      <div class="container">
        <a class="navbar-brand" href="#">Invest App</a>
        <button
          class="navbar-toggler"
          type="button"
          data-bs-toggle="collapse"
          data-bs-target="#navbarSupportedContent"
          aria-controls="navbarSupportedContent"
          aria-expanded="false"
          aria-label="Toggle navigation"
        >
          <span class="navbar-toggler-icon"></span>
        </button>
        <div class="collapse navbar-collapse" id="navbarSupportedContent">
          <ul class="navbar-nav me-auto mb-2 mb-lg-0">
            <li class="nav-item">
              <a class="nav-link active" aria-current="page" href="#"
                >Investimento</a
              >
            </li>
          </ul>
          <div class="dropdown">
            <div
              class="d-flex justify-content-center align-items-center rounded-circle bg-dark ms-2 text-white"
              style="width: 1.5rem; height: 1.5rem"
              data-bs-toggle="dropdown"
              aria-expanded="false"
            >
              <span class="iconify" data-icon="mdi:account"></span>
            </div>
            <ul class="dropdown-menu dropdown-menu-end">
              <li>
                <h5 id="user-name" class="dropdown-header"></h5>
              </li>
              <li>
                <a class="dropdown-item" href="#" onclick="signout()">Sair</a>
              </li>
            </ul>
          </div>
        </div>
      </div>
    </nav>
 
    <div class="container pb-5">
      <h1 class="text-center my-5">Investimentos</h1>
      <div class="investments">
        <p class="text-center">Nenhum investimento cadastrado.</p>
        <div
          id="investment-grid"
          class="row row-cols-1 row-cols-md-3 g-4"
        ></div>
      </div>
    </div>
 
    <div>
      <button
        class="btn btn-secondary create-investment"
        style="position: fixed; bottom: 24px; right: 24px"
        type="button"
        data-bs-toggle="offcanvas"
        data-bs-target="#offcanvasRight"
        aria-controls="offcanvasRight"
      >
        +
      </button>
    </div>
 
    <div
      class="offcanvas offcanvas-end"
      tabindex="-1"
      id="offcanvasRight"
      aria-labelledby="offcanvasRightLabel"
    >
      <div class="offcanvas-header">
        <h5 class="offcanvas-title" id="offcanvasRightLabel">
          Cadastro de Investimento
        </h5>
 
        <button
          type="button"
          id="offcanvas-close"
          class="btn-close"
          data-bs-dismiss="offcanvas"
          aria-label="Close"
        ></button>
      </div>
      <div class="offcanvas-body">
        <form>
          <div class="mb-3">
            <label for="name" class="form-label">Nome</label>
            <input
              type="name"
              class="form-control"
              id="name"
              name="name"
              placeholder="Ex: Tesouro Selic"
            />
          </div>
          <div class="mb-3">
            <label for="value" class="form-label">Valor</label>
            <input
              type="number"
              class="form-control"
              id="value"
              name="value"
              step="0.01"
              placeholder="100"
            />
          </div>
          <div class="mb-3">
            <label for="interest" class="form-label">Taxa</label>
            <input
              type="text"
              class="form-control"
              id="interest"
              name="interest"
              placeholder="100% Selic"
            />
          </div>
          <div class="mb-3">
            <label for="categoryId" class="form-label">Categoria</label>
            <select
              class="form-control"
              id="categoryId"
              name="categoryId"
            ></select>
          </div>
          <div class="mb-3">
            <label for="broker" class="form-label">Corretora</label>
            <input
              type="text"
              class="form-control"
              id="broker"
              name="broker"
              placeholder="Corretora"
            />
          </div>
          <div class="mb-3">
            <label for="createdAt" class="form-label">Data</label>
            <input
              type="date"
              class="form-control"
              id="createdAt"
              name="createdAt"
            />
          </div>
          <div class="mb-3">
            <input
              type="submit"
              class="form-control btn btn-secondary"
              id="submit"
              placeholder="100"
            />
          </div>
        </form>
      </div>
    </div>
 
    <div class="modal" tabindex="-1">
      <div class="modal-dialog">
        <div class="modal-content">
          <div class="modal-header">
            <h5 class="modal-title">Remover Investimento</h5>
            <button
              type="button"
              class="btn-close"
              data-bs-dismiss="modal"
              aria-label="Close"
            ></button>
          </div>
          <div class="modal-body">
            <p>Deseja remover o investimento?</p>
          </div>
          <div class="modal-footer">
            <button
              type="button"
              class="btn btn-secondary"
              data-bs-dismiss="modal"
            >
              Fechar
            </button>
            <button type="button" class="btn btn-primary">Confirmar</button>
          </div>
        </div>
      </div>
    </div>
 
    <script
      src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.bundle.min.js"
      integrity="sha384-w76AqPfDkMBDXo30jS1Sgez6pr3x5MlQ1ZAGC+nuZB+EYdgRZgiwxhTBTkF7CXvN"
      crossorigin="anonymous"
    ></script>
    <script src="./js/home.js" type="module"></script>
  </body>
</html>
 
/codes/expressjs/invest-app-auth/public/home.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>Invest App</title>
    <link
      href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css"
      rel="stylesheet"
      integrity="sha384-GLhlTQ8iRABdZLl6O3oVMWSktQOp6b7In1Zl3/Jr59b6EGGoI1aFkw7cmDA6j6gD"
      crossorigin="anonymous"
    />
    <link rel="stylesheet" href="css/style.css" />
    <script src="https://code.iconify.design/3/3.1.0/iconify.min.js"></script>
  </head>
  <body class="pb-5">
    <nav class="navbar navbar-expand-lg bg-body-tertiary">
      <div class="container">
        <a class="navbar-brand" href="#">Invest App</a>
        <button
          class="navbar-toggler"
          type="button"
          data-bs-toggle="collapse"
          data-bs-target="#navbarSupportedContent"
          aria-controls="navbarSupportedContent"
          aria-expanded="false"
          aria-label="Toggle navigation"
        >
          <span class="navbar-toggler-icon"></span>
        </button>
        <div class="collapse navbar-collapse" id="navbarSupportedContent">
          <ul class="navbar-nav me-auto mb-2 mb-lg-0">
            <li class="nav-item">
              <a class="nav-link active" aria-current="page" href="#"
                >Investimento</a
              >
            </li>
          </ul>
          <div class="dropdown">
            <div
              class="d-flex justify-content-center align-items-center rounded-circle bg-dark ms-2 text-white"
              style="width: 1.5rem; height: 1.5rem"
              data-bs-toggle="dropdown"
              aria-expanded="false"
            >
              <span class="iconify" data-icon="mdi:account"></span>
            </div>
            <ul class="dropdown-menu dropdown-menu-end">
              <li>
                <h5 id="user-name" class="dropdown-header"></h5>
              </li>
              <li>
                <a class="dropdown-item" href="#" onclick="signout()">Sair</a>
              </li>
            </ul>
          </div>
        </div>
      </div>
    </nav>
 
    <div class="container pb-5">
      <h1 class="text-center my-5">Investimentos</h1>
      <div class="investments">
        <p class="text-center">Nenhum investimento cadastrado.</p>
        <div
          id="investment-grid"
          class="row row-cols-1 row-cols-md-3 g-4"
        ></div>
      </div>
    </div>
 
    <div>
      <button
        class="btn btn-secondary create-investment"
        style="position: fixed; bottom: 24px; right: 24px"
        type="button"
        data-bs-toggle="offcanvas"
        data-bs-target="#offcanvasRight"
        aria-controls="offcanvasRight"
      >
        +
      </button>
    </div>
 
    <div
      class="offcanvas offcanvas-end"
      tabindex="-1"
      id="offcanvasRight"
      aria-labelledby="offcanvasRightLabel"
    >
      <div class="offcanvas-header">
        <h5 class="offcanvas-title" id="offcanvasRightLabel">
          Cadastro de Investimento
        </h5>
 
        <button
          type="button"
          id="offcanvas-close"
          class="btn-close"
          data-bs-dismiss="offcanvas"
          aria-label="Close"
        ></button>
      </div>
      <div class="offcanvas-body">
        <form>
          <div class="mb-3">
            <label for="name" class="form-label">Nome</label>
            <input
              type="name"
              class="form-control"
              id="name"
              name="name"
              placeholder="Ex: Tesouro Selic"
            />
          </div>
          <div class="mb-3">
            <label for="value" class="form-label">Valor</label>
            <input
              type="number"
              class="form-control"
              id="value"
              name="value"
              step="0.01"
              placeholder="100"
            />
          </div>
          <div class="mb-3">
            <label for="interest" class="form-label">Taxa</label>
            <input
              type="text"
              class="form-control"
              id="interest"
              name="interest"
              placeholder="100% Selic"
            />
          </div>
          <div class="mb-3">
            <label for="categoryId" class="form-label">Categoria</label>
            <select
              class="form-control"
              id="categoryId"
              name="categoryId"
            ></select>
          </div>
          <div class="mb-3">
            <label for="broker" class="form-label">Corretora</label>
            <input
              type="text"
              class="form-control"
              id="broker"
              name="broker"
              placeholder="Corretora"
            />
          </div>
          <div class="mb-3">
            <label for="createdAt" class="form-label">Data</label>
            <input
              type="date"
              class="form-control"
              id="createdAt"
              name="createdAt"
            />
          </div>
          <div class="mb-3">
            <input
              type="submit"
              class="form-control btn btn-secondary"
              id="submit"
              placeholder="100"
            />
          </div>
        </form>
      </div>
    </div>
 
    <div class="modal" tabindex="-1">
      <div class="modal-dialog">
        <div class="modal-content">
          <div class="modal-header">
            <h5 class="modal-title">Remover Investimento</h5>
            <button
              type="button"
              class="btn-close"
              data-bs-dismiss="modal"
              aria-label="Close"
            ></button>
          </div>
          <div class="modal-body">
            <p>Deseja remover o investimento?</p>
          </div>
          <div class="modal-footer">
            <button
              type="button"
              class="btn btn-secondary"
              data-bs-dismiss="modal"
            >
              Fechar
            </button>
            <button type="button" class="btn btn-primary">Confirmar</button>
          </div>
        </div>
      </div>
    </div>
 
    <script
      src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.bundle.min.js"
      integrity="sha384-w76AqPfDkMBDXo30jS1Sgez6pr3x5MlQ1ZAGC+nuZB+EYdgRZgiwxhTBTkF7CXvN"
      crossorigin="anonymous"
    ></script>
    <script src="./js/home.js" type="module"></script>
  </body>
</html>
 
/codes/expressjs/invest-app-auth/public/js/home.js
import API from './services/api.js';
import Auth from './lib/auth.js';
import { formatCurrency, formatDate } from './lib/format.js';
 
let removedHostId;
 
const form = document.querySelector('form');
 
const bsOffcanvas = new bootstrap.Offcanvas('.offcanvas');
 
const confirmModal = new bootstrap.Modal('.modal');
 
function InvestmentCard(investment) {
  return `<div class="col" id="investment-${investment.id}">
    <div class="card">
      <div class="card-header">
        <span class="investment-name">
          ${investment.name}
        </span>
        <span class="float-end">
          <span class="icon-trash" >
            <span
              class="iconify"
              data-icon="solar:trash-bin-minimalistic-broken"
            >
            </span>
          </span>
          <span class="icon-pencil">
            <span
              class="iconify"
              data-icon="tabler:pencil"
            >
            </span>
          </span>
        </span>
      </div>
      <div class="card-body">
        <div>
          <span class="fw-bold">Valor:</span>
          <span class="investment-value">
            ${formatCurrency(investment.value / 100)}
          </span>
        </div>
        <div>
          <span class="fw-bold">Taxa:</span>
          <span class="investment-interest">
            ${investment.interest}
          </span>
        </div>
        <div>
          <span class="fw-bold">Data:</span>
          <span class="investment-created-at">
            ${formatDate(investment.createdAt)}
          </span>
        </div>
        <div>
          <span class="fw-bold">Corretora:</span>
          <span class="investment-broker">
            ${investment.broker.name}
          </span>
        </div>
        <div>
          <span class="fw-bold">Categoria:</span>
          <span
            class="badge investment-category"
            style="background-color: ${investment.category.color}"
          >
            ${investment.category.name}
          </span>
        </div>
      </div>
    </div>
  </div>`;
}
 
function createInvestmentCard(investment) {
  document.querySelector('.investments p').style.display = 'none';
 
  const investmentContainer = document.querySelector(`#investment-grid`);
 
  investmentContainer.insertAdjacentHTML(
    'beforeend',
    InvestmentCard(investment)
  );
 
  loadHandleConfirmModal(investment.id);
 
  loadHandleUpdateInvestment(investment.id);
}
 
async function loadInvestmentCards() {
  const investments = await API.read('/investments');
 
  for (const investment of investments) {
    createInvestmentCard(investment);
  }
}
 
function updateInvestmentCard({
  id,
  name,
  value,
  createdAt,
  category,
  broker,
  interest,
}) {
  document.querySelector(`#investment-${id} .investment-name`).innerText = name;
 
  document.querySelector(`#investment-${id} .investment-value`).innerText =
    formatCurrency(value / 100);
 
  document.querySelector(`#investment-${id} .investment-interest`).innerText =
    interest;
 
  document.querySelector(`#investment-${id} .investment-broker`).innerText =
    broker.name;
 
  document.querySelector(`#investment-${id} .investment-created-at`).innerText =
    createdAt;
 
  document.querySelector(
    `#investment-${id} .investment-category`
  ).style.backgroundColor = category.color;
 
  document.querySelector(`#investment-${id} .investment-category`).innerText =
    category.name;
}
 
function loadHandleFormSubmit(type, id) {
  form.onsubmit = async (event) => {
    event.preventDefault();
 
    const investment = Object.fromEntries(new FormData(form));
 
    investment.value = Number(investment.value) * 100;
 
    if (type === 'create') {
      const createdInvestment = await API.create('/investments', investment);
 
      createInvestmentCard(createdInvestment);
    } else if (type === 'update') {
      const updatedInvestment = await API.update(
        `/investments/${id}`,
        investment
      );
 
      updateInvestmentCard(updatedInvestment);
    }
 
    form.reset();
 
    document.querySelector('#offcanvas-close').click();
  };
}
 
function loadHandleCreateInvestment() {
  const button = document.querySelector('.btn.create-investment');
 
  button.onclick = () => {
    form.reset();
 
    bsOffcanvas.show();
 
    loadHandleFormSubmit('create');
  };
}
 
function loadHandleUpdateInvestment(id) {
  const iconPencil = document.querySelector(`#investment-${id} .icon-pencil`);
 
  iconPencil.onclick = async () => {
    const investment = await API.read(`/investments/${id}`);
 
    const { name, value, interest, createdAt, categoryId, broker } = investment;
 
    document.querySelector('form #name').value = name;
 
    document.querySelector('form #value').value = value / 100;
 
    document.querySelector('form #interest').value = interest;
 
    document.querySelector('form #categoryId').value = categoryId;
 
    document.querySelector('form #createdAt').value = formatDate(
      createdAt,
      'ymd'
    );
 
    document.querySelector('form #broker').value = broker.name;
 
    bsOffcanvas.show();
 
    loadHandleFormSubmit('update', id);
  };
}
 
function loadHandleConfirmModal(id) {
  const iconTrash = document.querySelector(`#investment-${id} .icon-trash`);
 
  iconTrash.onclick = () => {
    removedHostId = id;
 
    confirmModal.show();
  };
}
 
function loadHandleRemoveInvestment() {
  const confirmBtn = document.querySelector('.modal .btn-primary');
 
  confirmBtn.onclick = () => {
    API.remove(`/investments/${removedHostId}`);
 
    document.querySelector(`#investment-${removedHostId}`).remove();
 
    confirmModal.hide();
  };
}
 
async function loadCategoriesSelect() {
  const select = document.querySelector('#categoryId');
 
  const categories = await API.read('/categories');
 
  for (const category of categories) {
    const option = `<option value="${category.id}">${category.name}</option>`;
 
    select.insertAdjacentHTML('beforeend', option);
  }
}
 
async function loadUser() {
  const user = await API.read('/users/me');
 
  const userName = document.querySelector('#user-name');
 
  userName.innerText = user.name;
}
 
window.signout = Auth.signout;
 
if (Auth.isAuthenticated()) {
  loadInvestmentCards();
 
  loadHandleCreateInvestment();
 
  loadCategoriesSelect();
 
  loadHandleRemoveInvestment();
 
  loadUser();
}
 
/codes/expressjs/invest-app-auth/public/js/home.js
import API from './services/api.js';
import Auth from './lib/auth.js';
import { formatCurrency, formatDate } from './lib/format.js';
 
let removedHostId;
 
const form = document.querySelector('form');
 
const bsOffcanvas = new bootstrap.Offcanvas('.offcanvas');
 
const confirmModal = new bootstrap.Modal('.modal');
 
function InvestmentCard(investment) {
  return `<div class="col" id="investment-${investment.id}">
    <div class="card">
      <div class="card-header">
        <span class="investment-name">
          ${investment.name}
        </span>
        <span class="float-end">
          <span class="icon-trash" >
            <span
              class="iconify"
              data-icon="solar:trash-bin-minimalistic-broken"
            >
            </span>
          </span>
          <span class="icon-pencil">
            <span
              class="iconify"
              data-icon="tabler:pencil"
            >
            </span>
          </span>
        </span>
      </div>
      <div class="card-body">
        <div>
          <span class="fw-bold">Valor:</span>
          <span class="investment-value">
            ${formatCurrency(investment.value / 100)}
          </span>
        </div>
        <div>
          <span class="fw-bold">Taxa:</span>
          <span class="investment-interest">
            ${investment.interest}
          </span>
        </div>
        <div>
          <span class="fw-bold">Data:</span>
          <span class="investment-created-at">
            ${formatDate(investment.createdAt)}
          </span>
        </div>
        <div>
          <span class="fw-bold">Corretora:</span>
          <span class="investment-broker">
            ${investment.broker.name}
          </span>
        </div>
        <div>
          <span class="fw-bold">Categoria:</span>
          <span
            class="badge investment-category"
            style="background-color: ${investment.category.color}"
          >
            ${investment.category.name}
          </span>
        </div>
      </div>
    </div>
  </div>`;
}
 
function createInvestmentCard(investment) {
  document.querySelector('.investments p').style.display = 'none';
 
  const investmentContainer = document.querySelector(`#investment-grid`);
 
  investmentContainer.insertAdjacentHTML(
    'beforeend',
    InvestmentCard(investment)
  );
 
  loadHandleConfirmModal(investment.id);
 
  loadHandleUpdateInvestment(investment.id);
}
 
async function loadInvestmentCards() {
  const investments = await API.read('/investments');
 
  for (const investment of investments) {
    createInvestmentCard(investment);
  }
}
 
function updateInvestmentCard({
  id,
  name,
  value,
  createdAt,
  category,
  broker,
  interest,
}) {
  document.querySelector(`#investment-${id} .investment-name`).innerText = name;
 
  document.querySelector(`#investment-${id} .investment-value`).innerText =
    formatCurrency(value / 100);
 
  document.querySelector(`#investment-${id} .investment-interest`).innerText =
    interest;
 
  document.querySelector(`#investment-${id} .investment-broker`).innerText =
    broker.name;
 
  document.querySelector(`#investment-${id} .investment-created-at`).innerText =
    createdAt;
 
  document.querySelector(
    `#investment-${id} .investment-category`
  ).style.backgroundColor = category.color;
 
  document.querySelector(`#investment-${id} .investment-category`).innerText =
    category.name;
}
 
function loadHandleFormSubmit(type, id) {
  form.onsubmit = async (event) => {
    event.preventDefault();
 
    const investment = Object.fromEntries(new FormData(form));
 
    investment.value = Number(investment.value) * 100;
 
    if (type === 'create') {
      const createdInvestment = await API.create('/investments', investment);
 
      createInvestmentCard(createdInvestment);
    } else if (type === 'update') {
      const updatedInvestment = await API.update(
        `/investments/${id}`,
        investment
      );
 
      updateInvestmentCard(updatedInvestment);
    }
 
    form.reset();
 
    document.querySelector('#offcanvas-close').click();
  };
}
 
function loadHandleCreateInvestment() {
  const button = document.querySelector('.btn.create-investment');
 
  button.onclick = () => {
    form.reset();
 
    bsOffcanvas.show();
 
    loadHandleFormSubmit('create');
  };
}
 
function loadHandleUpdateInvestment(id) {
  const iconPencil = document.querySelector(`#investment-${id} .icon-pencil`);
 
  iconPencil.onclick = async () => {
    const investment = await API.read(`/investments/${id}`);
 
    const { name, value, interest, createdAt, categoryId, broker } = investment;
 
    document.querySelector('form #name').value = name;
 
    document.querySelector('form #value').value = value / 100;
 
    document.querySelector('form #interest').value = interest;
 
    document.querySelector('form #categoryId').value = categoryId;
 
    document.querySelector('form #createdAt').value = formatDate(
      createdAt,
      'ymd'
    );
 
    document.querySelector('form #broker').value = broker.name;
 
    bsOffcanvas.show();
 
    loadHandleFormSubmit('update', id);
  };
}
 
function loadHandleConfirmModal(id) {
  const iconTrash = document.querySelector(`#investment-${id} .icon-trash`);
 
  iconTrash.onclick = () => {
    removedHostId = id;
 
    confirmModal.show();
  };
}
 
function loadHandleRemoveInvestment() {
  const confirmBtn = document.querySelector('.modal .btn-primary');
 
  confirmBtn.onclick = () => {
    API.remove(`/investments/${removedHostId}`);
 
    document.querySelector(`#investment-${removedHostId}`).remove();
 
    confirmModal.hide();
  };
}
 
async function loadCategoriesSelect() {
  const select = document.querySelector('#categoryId');
 
  const categories = await API.read('/categories');
 
  for (const category of categories) {
    const option = `<option value="${category.id}">${category.name}</option>`;
 
    select.insertAdjacentHTML('beforeend', option);
  }
}
 
async function loadUser() {
  const user = await API.read('/users/me');
 
  const userName = document.querySelector('#user-name');
 
  userName.innerText = user.name;
}
 
window.signout = Auth.signout;
 
if (Auth.isAuthenticated()) {
  loadInvestmentCards();
 
  loadHandleCreateInvestment();
 
  loadCategoriesSelect();
 
  loadHandleRemoveInvestment();
 
  loadUser();
}
 
/codes/expressjs/invest-app-auth/public/js/lib/format.js
export function formatCurrency(value) {
  return Number(value).toLocaleString('pt-br', {
    style: 'currency',
    currency: 'BRL',
  });
}
 
export function formatDate(value, pattern) {
  const date = new Date(value).toLocaleDateString('pt-br');
 
  if (pattern === 'ymd') {
    return date.split('/').reverse().join('-');
  } else {
    return date;
  }
}
 
/codes/expressjs/invest-app-auth/public/js/lib/format.js
export function formatCurrency(value) {
  return Number(value).toLocaleString('pt-br', {
    style: 'currency',
    currency: 'BRL',
  });
}
 
export function formatDate(value, pattern) {
  const date = new Date(value).toLocaleDateString('pt-br');
 
  if (pattern === 'ymd') {
    return date.split('/').reverse().join('-');
  } else {
    return date;
  }
}
 

Editar esta página