functional RESTful API
This commit is contained in:
parent
e5ac1a96bb
commit
7ea24df56b
14 changed files with 2677 additions and 1 deletions
65
.gitignore
vendored
Normal file
65
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# Typescript v1 declaration files
|
||||
typings/
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
|
||||
# next.js build output
|
||||
.next
|
||||
|
||||
# deta-space internal files
|
||||
.space
|
||||
Spacefile
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
# express-blog-api
|
||||
|
||||
Backend with two different front-ends for accessing and editing blog posts. One of the front-end sites will be for people that want to read and comment on posts while the other one will allow editing, writing and publishing of posts.
|
||||
RESTful blog API implemented in Express. Has POST/GET/PUT/DELETE methods for all posts and comments.
|
||||
Post Creation, Updates and Deletion require jwt token returned when post completed at /api/login.
|
||||
Hosted on Deta Space at https://expressblogapi-1-v2871156.deta.app/api/
|
||||
|
|
|
|||
41
app.js
Normal file
41
app.js
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
const express = require("express");
|
||||
const path = require("path");
|
||||
const cors = require("cors");
|
||||
const mongoose = require("mongoose");
|
||||
require("dotenv").config();
|
||||
|
||||
const apiRouter = require("./routes/api.js");
|
||||
|
||||
const app = express();
|
||||
|
||||
// get db
|
||||
const mongoDB = `mongodb+srv://${process.env.USER}:${process.env.PASS}@odin.eftl02o.mongodb.net/blogAPI?retryWrites=true&w=majority`;
|
||||
// or throw error
|
||||
main().catch((err) => console.log(err));
|
||||
async function main() {
|
||||
await mongoose.connect(mongoDB);
|
||||
}
|
||||
|
||||
// set paths
|
||||
app.use(
|
||||
"/styles/css",
|
||||
express.static(path.join(__dirname, "node_modules/bootstrap/dist/css"))
|
||||
);
|
||||
app.use(express.static(path.join(__dirname, "public")));
|
||||
|
||||
// view engine setup
|
||||
app.set("views", path.join(__dirname, "views"));
|
||||
app.set("view engine", "ejs");
|
||||
|
||||
// parsing
|
||||
app.use(express.json());
|
||||
app.use(express.urlencoded({ extended: false }));
|
||||
app.use(express.static(path.join(__dirname, "public")));
|
||||
|
||||
// cors
|
||||
app.use(cors());
|
||||
|
||||
// routing
|
||||
app.use("/api", apiRouter);
|
||||
|
||||
module.exports = app;
|
||||
90
bin/www
Executable file
90
bin/www
Executable file
|
|
@ -0,0 +1,90 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Module dependencies.
|
||||
*/
|
||||
|
||||
var app = require('../app');
|
||||
var debug = require('debug')('express-blog-api:server');
|
||||
var http = require('http');
|
||||
|
||||
/**
|
||||
* Get port from environment and store in Express.
|
||||
*/
|
||||
|
||||
var port = normalizePort(process.env.PORT || '3000');
|
||||
app.set('port', port);
|
||||
|
||||
/**
|
||||
* Create HTTP server.
|
||||
*/
|
||||
|
||||
var server = http.createServer(app);
|
||||
|
||||
/**
|
||||
* Listen on provided port, on all network interfaces.
|
||||
*/
|
||||
|
||||
server.listen(port);
|
||||
server.on('error', onError);
|
||||
server.on('listening', onListening);
|
||||
|
||||
/**
|
||||
* Normalize a port into a number, string, or false.
|
||||
*/
|
||||
|
||||
function normalizePort(val) {
|
||||
var port = parseInt(val, 10);
|
||||
|
||||
if (isNaN(port)) {
|
||||
// named pipe
|
||||
return val;
|
||||
}
|
||||
|
||||
if (port >= 0) {
|
||||
// port number
|
||||
return port;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Event listener for HTTP server "error" event.
|
||||
*/
|
||||
|
||||
function onError(error) {
|
||||
if (error.syscall !== 'listen') {
|
||||
throw error;
|
||||
}
|
||||
|
||||
var bind = typeof port === 'string'
|
||||
? 'Pipe ' + port
|
||||
: 'Port ' + port;
|
||||
|
||||
// handle specific listen errors with friendly messages
|
||||
switch (error.code) {
|
||||
case 'EACCES':
|
||||
console.error(bind + ' requires elevated privileges');
|
||||
process.exit(1);
|
||||
break;
|
||||
case 'EADDRINUSE':
|
||||
console.error(bind + ' is already in use');
|
||||
process.exit(1);
|
||||
break;
|
||||
default:
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Event listener for HTTP server "listening" event.
|
||||
*/
|
||||
|
||||
function onListening() {
|
||||
var addr = server.address();
|
||||
var bind = typeof addr === 'string'
|
||||
? 'pipe ' + addr
|
||||
: 'port ' + addr.port;
|
||||
debug('Listening on ' + bind);
|
||||
}
|
||||
155
controllers/comment.js
Normal file
155
controllers/comment.js
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
const { default: mongoose } = require("mongoose");
|
||||
const asyncHandler = require("express-async-handler");
|
||||
const { body, validationResult } = require("express-validator");
|
||||
|
||||
const Comment = require("../models/comment.js");
|
||||
|
||||
// makes new comment - C
|
||||
exports.post = [
|
||||
// Validate and sanitize text
|
||||
body("text", "Please enter comment!").isLength({ min: 1 }).trim().escape(),
|
||||
|
||||
// Validate and sanitize text
|
||||
body("author", "Please enter comment author!")
|
||||
.isLength({ min: 1 })
|
||||
.trim()
|
||||
.escape(),
|
||||
|
||||
// Validate and sanitize password
|
||||
body("password", "Please enter password for future comment modification!")
|
||||
.isLength({ min: 1 })
|
||||
.trim()
|
||||
.escape(),
|
||||
|
||||
// Process request after authentication, validation and sanitization
|
||||
asyncHandler(async (req, res, next) => {
|
||||
const errors = validationResult(req);
|
||||
|
||||
// if there are validation errors, render with errors
|
||||
if (!errors.isEmpty()) {
|
||||
return res.status(400).json({
|
||||
message: "Comment produced validation errors!",
|
||||
errors: errors.array(),
|
||||
});
|
||||
}
|
||||
|
||||
// else data is valid, make post object
|
||||
const comment = new Comment({
|
||||
date: new Date(),
|
||||
text: req.body.text,
|
||||
author: req.body.author,
|
||||
post: req.params.postID,
|
||||
_id: new mongoose.Types.ObjectId(),
|
||||
password: req.body.password,
|
||||
});
|
||||
|
||||
// save to DB
|
||||
await comment.save();
|
||||
|
||||
return res.status(200).json({
|
||||
message: "Comment uploaded!",
|
||||
});
|
||||
}),
|
||||
];
|
||||
|
||||
// returns comment in json format - R
|
||||
exports.get = asyncHandler(async (req, res, next) => {
|
||||
const comment = await Comment.findOne({ _id: req.params.commentID })
|
||||
.lean()
|
||||
.exec();
|
||||
return res.status(200).json({ comment });
|
||||
});
|
||||
|
||||
// updates comment - U
|
||||
exports.put = [
|
||||
// Validate and sanitize text
|
||||
body("text", "Please enter comment!").isLength({ min: 1 }).trim().escape(),
|
||||
|
||||
// Validate and sanitize author name
|
||||
body("author", "Please enter comment author!")
|
||||
.isLength({ min: 1 })
|
||||
.trim()
|
||||
.escape(),
|
||||
|
||||
// Validate and sanitize password
|
||||
body("password", "Please enter password to modify comment!")
|
||||
.isLength({ min: 1 })
|
||||
.trim()
|
||||
.escape(),
|
||||
|
||||
// Process request after sanitization and validation
|
||||
asyncHandler(async (req, res, next) => {
|
||||
const errors = validationResult(req);
|
||||
|
||||
// if there are validation errors, render with errors
|
||||
if (!errors.isEmpty()) {
|
||||
return res.status(400).json({
|
||||
message: "Comment produced validation errors!",
|
||||
errors: errors.array(),
|
||||
});
|
||||
}
|
||||
|
||||
const dbComment = await Comment.findOne({ _id: req.params.commentID })
|
||||
.lean()
|
||||
.exec();
|
||||
|
||||
if (req.body.author === dbComment.author) {
|
||||
if (req.body.password === dbComment.password) {
|
||||
const comment = {
|
||||
date: new Date(),
|
||||
text: req.body.text,
|
||||
author: req.body.author,
|
||||
post: dbComment.post,
|
||||
_id: dbComment._id,
|
||||
password: dbComment.password,
|
||||
};
|
||||
|
||||
await comment.save();
|
||||
return res.status(200).json({
|
||||
message: "Post updated!",
|
||||
});
|
||||
}
|
||||
return res.status(401).json({
|
||||
message: "Comments can only be updated with their original password!",
|
||||
});
|
||||
}
|
||||
return res.status(401).json({
|
||||
message: "Comments can only be updated by their original author!",
|
||||
});
|
||||
}),
|
||||
];
|
||||
|
||||
// deletes a comment - D
|
||||
exports.delete = [
|
||||
// Validate and sanitize password
|
||||
body("password", "Please enter comment deletion password!")
|
||||
.isLength({ min: 1 })
|
||||
.trim()
|
||||
.escape(),
|
||||
|
||||
// Process request after authentication, validation and sanitization
|
||||
asyncHandler(async (req, res, next) => {
|
||||
const errors = validationResult(req);
|
||||
|
||||
// if there are validation errors, render with errors
|
||||
if (!errors.isEmpty()) {
|
||||
return res.status(400).json({
|
||||
message: "Comment password produced validation errors!",
|
||||
errors: errors.array(),
|
||||
});
|
||||
}
|
||||
|
||||
const dbComment = await Comment.findOne({ _id: req.params.commentID })
|
||||
.lean()
|
||||
.exec();
|
||||
|
||||
if (req.body.password === dbComment.password) {
|
||||
await Comment.findByIdAndDelete({ _id: req.params.commentID }).exec();
|
||||
return res.status(200).json({ message: "Comment deleted!" });
|
||||
}
|
||||
|
||||
return res.status(401).json({
|
||||
message: "Incorrect password!",
|
||||
});
|
||||
}),
|
||||
];
|
||||
24
controllers/login.js
Normal file
24
controllers/login.js
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
const asyncHandler = require("express-async-handler");
|
||||
const jwt = require("jsonwebtoken");
|
||||
const bcrypt = require("bcryptjs");
|
||||
const User = require("../models/user.js");
|
||||
|
||||
let opts = {};
|
||||
|
||||
exports.post = asyncHandler(async (req, res, next) => {
|
||||
const admin = await User.findOne({}).lean().exec(); // only one user in DB - admin. pull it up, jamie!
|
||||
const { username, password } = req.body; // get fields from body
|
||||
if (username === admin.username) {
|
||||
console.log();
|
||||
const match = await bcrypt.compare(password, admin.password); // compare bcrypt hashed passwords
|
||||
if (match) {
|
||||
opts.expiresIn = 120;
|
||||
const token = jwt.sign({ username }, process.env.SECRET_KEY, opts); // create token and return below
|
||||
return res.status(200).json({
|
||||
message: "Authentication complete",
|
||||
token,
|
||||
});
|
||||
}
|
||||
}
|
||||
return res.status(401).json({ message: "Authentication failed" }); // get rekt nerd
|
||||
});
|
||||
124
controllers/post.js
Normal file
124
controllers/post.js
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
const { default: mongoose } = require("mongoose");
|
||||
const asyncHandler = require("express-async-handler");
|
||||
const { body, validationResult } = require("express-validator");
|
||||
|
||||
const Post = require("../models/post.js");
|
||||
const Comment = require("../models/comment.js");
|
||||
|
||||
const passport = require("passport");
|
||||
const jwtStrategy = require("../strategy/jwt.js");
|
||||
passport.use(jwtStrategy);
|
||||
|
||||
// returns json object with ALL posts and comments
|
||||
exports.index = asyncHandler(async (req, res, next) => {
|
||||
const dbPosts = await Post.find().lean().exec();
|
||||
const posts = [];
|
||||
for (let i = 0; i < dbPosts.length; i++) {
|
||||
const comments = await Comment.find({ post: dbPosts[i]._id });
|
||||
const post = {
|
||||
title: dbPosts[i].title,
|
||||
date: dbPosts[i].date,
|
||||
text: dbPosts[i].text,
|
||||
author: dbPosts[i].author,
|
||||
published: dbPosts[i].published,
|
||||
_id: dbPosts[i]._id,
|
||||
comments: comments,
|
||||
};
|
||||
posts.push(post);
|
||||
}
|
||||
return res.status(200).json({ posts });
|
||||
});
|
||||
|
||||
// makes new post - C
|
||||
(exports.post = passport.authenticate("jwt", { session: false })),
|
||||
[
|
||||
// Validate and sanitize title
|
||||
body("title", "Please enter blog post title!")
|
||||
.isLength({ min: 1 })
|
||||
.trim()
|
||||
.escape(),
|
||||
|
||||
// Validate and sanitize text
|
||||
body("text", "Please enter blog post text!")
|
||||
.isLength({ min: 1 })
|
||||
.trim()
|
||||
.escape(),
|
||||
|
||||
// Process request after authentication, validation and sanitization
|
||||
asyncHandler(async (req, res, next) => {
|
||||
const errors = validationResult(req);
|
||||
|
||||
// if there are validation errors, render with errors
|
||||
if (!errors.isEmpty()) {
|
||||
res.render("/admin/create"),
|
||||
{
|
||||
errors: errors.array(),
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
// else data is valid, make post object
|
||||
const post = new Post({
|
||||
title: req.body.title,
|
||||
date: new Date(),
|
||||
text: req.body.text,
|
||||
author: req.user.username,
|
||||
_id: new mongoose.Types.ObjectId(),
|
||||
});
|
||||
|
||||
// save to DB
|
||||
await post.save();
|
||||
|
||||
return res.status(200).json({
|
||||
message: "Post created!",
|
||||
});
|
||||
}),
|
||||
];
|
||||
|
||||
// returns post in json format - R
|
||||
exports.get = asyncHandler(async (req, res, next) => {
|
||||
const post = await Post.findOne({ _id: req.params.postID }).lean().exec();
|
||||
return res.status(200).json({ post });
|
||||
});
|
||||
|
||||
// updates post - U
|
||||
(exports.put = passport.authenticate("jwt", { session: false })),
|
||||
[
|
||||
// Validate and sanitize title
|
||||
body("title", "Please enter blog post title!")
|
||||
.isLength({ min: 1 })
|
||||
.trim()
|
||||
.escape(),
|
||||
|
||||
// Validate and sanitize text
|
||||
body("text", "Please enter blog post text!")
|
||||
.isLength({ min: 1 })
|
||||
.trim()
|
||||
.escape(),
|
||||
|
||||
// Process request after sanitization and validation
|
||||
asyncHandler(async (req, res, next) => {
|
||||
const dbPost = await Post.findOne({ _id: req.params.postID })
|
||||
.lean()
|
||||
.exec();
|
||||
const post = {
|
||||
title: req.body.title,
|
||||
date: new Date(),
|
||||
text: req.body.text,
|
||||
author: dbPost.author,
|
||||
published: dbPost.published,
|
||||
_id: dbPost._id,
|
||||
};
|
||||
await post.save();
|
||||
return res.status(200).json({
|
||||
message: "Post updated!",
|
||||
});
|
||||
}),
|
||||
];
|
||||
|
||||
// deletes a post - D
|
||||
(exports.delete = passport.authenticate("jwt", { session: false })),
|
||||
asyncHandler(async (req, res, next) => {
|
||||
await Post.findByIdAndDelete({ _id: req.params.postID }).exec();
|
||||
return res.status(200).json({ message: "Post deleted!" });
|
||||
});
|
||||
20
models/comment.js
Normal file
20
models/comment.js
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
const mongoose = require("mongoose");
|
||||
const Schema = mongoose.Schema;
|
||||
|
||||
const CommentSchema = new Schema({
|
||||
date: { type: Date, required: true },
|
||||
text: { type: String, required: true },
|
||||
author: { type: String, required: true },
|
||||
post: { type: mongoose.ObjectId, required: true },
|
||||
_id: { type: mongoose.ObjectId, required: true },
|
||||
password: { type: String, required: true },
|
||||
});
|
||||
|
||||
// Virtual for comment RESTful functions
|
||||
CommentSchema.virtual("url").get(function () {
|
||||
// We don't use an arrow function as we'll need the this object
|
||||
return `/api/${post}/${this._id}`;
|
||||
});
|
||||
|
||||
// Export model
|
||||
module.exports = mongoose.model("Comment", CommentSchema);
|
||||
20
models/post.js
Normal file
20
models/post.js
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
const mongoose = require("mongoose");
|
||||
const Schema = mongoose.Schema;
|
||||
|
||||
const PostSchema = new Schema({
|
||||
title: { type: String, required: true },
|
||||
date: { type: Date, required: true },
|
||||
text: { type: String, required: true },
|
||||
author: { type: String, required: true },
|
||||
published: { type: Boolean, required: true },
|
||||
_id: { type: mongoose.ObjectId, required: true },
|
||||
});
|
||||
|
||||
// Virtual for message URL
|
||||
PostSchema.virtual("url").get(function () {
|
||||
// We don't use an arrow function as we'll need the this object
|
||||
return `/api/${this._id}`;
|
||||
});
|
||||
|
||||
// Export model
|
||||
module.exports = mongoose.model("Post", PostSchema);
|
||||
12
models/user.js
Normal file
12
models/user.js
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
const mongoose = require("mongoose");
|
||||
const Schema = mongoose.Schema;
|
||||
|
||||
const UserSchema = new Schema({
|
||||
username: { type: String, required: true },
|
||||
password: { type: String, required: true },
|
||||
});
|
||||
|
||||
// we will NOT be exposing any URLs for users
|
||||
|
||||
// Export model
|
||||
module.exports = mongoose.model("User", UserSchema);
|
||||
2039
package-lock.json
generated
Normal file
2039
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
27
package.json
Normal file
27
package.json
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"name": "express-blog-api",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "node ./bin/www"
|
||||
},
|
||||
"dependencies": {
|
||||
"bcryptjs": "^2.4.3",
|
||||
"bootstrap": "^5.3.1",
|
||||
"cors": "^2.8.5",
|
||||
"debug": "~2.6.9",
|
||||
"dotenv": "^16.3.1",
|
||||
"ejs": "^3.1.9",
|
||||
"ejs-lint": "^2.0.0",
|
||||
"express": "^4.18.2",
|
||||
"express-async-handler": "^1.2.0",
|
||||
"express-session": "^1.17.3",
|
||||
"express-validator": "^7.0.1",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"mongoose": "^7.5.1",
|
||||
"passport": "^0.6.0",
|
||||
"passport-jwt": "^4.0.1",
|
||||
"passport-local": "^1.0.0",
|
||||
"passport-local-mongoose": "^8.0.0"
|
||||
}
|
||||
}
|
||||
37
routes/api.js
Normal file
37
routes/api.js
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
const express = require("express");
|
||||
const router = express.Router();
|
||||
const login_controller = require("../controllers/login.js");
|
||||
const post_controller = require("../controllers/post.js");
|
||||
const comment_controller = require("../controllers/comment.js");
|
||||
|
||||
// list all posts and append comments to each post based on id, return as json
|
||||
router.get("/", post_controller.index);
|
||||
|
||||
// login page - should work with json
|
||||
router.post("/login", login_controller.post);
|
||||
|
||||
// post post
|
||||
router.post("/new_post", post_controller.post);
|
||||
|
||||
// post get
|
||||
router.get("/:postID", post_controller.get);
|
||||
|
||||
// post put
|
||||
router.put("/:postID", post_controller.put);
|
||||
|
||||
// post delete
|
||||
router.delete("/:postID", post_controller.delete);
|
||||
|
||||
// comment post
|
||||
router.post("/:postID/new_comment", comment_controller.post);
|
||||
|
||||
// comment get
|
||||
router.get("/:postID/:commentID", comment_controller.get);
|
||||
|
||||
// comment put
|
||||
router.put("/:postID/:commentID", comment_controller.put);
|
||||
|
||||
// comment delete
|
||||
router.delete("/:postID/:commentID", comment_controller.delete);
|
||||
|
||||
module.exports = router;
|
||||
20
strategy/jwt.js
Normal file
20
strategy/jwt.js
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
const User = require("../models/user.js");
|
||||
const JwtStrategy = require("passport-jwt").Strategy;
|
||||
const ExtractJwt = require("passport-jwt").ExtractJwt;
|
||||
|
||||
const opts = {};
|
||||
opts.jwtFromRequest = ExtractJwt.fromAuthHeaderAsBearerToken();
|
||||
opts.secretOrKey = process.env.SECRET_KEY;
|
||||
|
||||
const getAdmin = async () => {
|
||||
const admin = await User.findOne({}).lean().exec(); // only one user in DB - admin. pull it up, jamie!
|
||||
return admin;
|
||||
};
|
||||
|
||||
module.exports = new JwtStrategy(opts, (jwt_payload, done) => {
|
||||
const admin = getAdmin();
|
||||
if (jwt_payload.username === admin.username) {
|
||||
return done(null, true);
|
||||
}
|
||||
return done(null, false);
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue