basic blog api

This commit is contained in:
Jyotirmoy Bandyopadhayaya 2023-01-28 18:50:40 +05:30
commit 7c9799ccd3
Signed by: bravo68web
GPG Key ID: F5671FD7BCB9917A
24 changed files with 2199 additions and 0 deletions

3
.env.example Normal file
View File

@ -0,0 +1,3 @@
MONGODB_URI='mongodb://localhost:27017/express-practice'
PORT=4000
JWT_SECRET="3rwfesdj5rtghfb7ty7ujgh"

137
.gitignore vendored Normal file
View File

@ -0,0 +1,137 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# 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
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://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/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
.cache
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
# Public Private key files
private.key
public.key
# Temp files
test.js

4
.husky/pre-commit Normal file
View File

@ -0,0 +1,4 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npm run prettify

0
.prettierignore Normal file
View File

6
.prettierrc Normal file
View File

@ -0,0 +1,6 @@
{
"trailingComma": "es5",
"tabWidth": 4,
"semi": false,
"singleQuote": true
}

3
README.md Normal file
View File

@ -0,0 +1,3 @@
# Express Practice
My Practice Repo for Express API

34
package.json Normal file
View File

@ -0,0 +1,34 @@
{
"name": "express-practice",
"version": "1.0.0",
"description": "Express Testing Repo",
"main": "src/app.js",
"scripts": {
"dev": "nodemon src/app.js",
"start": "node src/app.js",
"prepare": "husky install",
"prettify": "prettier --write ."
},
"author": "Kausik07",
"license": "ISC",
"dependencies": {
"cors": "^2.8.5",
"dotenv": "^16.0.1",
"express": "^4.18.1",
"express-autoload-router": "^1.0.5",
"helmet": "^5.1.1",
"joi": "^17.6.0",
"jsonwebtoken": "^8.5.1",
"mongoose": "^6.5.2",
"morgan": "^1.10.0",
"napi-nanoid": "^0.0.4",
"node-cache": "^5.1.2",
"nodemailer": "^6.7.8",
"redis": "^4.3.1"
},
"devDependencies": {
"husky": "^8.0.0",
"nodemon": "^2.0.19",
"prettier": "^2.7.1"
}
}

45
src/app.js Normal file
View File

@ -0,0 +1,45 @@
const express = require('express')
const cors = require('cors')
const helmet = require('helmet')
const morgan = require('morgan')
const { PORT } = require('./configs')
const connectMongo = require('./services/mongodb.service')
require('dotenv').config()
const app = express()
app.use(cors())
app.use(helmet())
app.use(morgan('dev'))
app.use(express.json())
app.get('/health', (req, res) => {
res.status(200).send('OK')
})
console.log('☄', 'Base Route', '/api')
const router = require('./routers')
app.use('/api', router)
app.use((err, req, res, next) => {
console.log('💀', 'Error')
res.status(500).json({
status: "error",
message: err.message,
error: true,
})
})
app.use("*", (req, res) => {
res.status(404).json({
status: "error",
message: "Not found",
error: true,
})
})
app.listen(PORT, async () => {
connectMongo()
console.log(`🚀 API listening on port ${PORT}`)
})

8
src/configs/index.js Normal file
View File

@ -0,0 +1,8 @@
require('dotenv').config()
module.exports = {
MONGODB_URI:
process.env.MONGODB_URI || 'mongodb://localhost:27017/blog',
PORT: process.env.PORT || 3000,
JWT_SECRET: process.env.JWT_SECRET || 'secret',
}

View File

@ -0,0 +1,67 @@
const User = require('../models/user.model')
const { generateJWT } = require('../lib/auth.adapter')
const { validateUser } = require('../validators/user.validation')
const CreateUser = async (req, res, next) => {
try {
const userRegData = await validateUser(req.body)
const { username, password, email, name } = userRegData
const user = new User({
username,
email,
name,
attributes: {
role: 'user',
isDisabled: false,
}
})
await user.setPassword(String(password))
await user.save()
res.status(200).json({
status: 'success',
message: 'User created successfully',
error: null,
data: {
username: user.username,
email: user.email,
name: user.name,
role: user.attributes.role,
}
})
}
catch(err){
next(err)
}
}
async function loginUser(req, res, next) {
try {
const { login_user, password } = req.body
const user = await User.findOne({ $or: [{ username: login_user }, { email: login_user }] })
if(!user){
throw new Error('User not found')
}
if(!user.validatePassword(password)){
throw new Error('Invalid password')
}
const token = await generateJWT(user)
res.status(200).json({
status: 'success',
message: 'User logged in successfully',
error: null,
data: {
access_token: token,
}
})
}
catch(err){
next(err)
}
}
module.exports = {
loginUser,
CreateUser,
}

