back-end app

This commit is contained in:
rodrigo 2022-09-29 20:45:12 -03:00
commit 59d1ea5555
90 changed files with 5935 additions and 0 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
node_modules

13
README.md Normal file
View file

@ -0,0 +1,13 @@
### Scripts
| Script | Target |
| ------------------------- | -------------------------------------------------- |
| `npm run dev` | Run API in **development** environment |
| `npm start` | Run API in **production** environment |
| `npm run migrate` | Create database tables |
| `npm run seed` | Populate database tables |
### API Docs
To view the API documentation, run the API and access [http://localhost:3333/api-docs](http://localhost:3333/api-docs) in your browser

BIN
exercises/gif/abdutora.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 414 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 254 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 349 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 375 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 268 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 294 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 222 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 366 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 286 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 352 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 285 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 224 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 279 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 285 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 285 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 351 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 414 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 308 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 292 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 292 KiB

BIN
exercises/gif/serrote.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 354 KiB

BIN
exercises/gif/stiff.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 266 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 324 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 349 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 242 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8 KiB

BIN
exercises/thumb/serrote.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

BIN
exercises/thumb/stiff.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

1
insomnia.json Normal file

File diff suppressed because one or more lines are too long

17
knexfile.js Normal file
View file

@ -0,0 +1,17 @@
const path = require("path");
module.exports = {
development: {
client: "sqlite3",
connection: {
filename: path.resolve(__dirname, "src", "database", "database.db")
},
migrations: {
directory: path.resolve(__dirname, "src", "database", "migrations")
},
seeds: {
directory: path.resolve(__dirname, "src", "database", "seeds")
},
useNullAsDefault: true
}
};

4564
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

29
package.json Normal file
View file

@ -0,0 +1,29 @@
{
"name": "api",
"version": "1.0.0",
"description": "API desenvolvida para ser utilizada no módulo de consumo de API na trilha de React Native do Ignite.",
"main": "index.js",
"scripts": {
"start": "node ./src/server.js",
"dev": "nodemon ./src/server.js",
"migrate": "knex migrate:latest",
"seed": "knex seed:run"
},
"author": "Rodrigo Gonçalves Santana",
"license": "ISC",
"dependencies": {
"bcryptjs": "^2.4.3",
"cors": "^2.8.5",
"dayjs": "^1.11.5",
"express": "^4.18.1",
"express-async-errors": "^3.1.1",
"jsonwebtoken": "^8.5.1",
"knex": "^2.2.0",
"multer": "^1.4.5-lts.1",
"sqlite3": "^5.0.11",
"swagger-ui-express": "^4.5.0"
},
"devDependencies": {
"nodemon": "^2.0.19"
}
}

6
src/configs/auth.js Normal file
View file

@ -0,0 +1,6 @@
module.exports = {
jwt: {
secret: "rodrigo",
expiresIn: "10s"
},
};

24
src/configs/upload.js Normal file
View file

@ -0,0 +1,24 @@
const multer = require("multer");
const crypto = require("crypto");
const path = require("path");
const TMP_FOLDER = path.resolve(__dirname, "..", "..", "tmp");
const UPLOADS_FOLDER = path.resolve(TMP_FOLDER, "uploads");
const MULTER = {
storage: multer.diskStorage({
destination: TMP_FOLDER,
filename(request, file, callback) {
const fileHash = crypto.randomBytes(10).toString("hex");
const fileName = `${fileHash}-${file.originalname}`;
return callback(null, fileName);
},
}),
};
module.exports = {
TMP_FOLDER,
UPLOADS_FOLDER,
MULTER
}

View file

@ -0,0 +1,21 @@
const knex = require("../database");
class ExercisesController {
async index(request, response) {
const { group } = request.params;
const exercises = await knex("exercises").where({ group }).orderBy("name");
return response.json(exercises);
}
async show(request, response) {
const { id } = request.params;
const exercise = await knex("exercises").where({ id }).first();
return response.json(exercise);
}
}
module.exports = ExercisesController;

View file

@ -0,0 +1,13 @@
const knex = require("../database");
class GroupsController {
async index(request, response) {
const groups = await knex("exercises").select("group").groupBy("group").orderBy("group");
const formattedGroups = groups.map(item => item.group);
return response.json(formattedGroups);
}
}
module.exports = GroupsController;

View file

@ -0,0 +1,62 @@
const AppError = require("../utils/AppError");
const knex = require("../database");
const dayjs = require("dayjs");
class HistoryController {
async index(request, response) {
const user_id = request.user.id;
const history = await knex("history")
.select(
"history.id",
"history.user_id",
"history.exercise_id",
"exercises.name",
"exercises.group",
"history.created_at"
)
.leftJoin("exercises", "exercises.id", "=", "history.exercise_id")
.where({ user_id }).orderBy("history.created_at", "desc");
const days = [];
for (let exercise of history) {
const day = dayjs(exercise.created_at).format('DD.MM.YYYY');
if (!days.includes(day)) {
days.push(day);
}
}
const exercisesByDay = days.map(day => {
const exercises = history
.filter((exercise) => dayjs(exercise.created_at).format('DD.MM.YYYY') === day).
map((exercise) => {
return {
...exercise,
hour: dayjs(exercise.created_at).format('HH:mm')
}
});
return ({ title: day, data: exercises });
});
return response.json(exercisesByDay);
}
async create(request, response) {
const { exercise_id } = request.body;
const user_id = request.user.id;
if (!exercise_id) {
throw new AppError("Informe o id do exercício.");
}
await knex("history").insert({ user_id, exercise_id });
return response.status(201).json();
}
}
module.exports = HistoryController;

View file

@ -0,0 +1,39 @@
const knex = require("../database");
const { compare } = require("bcryptjs");
const { sign } = require("jsonwebtoken");
const AppError = require("../utils/AppError");
const authConfig = require("../configs/auth");
const GenerateRefreshToken = require("../providers/GenerateRefreshToken");
const GenerateToken = require("../providers/GenerateToken");
class SessionsController {
async create(request, response) {
const { email, password } = request.body;
const user = await knex("users").where({ email }).first();
if (!user) {
throw new AppError("E-mail e/ou senha incorreta.", 404);
}
const passwordMatched = await compare(password, user.password);
if (!passwordMatched) {
throw new AppError("E-mail e/ou senha incorreta.", 404);
}
const generateTokenProvider = new GenerateToken();
const token = await generateTokenProvider.execute(user.id);
await knex("users_tokens").where({ user_id: user.id }).delete();
const generateRefreshToken = new GenerateRefreshToken();
generateRefreshToken.execute(user.id, token);
delete user.password;
response.status(201).json({ token, user });
}
}
module.exports = SessionsController;

View file

@ -0,0 +1,30 @@
const knex = require("../database");
const DiskStorage = require("../providers/DiskStorage");
class UserAvatarController {
async update(request, response) {
const user_id = request.user.id;
const avatarFilename = request.file.filename;
const diskStorage = new DiskStorage();
const user = await knex("users").where({ id: user_id }).first();
if (!user) {
throw new AppError("Somente usuários autenticados podem mudar o avatar", 401);
}
if (user.avatar) {
await diskStorage.deleteFile(user.avatar);
}
const filename = await diskStorage.saveFile(avatarFilename);
user.avatar = filename;
await knex("users").where({ id: user_id }).update(user);
return response.json(user);
}
}
module.exports = UserAvatarController;

View file

@ -0,0 +1,39 @@
const knex = require("../database");
const AppError = require("../utils/AppError");
const GenerateRefreshToken = require("../providers/GenerateRefreshToken");
const GenerateToken = require("../providers/GenerateToken");
const dayjs = require("dayjs");
class UserRefreshToken {
async create(request, response) {
const { token } = request.body;
if (!token) {
throw new AppError("Informe o token de autenticação.", 401);
}
const userToken = await knex("users_tokens").where({ token }).first();
if (!userToken) {
throw new AppError("Refresh token não encontrado para este usuário.", 404);
}
const generateTokenProvider = new GenerateToken();
const refreshToken = await generateTokenProvider.execute(userToken.user_id);
const refreshTokenExpired = dayjs().isAfter(dayjs.unix(userToken.expires_in));
if (refreshTokenExpired) {
await knex("users_tokens").where({ user_id: userToken.user_id }).delete();
const generateRefreshToken = new GenerateRefreshToken();
await generateRefreshToken.execute(userToken.user_id, refreshToken);
return response.json({ token: refreshToken });
}
return response.json({ token });
}
}
module.exports = UserRefreshToken;

View file

@ -0,0 +1,71 @@
const knex = require("../database");
const { hash, compare } = require("bcryptjs");
const AppError = require("../utils/AppError");
class UsersController {
async create(request, response) {
const { name, email, password } = request.body;
if (!name || !email || !password) {
throw new AppError("Informe todos os campos (nome, email e senha).");
}
const checkUserExists = await knex("users").where({ email }).first();
if (checkUserExists) {
throw new AppError("Este e-mail já está em uso.");
}
const hashedPassword = await hash(password, 8);
await knex("users").insert({
name,
email,
password: hashedPassword
});
return response.status(201).json();
}
async update(request, response) {
const { name, password, old_password } = request.body;
const user_id = request.user.id;
const user = await knex("users").where({ id: user_id }).first();
if (!user) {
throw new AppError("Usuário não encontrado", 404);
}
user.name = name ?? user.name;
if (password && !old_password) {
throw new AppError(
"Você precisa informar a senha antiga para definir a nova senha.",
);
}
if (!password && old_password) {
throw new AppError(
"Informe a nova senha.",
);
}
if (password && old_password) {
const checkOldPassword = await compare(old_password, user.password);
if (!checkOldPassword) {
throw new AppError("A senha antiga não confere.");
}
user.password = await hash(password, 8);
}
await knex("users").where({ id: user_id }).update(user);
return response.json();
}
}
module.exports = UsersController;

BIN
src/database/database.db Normal file

Binary file not shown.

6
src/database/index.js Normal file
View file

@ -0,0 +1,6 @@
const config = require("../../knexfile");
const knex = require("knex");
const connection = knex(config.development);
module.exports = connection;

View file

@ -0,0 +1,11 @@
exports.up = knex => knex.schema.createTable("users", table => {
table.increments("id");
table.text("name").notNullable();
table.text("email").notNullable();
table.text("password").notNullable();
table.text("avatar");
table.timestamp("created_at").default(knex.fn.now());
table.timestamp("updated_at").default(knex.fn.now());
});
exports.down = knex => knex.schema.dropTable("users");

View file

@ -0,0 +1,13 @@
exports.up = knex => knex.schema.createTable("exercises", table => {
table.increments("id");
table.text("name").notNullable();
table.integer("series").notNullable();
table.integer("repetitions").notNullable();
table.text("group").notNullable();
table.text("demo").notNullable();
table.text("thumb").notNullable();
table.timestamp("created_at").default(knex.fn.now());
table.timestamp("updated_at").default(knex.fn.now());
});
exports.down = knex => knex.schema.dropTable("exercises");

View file

@ -0,0 +1,10 @@
exports.up = knex => knex.schema.createTable("history", table => {
table.increments("id");
table.integer("user_id").references("id").inTable("users");
table.integer("exercise_id").references("id").inTable("exercises");
table.timestamp("created_at").default(knex.fn.now());
});
exports.down = knex => knex.schema.dropTable("history");

View file

@ -0,0 +1,9 @@
exports.up = knex => knex.schema.createTable("users_tokens", table => {
table.increments("id");
table.integer("expires_in")
table.integer("user_id").references("id").inTable("users");
table.text("token").notNullable();
table.timestamp("created_at").default(knex.fn.now());
});
exports.down = knex => knex.schema.dropTable("users_tokens");

View file

@ -0,0 +1,213 @@
exports.seed = async function (knex) {
await knex('exercises').del()
await knex('exercises').insert([
{
name: 'Supino inclinado com barra',
series: 4,
repetitions: 12,
group: 'peito',
demo: 'supino_inclinado_com_barra.gif',
thumb: 'supino_inclinado_com_barra.png',
},
{
name: 'Crucifixo reto',
series: 3,
repetitions: 12,
group: 'peito',
demo: 'crucifixo_reto.gif',
thumb: 'crucifixo_reto.png'
},
{
name: 'Supino reto com barra',
series: 3,
repetitions: 12,
group: 'peito',
demo: 'supino_reto_com_barra.gif',
thumb: 'supino_reto_com_barra.png'
},
{
name: 'Francês deitado com halteres',
series: 3,
repetitions: 12,
group: 'tríceps',
demo: 'frances_deitado_com_halteres.gif',
thumb: 'frances_deitado_com_halteres.png'
},
{
name: 'Corda Cross',
series: 4,
repetitions: 12,
group: 'tríceps',
demo: 'corda_cross.gif',
thumb: 'corda_cross.png'
},
{
name: 'Barra Cross',
series: 3,
repetitions: 12,
group: 'tríceps',
demo: 'barra_cross.gif',
thumb: 'barra_cross.png'
},
{
name: 'Tríceps testa',
series: 4,
repetitions: 12,
group: 'tríceps',
demo: 'triceps_testa.gif',
thumb: 'triceps_testa.png'
},
{
name: 'Levantamento terra',
series: 3,
repetitions: 12,
group: 'costas',
demo: 'levantamento_terra.gif',
thumb: 'levantamento_terra.png'
},
{
name: 'Pulley frontal',
series: 3,
repetitions: 12,
group: 'costas',
demo: 'pulley_frontal.gif',
thumb: 'pulley_frontal.png'
},
{
name: 'Pulley atrás',
series: 4,
repetitions: 12,
group: 'costas',
demo: 'pulley_atras.gif',
thumb: 'pulley_atras.png'
},
{
name: 'Remada baixa',
series: 4,
repetitions: 12,
group: 'costas',
demo: 'remada_baixa.gif',
thumb: 'remada_baixa.png'
},
{
name: 'Serrote',
series: 4,
repetitions: 12,
group: 'costas',
demo: 'serrote.gif',
thumb: 'serrote.png'
},
{
name: 'Rosca alternada com banco inclinado',
series: 4,
repetitions: 12,
group: 'bíceps',
demo: 'rosca_alternada_com_banco_inclinado.gif',
thumb: 'rosca_alternada_com_banco_inclinado.png'
},
{
name: 'Rosca Scott barra w',
series: 4,
repetitions: 12,
group: 'bíceps',
demo: 'rosca_scott_barra_w.gif',
thumb: 'rosca_scott_barra_w.png'
},
{
name: 'Rosca direta barra reta',
series: 3,
repetitions: 12,
group: 'bíceps',
demo: 'rosca_direta_barra_reta.gif',
thumb: 'rosca_direta_barra_reta.png'
},
{
name: 'Martelo em pé',
series: 3,
repetitions: 12,
group: 'bíceps',
demo: 'martelo_em_pe.gif',
thumb: 'martelo_em_pe.png'
},
{
name: 'Rosca punho',
series: 4,
repetitions: 12,
group: 'antebraço',
demo: 'rosca_punho.gif',
thumb: 'rosca_punho.png'
},
{
name: 'Leg press 45 graus',
series: 4,
repetitions: 12,
group: 'pernas',
demo: 'leg_press_45_graus.gif',
thumb: 'leg_press_45_graus.png'
},
{
name: 'Extensor de pernas',
series: 4,
repetitions: 12,
group: 'pernas',
demo: 'extensor_de_pernas.gif',
thumb: 'extensor_de_pernas.png'
},
{
name: 'Abdutora',
series: 4,
repetitions: 12,
group: 'pernas',
demo: 'abdutora.gif',
thumb: 'abdutora.png'
},
{
name: 'Stiff',
series: 4,
repetitions: 12,
group: 'pernas',
demo: 'stiff.gif',
thumb: 'stiff.png',
},
{
name: 'Neck Press',
series: 4,
repetitions: 10,
group: 'ombro',
demo: 'neck-press.gif',
thumb: 'neck-press.png'
},
{
name: 'Desenvolvimento maquina',
series: 3,
repetitions: 10,
group: 'ombro',
demo: 'desenvolvimento_maquina.gif',
thumb: 'desenvolvimento_maquina.png'
},
{
name: 'Elevação lateral com halteres sentado',
series: 4,
repetitions: 10,
group: 'ombro',
demo: 'elevacao_lateral_com_halteres_sentado.gif',
thumb: 'elevacao_lateral_com_halteres_sentado.png'
},
{
name: 'Encolhimento com halteres',
series: 4,
repetitions: 10,
group: 'trapézio',
demo: 'encolhimento_com_halteres.gif',
thumb: 'encolhimento_com_halteres.png'
},
{
name: 'Encolhimento com barra',
series: 4,
repetitions: 10,
group: 'trapézio',
demo: 'encolhimento_com_barra.gif',
thumb: 'encolhimento_com_barra.png'
}
]);
};

502
src/docs/swagger.json Normal file
View file

@ -0,0 +1,502 @@
{
"openapi": "3.0.0",
"info": {
"title": "Ignite Gym API",
"description": "API developed by Rodrigo Gonçalves to be used in Ignite training in the mobile backend integration.",
"version": "1.0.0",
"contact": {
"email": "rodrigo.rgtic@gmail.com"
}
},
"paths": {
"/users": {
"post": {
"tags": [
"User"
],
"summary": "Create",
"description": "Create a new user",
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"email": {
"type": "string"
},
"password": {
"type": "string"
}
},
"example": {
"name": "Rodrigo",
"email": "rodrigo@email.com",
"password": "123"
}
}
}
}
},
"responses": {
"201": {
"description": "Created"
},
"400": {
"description": "Bad Request"
}
}
},
"put": {
"tags": [
"User"
],
"summary": "Update",
"description": "Update user profile",
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"name": {
"type": "string",
"required": false
},
"password": {
"type": "string",
"required": false
},
"old_password": {
"type": "string",
"required": false
}
},
"example": {
"name": "Rodrigo Gonçalves",
"password": "1234",
"old_password": "123"
}
}
}
}
},
"responses": {
"200": {
"description": "Updated"
},
"400": {
"description": "Bad Request"
},
"404": {
"description": "User not found"
}
}
}
},
"/users/avatar": {
"patch": {
"tags": [
"User"
],
"summary": "Upload",
"description": "Update user profile picture",
"requestBody": {
"content": {
"multipart/form-data": {
"schema": {
"type": "object",
"properties": {
"avatar": {
"type": "string",
"format": "base64"
}
},
"example": {
"avatar": "rodrigo.png"
}
},
"encoding": {
"avatar": {
"contentType": "image/png, image/jpeg"
}
}
}
}
},
"responses": {
"200": {
"description": "Updated"
},
"400": {
"description": "Bad Request"
},
"401": {
"description": "Not authorized"
}
}
}
},
"/sessions": {
"post": {
"tags": [
"User"
],
"summary": "Sign In",
"description": "User authentication",
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"email": {
"type": "string"
},
"password": {
"type": "string"
}
},
"example": {
"email": "rodrigo@email.com",
"password": "123"
}
}
}
}
},
"responses": {
"201": {
"description": "Authenticated",
"content": {
"application/json": {
"schema": {
"type": "object",
"example": {
"id": 1,
"name": "Rodrigo Gonçalves",
"email": "rodrigo@email.com",
"avatar": "346dab6b457abadbbb2a-49030804.jpg",
"created_at": "2022-08-22 19:59:46",
"updated_at": "2022-08-22T20:07:45.340Z"
}
}
}
}
},
"400": {
"description": "Bad Request"
},
"401": {
"description": "Not authorized/Invalid email or password"
}
}
}
},
"/sessions/refresh-token": {
"post": {
"tags": [
"User"
],
"summary": "Refresh Token",
"description": "Auth Refresh Token",
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"token": {
"type": "string"
}
},
"example": {
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6InJvZHJpZ29AZW1haWwuY29tIiwiaWF0IjoxNjYxMjc1NDAxLCJleHAiOjE2NjM4Njc0MDEsInN1YiI6IjEifQ.yQqqvmuZrF9ZM0LThzIu8dlwQtmuHdG0C_nwziXWyMo"
}
}
}
}
},
"responses": {
"201": {
"description": "Created",
"content": {
"application/json": {
"schema": {
"type": "string",
"example": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6InJvZHJpZ29AZW1haWwuY29tIiwiaWF0IjoxNjYxMjc1NDE5LCJleHAiOjE2NjM4Njc0MTksInN1YiI6IjEifQ.kQoOrRyGvSkLcFS49ItDcLUEB7pEhbwyPRoEA5sR4ao"
}
}
}
},
"400": {
"description": "Bad Request"
},
"404": {
"description": "Refresh token not found"
}
}
}
},
"/exercises": {
"get": {
"tags": [
"Exercise"
],
"summary": "Index",
"description": "List all exercises",
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"example": [
{
"id": 16,
"name": "Martelo em pé",
"series": 3,
"repetitions": "10 a 12",
"group": "bíceps",
"created_at": "2022-08-23 11:12:32",
"updated_at": "2022-08-23 11:12:32"
},
{
"id": 13,
"name": "Rosca alternada com banco inclinado",
"series": 4,
"repetitions": "10 a 12",
"group": "bíceps",
"created_at": "2022-08-23 11:12:32",
"updated_at": "2022-08-23 11:12:32"
},
{
"id": 15,
"name": "Rosca direta barra reta",
"series": 3,
"repetitions": "10 a 12",
"group": "bíceps",
"created_at": "2022-08-23 11:12:32",
"updated_at": "2022-08-23 11:12:32"
},
{
"id": 14,
"name": "Rosca scott barra w",
"series": 4,
"repetitions": "10 a 12",
"group": "bíceps",
"created_at": "2022-08-23 11:12:32",
"updated_at": "2022-08-23 11:12:32"
}
]
}
}
}
},
"responses": {
"200": {
"description": "Success"
},
"400": {
"description": "Bad Request"
}
}
}
},
"/exercises/{id}": {
"get": {
"tags": [
"Exercise"
],
"summary": "Index",
"description": "List all exercises",
"parameters": [
{
"in": "path",
"name": "id",
"required": true
}
],
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"example": {
"id": 1,
"name": "Supino inclinado com barra",
"series": 4,
"repetitions": "10 a 12",
"group": "peito",
"created_at": "2022-08-23 11:12:32",
"updated_at": "2022-08-23 11:12:32"
}
}
}
}
},
"responses": {
"200": {
"description": "Success"
},
"400": {
"description": "Bad Request"
}
}
}
},
"/groups": {
"get": {
"tags": [
"Group"
],
"summary": "Index",
"description": "List all groups",
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"example": [
"Trapézio",
"antebraço",
"bíceps",
"costas",
"ombro",
"peito",
"pernas",
"tríceps"
]
}
}
}
},
"responses": {
"200": {
"description": "Success"
},
"400": {
"description": "Bad Request"
}
}
}
},
"/history": {
"get": {
"tags": [
"history"
],
"summary": "Index",
"description": "List history by user",
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"example": [
{
"id": 1,
"user_id": 1,
"exercise_id": 1,
"name": "Supino inclinado com barra",
"group": "peito",
"created_at": "2022-08-23 11:55:29"
},
{
"id": 2,
"user_id": 1,
"exercise_id": 2,
"name": "Supino inclinado com barra",
"group": "peito",
"created_at": "2022-08-23 12:16:01"
}
]
}
}
}
},
"responses": {
"200": {
"description": "Success"
},
"400": {
"description": "Bad Request"
}
}
},
"post": {
"tags": [
"history"
],
"summary": "Index",
"description": "Create user exercise history ",
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"exercise_id": {
"type": "number"
}
},
"example": [
{
"id": 1,
"user_id": 1,
"exercise_id": 1,
"name": "Supino inclinado com barra",
"group": "peito",
"created_at": "2022-08-23 11:55:29"
},
{
"id": 2,
"user_id": 1,
"exercise_id": 2,
"name": "Supino inclinado com barra",
"group": "peito",
"created_at": "2022-08-23 12:16:01"
}
]
}
}
}
},
"responses": {
"201": {
"description": "Created"
},
"400": {
"description": "Bad Request"
}
}
}
},
"/files/${filename.png}": {
"get": {
"tags": [
"Image"
],
"summary": "Show",
"description": "Show image file",
"parameters": [
{
"in": "path",
"name": "filename",
"required": true
}
],
"responses": {
"200": {
"description": "Success"
},
"400": {
"description": "Bad Request"
}
}
}
}
}
}

