Skip to content

Commit 9245756

Browse files
feature: create the API with authorization and registration handled by the back-end server.
1 parent 29ce9ad commit 9245756

25 files changed

+3750
-1
lines changed

.editorconfig

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# http://editorconfig.org
2+
3+
# A special property that should be specified at the top of the file outside of
4+
# any sections. When set to true, stops Editorconfig from searching any higher
5+
# in the directory tree for .editorconfig files.
6+
root = true
7+
8+
[*]
9+
# Indentation style
10+
# Possible values - tab, space
11+
indent_style = space
12+
13+
# Indentation size in single-spaced characters
14+
# Possible values - an integer, tab
15+
indent_size = 2
16+
17+
# Newline character(s) to use (varies by OS otherwise)
18+
# Possible values - lf, crlf, cr
19+
end_of_line = lf
20+
21+
# File character encoding
22+
# Possible values - latin1, utf-8, utf-16be, utf-16le
23+
charset = utf-8
24+
25+
# Denotes whether to trim whitespace at the end of lines
26+
# Possible values - true, false
27+
trim_trailing_whitespace = true
28+
29+
[*.md]
30+
trim_trailing_whitespace = false
31+
32+
# Denotes whether file should end with a newline
33+
# Possible values - true, false
34+
insert_final_newline = true

.eslintrc

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"extends": "airbnb-base",
3+
"rules": {
4+
"no-underscore-dangle": ["error", { "allow": ["_id"] }],
5+
"no-console": "off",
6+
"no-unused-vars": ["error", { "argsIgnorePattern": "next" }]
7+
}
8+
}

.gitignore

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Logs
2+
logs
3+
*.log
4+
npm-debug.log*
5+
6+
# Dependency directory
7+
node_modules
8+
9+
# Optional npm cache directory
10+
.npm
11+
12+
# System files and IDE settings folders
13+
.DS_Store
14+
.idea
15+
.vscode
16+
17+
.env

README.md