View File

@ -0,0 +1,172 @@
const Blog = require('../models/blog.model');
const { validateBlog } = require('../validators/blog.validation')
const Create = async (req, res, next) => {
try {
const blogData = await validateBlog(req.body);
const { title, content } = blogData;
const author = req.userData.sub;
const blog = new Blog({
title,
content,
author,
});
await blog.save();
res.status(201).json({
status: 'success',
message: 'Blog created successfully',
error: null,
data: {
id: blog._id,
title: blog.title,
content: blog.content,
}
})
} catch (err) {
next(err);
}
}
const ListAll = async (req, res, next) => {
try {
const blogs = await Blog.find({});
res.status(200).json({
status: 'success',
message: 'Blogs listed successfully',
error: null,
data: blogs
})
} catch (err) {
next(err);
}
}
const ListOne = async (req, res, next) => {
try {
const { id } = req.params;
const blog = await Blog.findById(id);
res.status(200).json({
status: 'success',
message: 'Blog found successfully',
error: null,
data: blog
})
} catch (err) {
next(err);
}
}
const Update = async (req, res, next) => {
try {
const { id } = req.params;
let blog = await Blog.findById(id);
if(blog.author != req.userData.sub) {
throw new Error('You are not authorized to update this blog');
}
const { title, content } = req.body;
blog = await Blog.findByIdAndUpdate(id, {
title,
content,
}, { new: true });
res.status(200).json({
status: 'success',
message: 'Blog updated successfully',
error: null,
data: blog
})
} catch (err) {
next(err);
}
}
const Delete = async (req, res, next) => {
try {
const { id } = req.params;
const blog = await Blog.findById(id);
if(blog.author != req.userData.sub) {
throw new Error('You are not authorized to delete this blog');
}
await Blog.findByIdAndUpdate(id, {
attributes: {
isDeleted: true,
}
});
res.status(200).json({
status: 'success',
message: 'Blog deleted successfully',
error: null,
})
} catch (err) {
next(err);
}
}
const Publish = async (req, res, next) => {
try {
const { id } = req.params;
const blog = await Blog.findById(id);
if(blog.author != req.userData.sub) {
throw new Error('You are not authorized to publish this blog');
}
await Blog.findByIdAndUpdate(id, {
attributes: {
isPublished: true,
}
});
res.status(200).json({
status: 'success',
message: 'Blog Published successfully',
error: null,
})
} catch (err) {
next(err);
}
}
const Draft = async (req, res, next) => {
try {
const { id } = req.params;
const blog = await Blog.findById(id);
if(blog.author != req.userData.sub) {
throw new Error('You are not authorized to draft this blog');
}
await Blog.findByIdAndUpdate(id, {
attributes: {
isPublished: false,
}
});
res.status(200).json({
status: 'success',
message: 'Blog saved to Draft successfully',
error: null,
})
} catch (err) {
next(err);
}
}
const ListMine = async (req, res, next) => {
try {
const author = req.userData.sub;
const blogs = await Blog.find({ author });
res.status(200).json({
status: 'success',
message: 'Blogs listed successfully',
error: null,
data: blogs
})
} catch (err) {
next(err);
}
}
module.exports = {
Create,
ListAll,
ListOne,
Update,
Delete,
Publish,
Draft,
ListMine
}

View File

@ -0,0 +1,167 @@
const User = require('../models/user.model')
async function getCurrentUser(req, res, next){
try {
const user = await User.findById(req.userData.sub)
res.status(200).json({
status: 'success',
message: 'User found',
error: null,
data: {
username: user.username,
email: user.email,
name: user.name,
role: user.attributes.role,
}
})
}
catch(err){
next(err)
}
}
async function updateCurrentUser(req, res, next){
try {
const { email, name } = req.body
const user = await User.findById(req.userData.sub)
if(email){
user.email = email
}
if(name){
user.name = name
}
await user.save()
res.status(200).json({
status: 'success',
message: 'User updated successfully',
error: null,
data: {
username: user.username,
email: user.email,
name: user.name,
}
})
}
catch(err){
next(err)
}
}
async function getAllUsers(req, res, next){
const rawUsers = await User.find({})
let users = []
for (const u of rawUsers) {
u.hash = undefined
u.salt = undefined
u.attributes = {
role: u.attributes.role,
}
users.push(u)
}
res.status(200).json({
status: 'success',
message: 'Users found',
error: null,
data: users
})
}
async function getUserById(req, res, next){
try {
if(!req.params.usrid){
throw new Error('User id is required')
}
let user = {}
if(req.query.u_type === 'oid'){
user = await User.findById(req.params.usrid)
}
else{
user = await User.findOne({username: req.params.usrid})
}
if(!user){
throw new Error('User not found')
}
res.status(200).json({
status: 'success',
message: 'User found',
error: null,
data: {
username: user.username,
email: user.email,
name: user.name,
role: user.attributes.role,
}
})
}
catch(err){
next(err)
}
}
async function updateUserById(req, res, next){
try {
if(!req.params.usrid){
throw new Error('User id is required')
}
const { email, name } = req.body
const user = await User.findById(req.params.usrid)
if(email){
user.email = email
}
if(name){
user.name = name
}
await user.save()
res.status(200).json({
status: 'success',
message: 'User updated successfully',
error: null,
data: {
username: user.username,
email: user.email,
name: user.name,
}
})
}
catch(err){
next(err)
}
}
async function disableUserById(req, res, next){
try {
if(!req.params.usrid){
throw new Error('User id is required')
}
const user = await User.findById(req.params.usrid)
if(!user){
throw new Error('User not found')
}
user.attributes.isDisabled = true
await user.save()
res.status(200).json({
status: 'success',
message: 'User disabled successfully',
error: null,
data: {
username: user.username,
attributes: {
isDisabled: user.attributes.isDisabled,
role: user.attributes.role,
},
}
})
}
catch(err){
next(err)
}
}
module.exports = {
getCurrentUser,
updateCurrentUser,
getAllUsers,
getUserById,
updateUserById,
disableUserById,
}