View file

@ -0,0 +1,29 @@
const { verify } = require("jsonwebtoken");
const AppError = require("../utils/AppError");
const authConfig = require("../configs/auth");
async function ensureAuthenticated(request, response, next) {
const authHeader = request.headers.authorization;
if (!authHeader) {
throw new AppError("JWT token não informado", 401);
}
const [, token] = authHeader.split(" ");
try {
const { sub: user_id } = verify(token, authConfig.jwt.secret);
request.user = {
id: Number(user_id),
};
return next();
} catch {
throw new AppError("token.invalid", 401);
}
}
module.exports = ensureAuthenticated;

View file

@ -0,0 +1,29 @@
const fs = require("fs");
const path = require("path");
const uploadConfig = require("../configs/upload");
class DiskStorage {
async saveFile(file) {
await fs.promises.rename(
path.resolve(uploadConfig.TMP_FOLDER, file),
path.resolve(uploadConfig.UPLOADS_FOLDER, file),
);
return file;
}
async deleteFile(file) {
const filePath = path.resolve(uploadConfig.UPLOADS_FOLDER, file);
try {
await fs.promises.stat(filePath);
} catch {
return;
}
await fs.promises.unlink(filePath);
}
}
module.exports = DiskStorage;

View file

@ -0,0 +1,16 @@
const knex = require("../database");
const dayjs = require("dayjs");
class GenerateRefreshToken {
async execute(userId, newToken) {
const expires_in = dayjs().add(15, "second").unix();
await knex("users_tokens").insert({
user_id: userId,
expires_in,
token: newToken
});
}
}
module.exports = GenerateRefreshToken;

