back-end app
1
.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
node_modules
|
||||||
13
README.md
Normal 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
|
After Width: | Height: | Size: 414 KiB |
BIN
exercises/gif/barra_cross.gif
Normal file
|
After Width: | Height: | Size: 254 KiB |
BIN
exercises/gif/corda_cross.gif
Normal file
|
After Width: | Height: | Size: 218 KiB |
BIN
exercises/gif/crucifixo_reto.gif
Normal file
|
After Width: | Height: | Size: 349 KiB |
BIN
exercises/gif/desenvolvimento_maquina.gif
Normal file
|
After Width: | Height: | Size: 375 KiB |
BIN
exercises/gif/elevacao_lateral_com_halteres_sentado.gif
Normal file
|
After Width: | Height: | Size: 268 KiB |
BIN
exercises/gif/encolhimento_com_barra.gif
Normal file
|
After Width: | Height: | Size: 294 KiB |
BIN
exercises/gif/encolhimento_com_halteres.gif
Normal file
|
After Width: | Height: | Size: 222 KiB |
BIN
exercises/gif/extensor_de_pernas.gif
Normal file
|
After Width: | Height: | Size: 366 KiB |
BIN
exercises/gif/frances_deitado_com_halteres.gif
Normal file
|
After Width: | Height: | Size: 286 KiB |
BIN
exercises/gif/leg_press_45_graus.gif
Normal file
|
After Width: | Height: | Size: 352 KiB |
BIN
exercises/gif/levantamento_terra.gif
Normal file
|
After Width: | Height: | Size: 285 KiB |
BIN
exercises/gif/martelo_em_pe.gif
Normal file
|
After Width: | Height: | Size: 224 KiB |
BIN
exercises/gif/neck-press.gif
Normal file
|
After Width: | Height: | Size: 279 KiB |
BIN
exercises/gif/pulley_atras.gif
Normal file
|
After Width: | Height: | Size: 285 KiB |
BIN
exercises/gif/pulley_frontal.gif
Normal file
|
After Width: | Height: | Size: 285 KiB |
BIN
exercises/gif/remada_baixa.gif
Normal file
|
After Width: | Height: | Size: 351 KiB |
BIN
exercises/gif/rosca_alternada_com_banco_inclinado.gif
Normal file
|
After Width: | Height: | Size: 414 KiB |
BIN
exercises/gif/rosca_direta_barra_reta.gif
Normal file
|
After Width: | Height: | Size: 308 KiB |
BIN
exercises/gif/rosca_punho.gif
Normal file
|
After Width: | Height: | Size: 292 KiB |
BIN
exercises/gif/rosca_scott_barra_w.gif
Normal file
|
After Width: | Height: | Size: 292 KiB |
BIN
exercises/gif/serrote.gif
Normal file
|
After Width: | Height: | Size: 354 KiB |
BIN
exercises/gif/stiff.gif
Normal file
|
After Width: | Height: | Size: 266 KiB |
BIN
exercises/gif/supino_inclinado_com_barra.gif
Normal file
|
After Width: | Height: | Size: 324 KiB |
BIN
exercises/gif/supino_reto_com_barra.gif
Normal file
|
After Width: | Height: | Size: 349 KiB |
BIN
exercises/gif/triceps_testa.gif
Normal file
|
After Width: | Height: | Size: 242 KiB |
BIN
exercises/thumb/abdutora.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
exercises/thumb/barra_cross.png
Normal file
|
After Width: | Height: | Size: 5.3 KiB |
BIN
exercises/thumb/corda_cross.png
Normal file
|
After Width: | Height: | Size: 4.9 KiB |
BIN
exercises/thumb/crucifixo_reto.png
Normal file
|
After Width: | Height: | Size: 8.6 KiB |
BIN
exercises/thumb/desenvolvimento_maquina.png
Normal file
|
After Width: | Height: | Size: 8.6 KiB |
BIN
exercises/thumb/elevacao_lateral_com_halteres_sentado.png
Normal file
|
After Width: | Height: | Size: 6.4 KiB |
BIN
exercises/thumb/encolhimento_com_barra.png
Normal file
|
After Width: | Height: | Size: 7.1 KiB |
BIN
exercises/thumb/encolhimento_com_halteres.png
Normal file
|
After Width: | Height: | Size: 5.3 KiB |
BIN
exercises/thumb/extensor_de_pernas.png
Normal file
|
After Width: | Height: | Size: 9.1 KiB |
BIN
exercises/thumb/frances_deitado_com_halteres.png
Normal file
|
After Width: | Height: | Size: 7.6 KiB |
BIN
exercises/thumb/leg_press_45_graus.png
Normal file
|
After Width: | Height: | Size: 9.3 KiB |
BIN
exercises/thumb/levantamento_terra.png
Normal file
|
After Width: | Height: | Size: 5.7 KiB |
BIN
exercises/thumb/martelo_em_pe.png
Normal file
|
After Width: | Height: | Size: 5.7 KiB |
BIN
exercises/thumb/neck-press.png
Normal file
|
After Width: | Height: | Size: 7.3 KiB |
BIN
exercises/thumb/pulley_atras.png
Normal file
|
After Width: | Height: | Size: 7.4 KiB |
BIN
exercises/thumb/pulley_frontal.png
Normal file
|
After Width: | Height: | Size: 7.4 KiB |
BIN
exercises/thumb/remada_baixa.png
Normal file
|
After Width: | Height: | Size: 8.3 KiB |
BIN
exercises/thumb/rosca_alternada_com_banco_inclinado.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
exercises/thumb/rosca_direta_barra_reta.png
Normal file
|
After Width: | Height: | Size: 7.2 KiB |
BIN
exercises/thumb/rosca_punho.png
Normal file
|
After Width: | Height: | Size: 8 KiB |
BIN
exercises/thumb/rosca_scott_barra_w.png
Normal file
|
After Width: | Height: | Size: 8 KiB |
BIN
exercises/thumb/serrote.png
Normal file
|
After Width: | Height: | Size: 8.6 KiB |
BIN
exercises/thumb/stiff.png
Normal file
|
After Width: | Height: | Size: 5.7 KiB |
BIN
exercises/thumb/supino_inclinado_com_barra.png
Normal file
|
After Width: | Height: | Size: 8.4 KiB |
BIN
exercises/thumb/supino_reto_com_barra.png
Normal file
|
After Width: | Height: | Size: 8.6 KiB |
BIN
exercises/thumb/triceps_testa.png
Normal file
|
After Width: | Height: | Size: 6.9 KiB |
1
insomnia.json
Normal file
17
knexfile.js
Normal 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
29
package.json
Normal 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
|
|
@ -0,0 +1,6 @@
|
||||||
|
module.exports = {
|
||||||
|
jwt: {
|
||||||
|
secret: "rodrigo",
|
||||||
|
expiresIn: "10s"
|
||||||
|
},
|
||||||
|
};
|
||||||
24
src/configs/upload.js
Normal 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
|
||||||
|
}
|
||||||
21
src/controllers/ExercisesController.js
Normal 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;
|
||||||
13
src/controllers/GroupsController.js
Normal 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;
|
||||||
62
src/controllers/HistoryController.js
Normal 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;
|
||||||
39
src/controllers/SessionsController.js
Normal 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;
|
||||||
30
src/controllers/UserAvatarController.js
Normal 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;
|
||||||
39
src/controllers/UserRefreshToken.js
Normal 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;
|
||||||
71
src/controllers/UsersController.js
Normal 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
6
src/database/index.js
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
const config = require("../../knexfile");
|
||||||
|
const knex = require("knex");
|
||||||
|
|
||||||
|
const connection = knex(config.development);
|
||||||
|
|
||||||
|
module.exports = connection;
|
||||||
11
src/database/migrations/20220822190845_createUser.js
Normal 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");
|
||||||
13
src/database/migrations/20220823105515_createExercises.js
Normal 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");
|
||||||
10
src/database/migrations/20220823114449_createHistory.js
Normal 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");
|
||||||
|
|
@ -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");
|
||||||
213
src/database/seeds/createExercises.js
Normal 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
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
29
src/middlewares/ensureAuthenticated.js
Normal 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;
|
||||||
29
src/providers/DiskStorage.js
Normal 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;
|
||||||
16
src/providers/GenerateRefreshToken.js
Normal 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;
|
||||||
17
src/providers/GenerateToken.js
Normal 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;
|
||||||
12
src/routes/exercises.routes.js
Normal 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;
|
||||||
11
src/routes/group.routes.js
Normal 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;
|
||||||
15
src/routes/history.routes.js
Normal 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
|
|
@ -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;
|
||||||
13
src/routes/sessions.routes.js
Normal 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;
|
||||||
21
src/routes/users.routes.js
Normal 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
|
|
@ -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
|
|
@ -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
0
tmp/uploads/.gitkeep
Normal file
BIN
tmp/uploads/3ba0d7d8e36412093d3b-Rodrigo Gonalves.jpeg
Normal file
|
After Width: | Height: | Size: 70 KiB |