31
src/lib/auth.adapter.js Normal file
View File

@ -0,0 +1,31 @@
const jwt = require('jsonwebtoken');
const { JWT_SECRET, } = require('../configs');
async function generateJWT(user) {
const payload = {
sub: user.id,
username: user.username,
role: user.attributes.role,
};
return jwt.sign(payload, JWT_SECRET, {
algorithm: 'HS256',
expiresIn: '1d',
encoding: 'utf8',
});
}
async function verifyJWT(token) {
const payload = jwt.verify(token, JWT_SECRET, {
algorithms: 'HS256',
encoding: 'utf8',
});
if(!payload){
throw new Error('Invalid or expired token');
}
return payload;
}
module.exports = {
generateJWT,
verifyJWT,
}

View File

@ -0,0 +1,32 @@
const { verifyJWT } = require('../lib/auth.adapter')
async function verifyUser(req, res, next) {
try {
if(!req.headers.authorization){
throw new Error('No authorization header')
}
const token = (req.headers.authorization.split(' ')[1])
const decoded = await verifyJWT(token)
req.userData = decoded
next()
} catch (err) {
next(err)
}
}
async function verifyAdmin(req, res, next) {
try {
if(req.userData.role !== 'admin'){
throw new Error('You are not an admin')
}
next()
} catch (err) {
next(err)
}
}
module.exports = {
verifyUser,
verifyAdmin
}

49
src/models/blog.model.js Normal file
View File

@ -0,0 +1,49 @@
const mongoose = require('mongoose');
const blogSchema = new mongoose.Schema({
title: {
type: String,
required: true
},
content: {
type: String,
required: true
},
author: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
},
attributes: {
isPublished: {
type: Boolean,
default: false
},
isDeleted: {
type: Boolean,
default: false
}
},
comments: [{
type: mongoose.Schema.Types.ObjectId,
ref: 'Comments',
}]
},{
timestamps: true
})
blogSchema.methods.setAuthor = function(userID){
this.author = userID;
}
blogSchema.methods.publishBlog = function(condition){
this.attributes.isPublished = condition;
}
blogSchema.methods.deleteBlog = function(condition){
this.attributes.isDeleted = condition;
this.attributes.isPublished = false;
}
const Blog = mongoose.model('Blog', blogSchema)
module.exports = Blog

85
src/models/user.model.js Normal file
View File

@ -0,0 +1,85 @@
const mongoose = require('mongoose');
const crypto = require('crypto');
const userSchema = new mongoose.Schema({
username: {
type: String,
required: true,
unique: true
},
email:{
type: String,
required: true,
unique: true
},
name: {
first: {
type: String,
required: false
},
last: {
type: String,
required: false
}
},
attributes: {
role: {
type: String,
enum: ['user', 'admin'],
default: 'user'
},
emailVerificationToken: {
type: String,
default: null
},
resetToken: {
type: String,
default: null
},
lastResetDate: {
type: Date,
default: null
},
isDisabled: {
type: Boolean,
default: false
}
},
hash: {
type: String,
required: true
},
salt: {
type: String,
required: true
},
},{
timestamps: true
})
userSchema.methods.setPassword = function(password){
this.salt = crypto.randomBytes(16).toString('hex');
this.hash = crypto.pbkdf2Sync(password, this.salt, 1000, 64, 'sha512').toString('hex');
}
userSchema.methods.validatePassword = function(password) {
const hash = crypto.pbkdf2Sync(password, this.salt, 1000, 64, 'sha512').toString('hex');
return this.hash === hash;
}
userSchema.methods.validateResetToken = function(token) {
if(this.attributes.resetToken === token) {
this.attributes.resetToken = null;
return true;
}
return false;
}
userSchema.methods.generateResetToken = function() {
const token = crypto.randomBytes(32).toString('hex');
this.attributes.resetToken = token;
}
const User = mongoose.model('User', userSchema)
module.exports = User;