View file

@ -0,0 +1,17 @@
const { sign } = require("jsonwebtoken");
const authConfig = require("../configs/auth");
class GenerateToken {
async execute(userId) {
const { secret, expiresIn } = authConfig.jwt;
const token = sign({}, secret, {
subject: String(userId),
expiresIn
});
return token;
}
}
module.exports = GenerateToken;

View file

@ -0,0 +1,12 @@
const { Router } = require("express");
const ExercisesController = require("../controllers/ExercisesController");
const exercisesRoutes = Router();
const exercisesController = new ExercisesController();
exercisesRoutes.get("/bygroup/:group", exercisesController.index);
exercisesRoutes.get("/:id", exercisesController.show);
module.exports = exercisesRoutes;

View file

@ -0,0 +1,11 @@
const { Router } = require("express");
const GroupsController = require("../controllers/GroupsController");
const groupRoutes = Router();
const groupsController = new GroupsController();
groupRoutes.get("/", groupsController.index);
module.exports = groupRoutes;

View file

@ -0,0 +1,15 @@
const { Router } = require("express");
const HistoryController = require("../controllers/HistoryController");
const ensureAuthenticated = require("../middlewares/ensureAuthenticated");
const historyRoutes = Router();
const historyController = new HistoryController();
historyRoutes.use(ensureAuthenticated);
historyRoutes.get("/", historyController.index);
historyRoutes.post("/", historyController.create);
module.exports = historyRoutes;