+7-1
Original file line numberDiff line numberDiff line change
@@ -1 +1,7 @@
1-
# news-explorer-backend
1+
# news-explorer-backend
2+
3+
The API of "News Explorer" app with authorization and registration handled by the back-end server.
4+
5+
This repository contains back-end of "News Explorer" project that features user authorization and user registration and handles users and saved articles.
6+
7+
* [a link to the API](https://api.explorernewsapp.students.nomoreparties.site)

app.js

+49
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
const express = require('express');
2+
const mongoose = require('mongoose');
3+
const helmet = require('helmet');
4+
const cors = require('cors');
5+
const { errors } = require('celebrate');
6+
const routes = require('./routes');
7+
const config = require('./config');
8+
const handleError = require('./middlewares/handleError');
9+
const NotFoundError = require('./errors/NotFoundError');
10+
const { requestLogger, errorLogger } = require('./middlewares/logger');
11+
const rateLimiter = require('./middlewares/rateLimiter');
12+
13+
const { PORT = 3000 } = process.env;
14+
15+
const app = express();
16+
17+
require('dotenv').config();
18+
19+
app.use(helmet());
20+
21+
app.use(express.urlencoded({ extended: true }));
22+
app.use(express.json());
23+
24+
app.use(cors());
25+
app.options('*', cors());
26+
27+
mongoose.connect(process.env.NODE_ENV === 'production' ? process.env.MONGODB_HOST : config.db.host);
28+
29+
app.use(requestLogger);
30+
31+
app.use(rateLimiter);
32+
33+
app.use('/', routes);
34+
35+
app.use((req, res, next) => {
36+
next(new NotFoundError('Requested resource not found'));
37+
});
38+
39+
app.use(errorLogger);
40+
41+
app.use(errors());
42+
43+
app.use((err, req, res, next) => {
44+
handleError(err, res);
45+
});
46+
47+
app.listen(PORT, () => {
48+
console.log(`App listening at port ${PORT}`);
49+
});

config.js

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
const config = {
2+
db: {
3+
host: 'mongodb://localhost:27017/newsdb',
4+
},
5+
jwt: {
6+
devKey: 'b06e69b88dbbe0fdfe76f90af191777318f414fb532337e5ec723dd8ec19ef99',
7+
},
8+
};
9+
10+
module.exports = config;

controllers/articles.js

+59
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
const Article = require('../models/article');
2+
const BadRequestError = require('../errors/BadRequestError');
3+
const ForbiddenError = require('../errors/ForbiddenError');
4+
const NotFoundError = require('../errors/NotFoundError');
5+
6+
module.exports.getSavedArticles = (req, res, next) => {
7+
Article.find({ owner: req.user._id })
8+
.orFail(() => {
9+
throw new NotFoundError('No articles found');
10+
})
11+
.then((articles) => {
12+
res.status(200).send({ data: articles });
13+
})
14+
.catch(next);
15+
};
16+
17+
module.exports.createSavedArticle = (req, res, next) => {
18+
Article.create({ owner: req.user._id, ...req.body })
19+
.then((createdArticle) => {
20+
Article.findById(createdArticle._id)
21+
.then((article) => res.status(201).send({ data: article }))
22+
.catch(next);
23+
})
24+
.catch((err) => {
25+
if (err.name === 'ValidationError') {
26+
next(new BadRequestError(err.message));
27+
return;
28+
}
29+
next(err);
30+
});
31+
};
32+
33+
module.exports.removeSavedArticle = (req, res, next) => {
34+
const { articleId } = req.params;
35+
36+
Article.findById(articleId)
37+
.orFail(() => {
38+
throw new NotFoundError('Article ID not found');
39+
})
40+
.select('+owner')
41+
.then((article) => {
42+
if (article.owner.equals(req.user._id)) {
43+
Article.deleteOne(article)
44+
.then(() => {
45+
res.status(200).send({ message: 'The saved article has been successfully removed' });
46+
})
47+
.catch(next);
48+
} else {
49+
throw new ForbiddenError('Forbidden to remove an article saved by another user');
50+
}
51+
})
52+
.catch((err) => {
53+
if (err.name === 'CastError') {
54+
next(new BadRequestError('Incorrect ID'));
55+
return;
56+
}
57+
next(err);
58+
});
59+
};

controllers/users.js

+55
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
const jwt = require('jsonwebtoken');
2+
const bcrypt = require('bcryptjs');
3+
const User = require('../models/user');
4+
const NotFoundError = require('../errors/NotFoundError');
5+
const ConflictError = require('../errors/ConflictError');
6+
const BadRequestError = require('../errors/BadRequestError');
7+
const config = require('../config');
8+
9+
module.exports.login = (req, res, next) => {
10+
const { email, password } = req.body;
11+
User.findUserByCredentials(email, password)
12+
.then((user) => {
13+
const token = jwt.sign(
14+
{ _id: user._id },
15+
process.env.NODE_ENV === 'production' ? process.env.JWT_SECRET : config.jwt.devKey,
16+
{ expiresIn: '7d' },
17+
);
18+
res.status(200).send({ token });
19+
})
20+
.catch(next);
21+
};
22+
23+
module.exports.createUser = (req, res, next) => {
24+
const { password } = req.body;
25+
bcrypt.hash(password, 10)
26+
.then((hash) => User.create({
27+
...req.body,
28+
password: hash,
29+
}))
30+
.then((createdUser) => {
31+
User.findById(createdUser._id)
32+
.then((user) => res.status(201).send({ data: user }))
33+
.catch(next);
34+
})
35+
.catch((err) => {
36+
if (err.name === 'ValidationError') {
37+
next(new BadRequestError(err.message));
38+
} else if (err.code === 11000) {
39+
next(new ConflictError('User with this email address already exists.'));
40+
} else {
41+
next(err);
42+
}
43+
});
44+
};
45+
46+
module.exports.getUserInfo = (req, res, next) => {
47+
User.findById(req.user._id)
48+
.orFail(() => {
49+
throw new NotFoundError('User ID not found');
50+
})
51+
.then((user) => {
52+
res.status(200).send({ data: user });
53+
})
54+
.catch(next);
55+
};

errors/BadRequestError.js

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
class BadRequestError extends Error {
2+
constructor(message) {
3+
super(message);
4+
this.statusCode = 400;
5+
}
6+
}
7+
8+
module.exports = BadRequestError;

errors/ConflictError.js

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
class ConflictError extends Error {
2+
constructor(message) {
3+
super(message);
4+
this.statusCode = 409;
5+
}
6+
}
7+
8+
module.exports = ConflictError;

errors/ForbiddenError.js

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
class ForbiddenError extends Error {
2+
constructor(message) {
3+
super(message);
4+
this.statusCode = 403;
5+
}
6+
}
7+
8+
module.exports = ForbiddenError;

errors/NotFoundError.js

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
class NotFoundError extends Error {
2+
constructor(message) {
3+
super(message);
4+
this.statusCode = 404;
5+
}
6+
}
7+
8+
module.exports = NotFoundError;

errors/UnauthorizedError.js

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
class UnauthorizedError extends Error {
2+
constructor(message) {
3+
super(message);
4+
this.statusCode = 401;
5+
}
6+
}
7+
8+
module.exports = UnauthorizedError;

middlewares/auth.js

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
const jwt = require('jsonwebtoken');
2+
const config = require('../config');
3+
const UnauthorizedError = require('../errors/UnauthorizedError');
4+
5+
module.exports = (req, res, next) => {
6+
const { authorization } = req.headers;
7+
8+
if (!authorization || !authorization.startsWith('Bearer ')) {
9+
next(new UnauthorizedError('To have access to the content, authorization required'));
10+
return;
11+
}
12+
13+
const token = authorization.replace('Bearer ', '');
14+
let payload;
15+
16+
try {
17+
payload = jwt.verify(token, process.env.NODE_ENV === 'production' ? process.env.JWT_SECRET : config.jwt.devKey);
18+
} catch (err) {
19+
next(new UnauthorizedError('Invalid token. Authorization required'));
20+
return;
21+
}
22+
23+
req.user = payload;
24+
25+
next();
26+
};

middlewares/handleError.js

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
const handleError = (err, res) => {
2+
const { message, statusCode = 500 } = err;
3+
4+
res
5+
.status(statusCode)
6+
.send({
7+
message:
8+
statusCode === 500
9+
? 'An error has occurred on the server'
10+
: message,
11+
});
12+
};
13+
14+
module.exports = handleError;

middlewares/logger.js

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
const winston = require('winston');
2+
const expressWinston = require('express-winston');
3+
4+
const requestLogger = expressWinston.logger({
5+
transports: [
6+
new winston.transports.File({ filename: './logs/request.log' }),
7+
],
8+
format: winston.format.json(),
9+
});
10+
11+
const errorLogger = expressWinston.errorLogger({
12+
transports: [
13+
new winston.transports.File({ filename: './logs/error.log' }),
14+
],
15+
format: winston.format.json(),
16+
});
17+
18+
module.exports = {
19+
requestLogger,
20+
errorLogger,
21+
};

middlewares/rateLimiter.js

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
const rateLimit = require('express-rate-limit');
2+
3+
const rateLimiter = rateLimit({
4+
windowMs: 10 * 60 * 1000,
5+
max: 90,
6+
});
7+
8+
module.exports = rateLimiter;

0 commit comments

Comments
 (0)