View File

@ -0,0 +1,10 @@
const { Router } = require('express')
const { CreateUser, loginUser } = require('../controllers/auth.controller')
const route = new Router()
route.post('/register', CreateUser)
route.post('/login', loginUser)
module.exports = route

View File

@ -0,0 +1,23 @@
const { Router } = require('express')
const { Create, Delete, ListAll, ListOne, Draft, Publish, Update, ListMine, } = require('../controllers/blog.controller')
const { verifyUser, verifyAdmin } = require('../middlewares/auth.middleware')
const route = new Router()
route.post('/create', verifyUser, Create)
route.patch('/publish/:id', verifyUser, Publish)
route.patch('/draft/:id', verifyUser, Draft)
route.delete('/:id', verifyUser, Delete)
route.get("/", ListAll)
route.get('/me', verifyUser, ListMine)
route.get("/:id", ListOne)
route.patch("/:id", verifyUser, Update)
module.exports = route

43
src/routers/index.js Normal file
View File

@ -0,0 +1,43 @@
const path = require('path')
const { readdirSync } = require('fs')
const { Router } = require('express')
const router = Router()
const isCompiled = path.extname(__filename) === '.js'
const thisFileName = path.basename(__filename)
const loadRoutes = async (dirPath, prefix = '/') => {
readdirSync(dirPath, {
withFileTypes: true,
}).forEach(async (f) => {
if (f.isFile()) {
if (f.name == thisFileName) return
const isRouteMod = f.name.endsWith(
`.routes.${isCompiled ? 'js' : 'ts'}`
)
if (isRouteMod) {
const route = f.name.replace(
`.routes.${isCompiled ? 'js' : 'ts'}`,
''
)
const modRoute = path.join(prefix, route)
console.log('🛰️ Loaded', modRoute)
const mod = await import(path.join(baseDir, f.name))
router.use(modRoute, mod.default)
}
} else if (f.isDirectory()) {
await loadRoutes(path.resolve(dirPath, f.name), prefix + f.name)
}
})
}
let baseDir = path.dirname(__filename)
baseDir = path.resolve(baseDir)
loadRoutes(baseDir)
module.exports = router

View File

@ -0,0 +1,20 @@
const { Router } = require('express')
const { getCurrentUser, updateCurrentUser, getAllUsers, getUserById, updateUserById, disableUserById } = require('../controllers/user.controller')
const { verifyUser, verifyAdmin } = require('../middlewares/auth.middleware')
const route = new Router()
route.get('/me', verifyUser, getCurrentUser)
route.patch('/me', verifyUser, updateCurrentUser)
route.get("/", verifyUser, getAllUsers)
route.get("/:usrid", verifyUser, getUserById)
route.patch("/:usrid", verifyUser, verifyAdmin, updateUserById)
route.delete("/:usrid", verifyUser, verifyAdmin, disableUserById)
module.exports = route

View File

@ -0,0 +1,10 @@
const mongoose = require('mongoose');
const { MONGODB_URI } = require('../configs')
module.exports = async () => {
const mongo_url = process.env.MONGODB_URI || MONGODB_URI;
await mongoose.connect(
mongo_url,
() => console.log('Connected to MongoDB 🍀')
)
}

View File

@ -0,0 +1,25 @@
const Joi = require('joi');
const schema = Joi.object({
title: Joi.string()
.alphanum()
.min(3)
.max(30)
.required(),
content: Joi.string()
.min(50)
.max(500)
.required()
})
async function validateBlog(user) {
return schema.validateAsync({
title: user.title,
content: user.content
});
}
module.exports = {
validateBlog
}

View File

@ -0,0 +1,25 @@
const Joi = require('joi');
const schema = Joi.object({
username: Joi.string().alphanum().min(3).max(30).required(),
password: Joi.string().pattern(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/).required(),
email: Joi.string().email({ minDomainSegments: 2} ).required(),
name: {
first: Joi.string().min(3).max(30),
last: Joi.string().min(3).max(30)
}
});
async function validateUser(user) {
return schema.validateAsync({
username: user.username,
password: user.password,
email: user.email,
name: { first: user.name.first, last: user.name.last }
});
}
module.exports = {
validateUser
}

1200
yarn.lock Normal file

File diff suppressed because it is too large Load Diff