18
src/routes/index.js Normal file
View file

@ -0,0 +1,18 @@
const { Router } = require("express");
const usersRouter = require("./users.routes");
const sessionsRouter = require("./sessions.routes");
const exercisesRouter = require("./exercises.routes");
const groupRouter = require("./group.routes");
const historyRouter = require("./history.routes");
const routes = Router();
routes.use("/users", usersRouter);
routes.use("/sessions", sessionsRouter);
routes.use("/exercises", exercisesRouter);
routes.use("/groups", groupRouter);
routes.use("/history", historyRouter);
module.exports = routes;

View file

@ -0,0 +1,13 @@
const { Router } = require("express");
const SessionsController = require("../controllers/SessionsController");
const UserRefreshToken = require("../controllers/UserRefreshToken");
const sessionsController = new SessionsController();
const userRefreshToken = new UserRefreshToken();
const sessionsRoutes = Router();
sessionsRoutes.post("/", sessionsController.create);
sessionsRoutes.post("/refresh-token", userRefreshToken.create);
module.exports = sessionsRoutes;

View file

@ -0,0 +1,21 @@
const { Router } = require("express");
const multer = require("multer");
const uploadConfig = require("../configs/upload");
const ensureAuthenticated = require("../middlewares/ensureAuthenticated");
const UsersController = require("../controllers/UsersController");
const UserAvatarController = require("../controllers/UserAvatarController");
const usersRoutes = Router();
const usersController = new UsersController();
const userAvatarController = new UserAvatarController();
const upload = multer(uploadConfig.MULTER);
usersRoutes.post("/", usersController.create);
usersRoutes.put("/", ensureAuthenticated, usersController.update);
usersRoutes.patch("/avatar", ensureAuthenticated, upload.single("avatar"), userAvatarController.update);
module.exports = usersRoutes;

