Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
TYPEORM_CONNECTION=
TYPEORM_HOST=
TYPEORM_USERNAME=
TYPEORM_PASSWORD=
TYPEORM_DATABASE=
TYPEORM_PORT=
TYPEORM_SYNCHRONIZE=
TYPEORM_LOGGING=
TYPEORM_ENTITIES=

JWT_SECRET=

RIOT_API_KEY=

DEBUG=api*
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
node_modules/
build/

*.sqlite
.env

.DS_store
44 changes: 44 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
{
"name": "backend-challenge",
"version": "1.0.0",
"main": "index.js",
"repository": "git@github.com:SkyLkr/backend-challenge.git",
"author": "Adriano Rocha <adriano.olr@gmail.com>",
"license": "MIT",
"scripts": {
"dev": "ts-node-dev --respawn --transpile-only --ignore-watch node_modules --no-notify src/server.ts",
"test": "npx mocha -r ts-node/register test/**/*.test.ts --parallel --exit",
"test:dev": "npx mocha -r ts-node/register test/**/*.test.ts --watch --extension ts",
"test:cov": "npx nyc --reporter=html --reporter=text npm run test",
"build": "tsc"
},
"devDependencies": {
"@types/bcrypt": "^5.0.0",
"@types/chai": "^4.2.19",
"@types/debug": "^4.1.5",
"@types/express": "^4.17.12",
"@types/jsonwebtoken": "^8.5.2",
"@types/mocha": "^8.2.2",
"@types/node": "^15.12.4",
"@types/node-xlsx": "^0.15.1",
"@types/sinon": "^10.0.2",
"chai": "^4.3.4",
"mocha": "^9.0.1",
"nyc": "^15.1.0",
"sinon": "^11.1.1",
"ts-node-dev": "^1.1.6",
"typescript": "^4.3.4"
},
"dependencies": {
"axios": "^0.21.1",
"bcrypt": "^5.0.1",
"debug": "^4.3.1",
"dotenv": "^10.0.0",
"express": "^4.17.1",
"jsonwebtoken": "^8.5.1",
"node-xlsx": "^0.16.1",
"pg": "^8.6.0",
"reflect-metadata": "^0.1.13",
"typeorm": "^0.2.34"
}
}
13 changes: 13 additions & 0 deletions src/app.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import express from 'express';

import routes from './routes';

export default function createApp() {
const app = express();

app.use(express.json());

app.use(routes);

return app;
}
17 changes: 17 additions & 0 deletions src/authenticatedRoutes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Router } from 'express'

import authenticationMiddleware from './middlewares/authenticationMiddleware';
import SummonerController from './controllers/SummonerController';

const authenticatedRoutes = Router();

authenticatedRoutes.use(authenticationMiddleware);

authenticatedRoutes.get('/summoners', SummonerController.index);
authenticatedRoutes.get('/summoners/detail', SummonerController.detail);
authenticatedRoutes.post('/summoners', SummonerController.create);
authenticatedRoutes.put('/summoners/:id', SummonerController.update);
authenticatedRoutes.delete('/summoners/:id', SummonerController.delete);
authenticatedRoutes.post('/summoners/export', SummonerController.export);

export default authenticatedRoutes;
203 changes: 203 additions & 0 deletions src/controllers/SummonerController.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
import { AxiosError } from 'axios';
import { debug } from 'debug';
import { Response } from 'express';
import { getRepository } from 'typeorm';

import { Summoner } from '../entity/Summoner';
import { User } from '../entity/User';

import { AuthenticatedRequest } from '../middlewares/authenticationMiddleware';
import riotApi from '../services/riotApi';
import exportSummonersToXlsx from '../utils/exportSummonersToXlsx';
import prettifyPromise from '../utils/prettifyPromise';

const log = debug('api:controllers:summoner');

export default class SummonerController {
static async create(request: AuthenticatedRequest, response: Response) {
const { summonerName } = request.body;

const userRepository = getRepository(User);
const summonerRepository = getRepository(Summoner);

const [user, findUserError] = await prettifyPromise(userRepository.findOneOrFail(request.userId));

if (findUserError) {
return response.status(401).json({ error: 'Usuário inválido' });
}

const [apiResponse, apiError] = await prettifyPromise(riotApi.get(`/lol/summoner/v4/summoners/by-name/${summonerName}`));

if (apiError) {
if ((apiError as AxiosError).response?.status === 404) {
return response.status(404).json({ error: 'Summoner não encontrado' });
}

return response.status(500).json({ error: 'Erro ao se comunicar com a API da Rito' });
}

const { id, name, accountId, summonerLevel, profileIconId } = apiResponse.data;

const summonerExists = await summonerRepository.findOne({
where: [
{ accountId },
{ summonerId: id }
],
});

if (summonerExists) {
return response.status(409).json({ error: 'Summoner já foi cadastrado no sistema' });
}

const [summoner, createSummonerError] = await prettifyPromise(summonerRepository.save({
nickname: name,
accountId,
summonerLevel,
profileIconId,
summonerId: id,
user
}));

if (createSummonerError) {
return response.status(400).json({ error: 'Erro ao criar summoner' });
}

return response.status(201).send();
}

static async index(request: AuthenticatedRequest, response: Response) {
const summonerRepository = getRepository(Summoner);

const [summoners, findSummonersError] = await prettifyPromise(summonerRepository.find({
where: {
user: {
id: request.userId,
},
},
}));

if (findSummonersError) {
log(findSummonersError.stack);
return response.status(500).json({ error: 'Erro ao listar summoners' });
}

return response.json(summoners);
}

static async detail(request: AuthenticatedRequest, response: Response) {
const summonerRepository = getRepository(Summoner);

const [summoners, findSummonersError] = await prettifyPromise(summonerRepository.find({
where: {
user: {
id: request.userId,
},
},
}));

if (findSummonersError) {
log(findSummonersError.stack);
return response.status(500).json({ error: 'Erro ao listar summoners' });
}

const [detailedSummoners, fetchSummonersDetailsError] = await prettifyPromise(
Promise.all(summoners.map(async summoner => {
type ResponseType = Array<{ wins: number, losses: number }>;

const apiResponse = await riotApi.get<ResponseType>(
`/lol/league/v4/entries/by-summoner/${summoner.summonerId}`
);

const wins = apiResponse
.data
.map(entry => entry.wins)
.reduce((sum, wins) => sum + wins, 0);

const losses = apiResponse
.data
.map(entry => entry.losses)
.reduce((sum, losses) => sum + losses, 0);

return {
...summoner,
wins,
losses,
}
}))
);

if (fetchSummonersDetailsError) {
log(fetchSummonersDetailsError);
return response.status(500).json({ error: 'Erro ao se comunicar com a API da Rito Gomes' });
}

return response.json(detailedSummoners);
}

static async update(request: AuthenticatedRequest, response: Response) {
const { id } = request.params;
const { summonerName, summonerLevel } = request.body;

const summonerRepository = getRepository(Summoner);

const [summoner, findSummonerError] = await prettifyPromise(summonerRepository.findOneOrFail(id));

if (findSummonerError) {
return response.status(404).json({ error: 'Summoner não encontrado' });
}

summoner.nickname = summonerName ?? summoner.nickname;
summoner.summonerLevel = summonerLevel ?? summoner.summonerLevel;

const [, error] = await prettifyPromise(summonerRepository.save(summoner));

if (error) {
log(error.stack);
return response.status(500).json({ error: 'Erro ao salvar alterações no summoner' });
}

return response.json(summoner);
}

static async delete(request: AuthenticatedRequest, response: Response) {
const { id } = request.params;

const summonerRepository = getRepository(Summoner);

const [, error] = await prettifyPromise(summonerRepository.delete({ id, user: { id: request.userId } }));

if (error) {
return response.status(500).json({ error: 'Erro ao remover summoner' });
}

return response.json({ message: 'successfully deleted' });
}

static async export(request: AuthenticatedRequest, response: Response) {
const summonerRepository = getRepository(Summoner);

const [summoners, findSummonersError] = await prettifyPromise(
summonerRepository.find({
where: {
user: {
id: request.userId,
},
},
}),
);

if (findSummonersError) {
log(findSummonersError.stack);
return response.status(500).json({ error: 'Erro ao buscar summoners' });
}

const exportFile = exportSummonersToXlsx(summoners);

return response
.header({
'Content-Type': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'Content-Disposition': 'attachment; filename=summoners.xlsx',
})
.send(exportFile);
}
}
63 changes: 63 additions & 0 deletions src/controllers/UserController.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import debug from 'debug';
import { Request, Response } from 'express';
import { getRepository } from 'typeorm';

import { User } from '../entity/User';
import { generateAccessToken } from '../utils/accessToken';
import { compareHashedPassword, generateHashedPassword } from '../utils/hashPassword';
import prettifyPromise from '../utils/prettifyPromise';

const log = debug('api:controllers:user');

export default class UserController {
static async create(request: Request, response: Response) {
const { name, email, password } = request.body;

const userRepository = getRepository(User);

const userExists = await userRepository.findOne({ where: { email } });

if (userExists) {
return response.status(409).json({ error: 'Já existe um usuário cadastrado com o mesmo email' });
}

const [hashedPassword, passwordError] = await prettifyPromise(generateHashedPassword(password));

const [user, saveUserError] = await prettifyPromise(userRepository.save({ name, email, password: hashedPassword }));

if (passwordError || saveUserError) {
log((passwordError || saveUserError).stack);
return response.status(400).json({ error: 'Erro ao criar usuário' });
}

return response.status(201).json(user);
}

static async authenticate(request: Request, response: Response) {
const { name, email, password } = request.body;

const userRepository = getRepository(User);

const [user, findUserError] = await prettifyPromise(userRepository.findOneOrFail({ where: { name, email } }));

if (findUserError) {
log(findUserError);
return response.status(404).json({ error: 'Usuário não encontrado' });
}

const passwordIsValid = await compareHashedPassword(password, user.password);

if (!passwordIsValid) {
return response.status(403).json({ error: 'Senha inválida' });
}

const [token, generateJwtError] = await prettifyPromise(generateAccessToken(user));

if (generateJwtError) {
log(generateJwtError.stack);
return response.status(500).json({ error: 'Erro na autenticação do usuário' });
}

return response.json({ token });
}
}
Loading