49
src/server.js Normal file
View file

@ -0,0 +1,49 @@
require("express-async-errors");
const path = require("path");
const swaggerDocument = require("./docs/swagger.json");
const swaggerUI = require("swagger-ui-express");
const uploadConfig = require("./configs/upload");
const AppError = require("./utils/AppError");
const express = require("express");
const cors = require("cors");
const app = express();
app.use("/avatar", express.static(uploadConfig.UPLOADS_FOLDER));
const demoExercisePath = path.resolve(__dirname, "..", "exercises", "gif")
app.use("/exercise/demo", express.static(demoExercisePath));
const thumbExercisesPath = path.resolve(__dirname, "..", "exercises", "thumb")
app.use("/exercise/thumb", express.static(thumbExercisesPath));
const routes = require("./routes");
app.use(express.json());
app.use(cors());
app.use('/api-docs', swaggerUI.serve, swaggerUI.setup(swaggerDocument));
app.use(routes);
app.use((err, request, response, next) => {
if (err instanceof AppError) {
return response.status(err.statusCode).json({
status: "error",
message: err.message,
});
}
console.error(err);
return response.status(500).json({
status: "error",
message: "Internal server error",
});
});
const PORT = 3333;
app.listen(PORT, () => console.log(`Server is running on Port ${PORT}`));

11
src/utils/AppError.js Normal file
View file

@ -0,0 +1,11 @@
class AppError {
message;
statusCode;
constructor(message, statusCode = 400) {
this.message = message;
this.statusCode = statusCode;
}
}
module.exports = AppError;

0
tmp/.gitkeep Normal file
View file

0
tmp/uploads/.gitkeep Normal file
View file

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB