create, update, remove users

This commit is contained in:
2018-05-30 19:24:48 +03:00
parent 00f71bb78d
commit fd142c8710
11 changed files with 1853 additions and 1413 deletions

28
app.js
View File

@@ -1,14 +1,28 @@
require('dotenv').config(); require('dotenv').config();
const express = require('express');
const bodyParser = require('body-parser');
const morgan = require('morgan');
const cors = require('cors');
const config = require('./config'); const config = require('./config');
const db = require('./config/db'); const db = require('./config/db');
const app = require('./config/app');
const app = express();
app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json());
app.use(cors());
app.use(morgan('dev'));
require('./models/User');
require('./models/TodoList'); require('./models/TodoList');
require('./models/Todo'); require('./models/Todo');
const passport = require('./config/passport');
app.use(passport.initialize());
app.use('/lists', require('./routes/lists')); app.use('/lists', require('./routes/lists'));
app.use('/todos', require('./routes/todos')); app.use('/todos', require('./routes/todos'));
app.use('/users', require('./routes/users'));
// 404 route // 404 route
app.use((req, res) => { app.use((req, res) => {
@@ -31,17 +45,19 @@ app.use((req, res) => {
app.use((error, req, res, next) => { app.use((error, req, res, next) => {
switch (error.name) { switch (error.name) {
case 'ValidationError': case 'ValidationError':
case 'MissingPasswordError':
case 'BadRequestError':
res.status(400); res.status(400);
res.json({ success: false, error }); res.json({ success: false, error });
break; break;
case 'UnauthorizedError':
res.status(401);
res.json({ success: false, error });
break;
case 'NotFound': case 'NotFound':
res.status(404); res.status(404);
res.json({ success: false, error }); res.json({ success: false, error });
break; break;
case 'BadRequestError':
res.status(400);
res.json({ success: false, error });
break;
default: default:
res.status(500); res.status(500);
res.json({ success: false, error }); res.json({ success: false, error });

View File

@@ -1,17 +0,0 @@
const express = require('express');
const bodyParser = require('body-parser');
const methodOverride = require('method-override');
const morgan = require('morgan');
const cors = require('cors');
const app = express();
app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json());
app.use(cors());
app.use(morgan('dev'));
app.use(methodOverride('_method'));
module.exports = app;

View File

@@ -9,6 +9,7 @@ const dev = {
port: process.env.DEV_DB_PORT || 27017, port: process.env.DEV_DB_PORT || 27017,
name: process.env.DEV_DB_NAME || 'todolist', name: process.env.DEV_DB_NAME || 'todolist',
}, },
secret: process.env.DEV_SECRET || 'devsecret',
}; };
const test = { const test = {
app: { app: {
@@ -19,6 +20,7 @@ const test = {
port: process.env.TEST_DB_PORT || 27017, port: process.env.TEST_DB_PORT || 27017,
name: process.env.TEST_DB_NAME || 'todolistTest', name: process.env.TEST_DB_NAME || 'todolistTest',
}, },
secret: process.env.TEST_SECRET || 'testsecret',
}; };
const config = { const config = {

8
config/passport.js Normal file
View File

@@ -0,0 +1,8 @@
const passport = require('passport');
const mongoose = require('mongoose');
const User = mongoose.model('User');
passport.use(User.createStrategy());
module.exports = passport;

25
models/User.js Normal file
View File

@@ -0,0 +1,25 @@
const mongoose = require('mongoose');
const passportLocalMongoose = require('passport-local-mongoose');
const jwt = require('jsonwebtoken');
const { secret } = require('../config');
const { Schema } = mongoose;
const UserSchema = Schema({ username: { type: String, required: true } });
UserSchema.plugin(passportLocalMongoose);
UserSchema.methods.generateJwt = function () {
return jwt.sign({ id: this._id, username: this.username }, secret, { expiresIn: '1y' });
};
UserSchema.methods.toAuthJson = function () {
return {
id: this._id,
username: this.username,
jwt: this.generateJwt(),
};
};
mongoose.model('User', UserSchema);

2938
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -15,9 +15,13 @@
"cors": "^2.8.4", "cors": "^2.8.4",
"dotenv": "^5.0.1", "dotenv": "^5.0.1",
"express": "^4.16.3", "express": "^4.16.3",
"method-override": "^2.3.10", "express-jwt": "^5.3.1",
"jsonwebtoken": "^8.2.1",
"mongoose": "^5.1.1", "mongoose": "^5.1.1",
"morgan": "^1.9.0" "morgan": "^1.9.0",
"passport": "^0.4.0",
"passport-local": "^1.0.0",
"passport-local-mongoose": "^5.0.0"
}, },
"devDependencies": { "devDependencies": {
"eslint": "^4.19.1", "eslint": "^4.19.1",

7
routes/auth.js Normal file
View File

@@ -0,0 +1,7 @@
const jwt = require('express-jwt');
const { secret } = require('../config');
module.exports = {
required: jwt({ secret }),
optional: jwt({ secret, credentialsRequired: false }),
};

View File

@@ -43,11 +43,7 @@ router.patch(
if (completed !== undefined) { if (completed !== undefined) {
patch.completed = completed; patch.completed = completed;
} }
const todo = await Todo.findByIdAndUpdate( const todo = await Todo.findByIdAndUpdate(todoId, { $set: patch }, { new: true }).exec();
{ _id: todoId },
{ $set: patch },
{ new: true },
).exec();
if (!todo) { if (!todo) {
throw new NotFoundError(`can't find todo with id ${todoId}`); throw new NotFoundError(`can't find todo with id ${todoId}`);
} }

63
routes/users.js Normal file
View File

@@ -0,0 +1,63 @@
const express = require('express');
const mongoose = require('mongoose');
const passport = require('passport');
const User = mongoose.model('User');
const router = express.Router();
const asyncHelper = require('../asyncHelper');
const auth = require('./auth');
const { NotFoundError } = require('../errors');
router.post(
'/',
asyncHelper(async (req, res) => {
const { username, password } = req.body;
const user = new User({ username });
await user.setPassword(password);
await user.save();
res.json({ success: true, data: user.toAuthJson() });
}),
);
router.patch(
'/user',
auth.required,
asyncHelper(async (req, res) => {
const { username, password } = req.body;
const patch = {};
if (username !== undefined) {
patch.username = username;
}
const user = await User.findByIdAndUpdate(req.user.id, { $set: patch }, { new: true }).exec();
if (!user) {
throw new NotFoundError(`can't find user with username ${req.user.username}`);
}
if (password !== undefined) {
await user.setPassword(password);
await user.save();
}
res.json({ success: true, data: user.toAuthJson() });
}),
);
router.delete(
'/user',
auth.required,
asyncHelper(async (req, res) => {
await User.findByIdAndRemove(req.user.id).exec();
res.json({ success: true });
}),
);
router.post(
'/login',
passport.authenticate('local', { session: false }),
asyncHelper(async (req, res) => {
res.json({ success: true, data: req.user.toAuthJson() });
}),
);
module.exports = router;

View File

@@ -0,0 +1,164 @@
const server = require('../../app.js');
const request = require('supertest');
const mongoose = require('mongoose');
const jwt = require('jsonwebtoken');
const db = require('../../config/db');
const { secret } = require('../../config');
const User = mongoose.model('User');
let user;
let token;
beforeEach(async () => {
await db.connect();
user = new User({ username: 'User' });
await user.setPassword('password');
await user.save();
token = user.generateJwt();
});
afterEach(async () => {
await User.remove({}).exec();
await db.disconnect();
});
afterAll(async () => {
await server.close();
});
describe('test users', () => {
test('should create user', async () => {
const response = await request(server)
.post('/users')
.send({
username: 'User 2',
password: 'password 2',
})
.set('Content-Type', 'application/json')
.set('Accept', 'application/json')
.expect(200)
.expect('Content-Type', 'application/json; charset=utf-8');
expect(response.body.success).toBeTruthy();
const tokenDecoded = jwt.verify(response.body.data.jwt, secret);
expect(tokenDecoded.username).toEqual('User 2');
const userAuth = await User.authenticate()('User 2', 'password 2');
expect(userAuth.user).toBeTruthy();
});
test('should not create user with no username', async () => {
const response = await request(server)
.post('/users')
.send({
username: '',
password: 'password 2',
})
.set('Content-Type', 'application/json')
.set('Accept', 'application/json')
.expect(400)
.expect('Content-Type', 'application/json; charset=utf-8');
expect(response.body.success).toBeFalsy();
});
test('should not create user with no password', async () => {
const response = await request(server)
.post('/users')
.send({
username: 'User',
password: '',
})
.set('Content-Type', 'application/json')
.set('Accept', 'application/json')
.expect(400)
.expect('Content-Type', 'application/json; charset=utf-8');
expect(response.body.success).toBeFalsy();
});
test('should login user', async () => {
const response = await request(server)
.post('/users/login')
.send({
username: 'User',
password: 'password',
})
.set('Content-Type', 'application/json')
.set('Accept', 'application/json')
.expect(200)
.expect('Content-Type', 'application/json; charset=utf-8');
expect(response.body.success).toBeTruthy();
const tokenDecoded = jwt.verify(response.body.data.jwt, secret);
expect(tokenDecoded.username).toEqual('User');
});
test('should not login user with no name', async () => {
await request(server)
.post('/users/login')
.send({
username: '',
password: 'notpassword',
})
.set('Content-Type', 'application/json')
.set('Accept', 'application/json')
.expect(400);
});
test('should not login user with wrong password', async () => {
await request(server)
.post('/users/login')
.send({
username: 'User',
password: 'notpassword',
})
.set('Content-Type', 'application/json')
.set('Accept', 'application/json')
.expect(401);
});
test('should update user', async () => {
const response = await request(server)
.patch('/users/user')
.send({
username: 'User 2',
password: 'password2',
})
.set('Authorization', `Bearer ${token}`)
.set('Content-Type', 'application/json')
.set('Accept', 'application/json')
.expect(200)
.expect('Content-Type', 'application/json; charset=utf-8');
expect(response.body.success).toBeTruthy();
const tokenDecoded = jwt.verify(response.body.data.jwt, secret);
expect(tokenDecoded.username).toEqual('User 2');
const userAuth = await User.authenticate()('User 2', 'password2');
expect(userAuth.user).toBeTruthy();
});
test('should not update user without authentication', async () => {
const response = await request(server)
.patch('/users/user')
.send({
username: 'User 2',
password: 'password2',
})
.set('Content-Type', 'application/json')
.set('Accept', 'application/json')
.expect(401);
expect(response.body.success).toBeFalsy();
});
test('should not delete user without authentication', async () => {
const response = await request(server)
.delete('/users/user')
.set('Content-Type', 'application/json')
.set('Accept', 'application/json')
.expect(401);
expect(response.body.success).toBeFalsy();
expect(await User.findOne({ username: 'User' }).exec()).toBeTruthy();
});
test('should delete user', async () => {
const response = await request(server)
.delete('/users/user')
.set('Authorization', `Bearer ${token}`)
.set('Content-Type', 'application/json')
.set('Accept', 'application/json')
.expect(200)
.expect('Content-Type', 'application/json; charset=utf-8');
expect(response.body.success).toBeTruthy();
expect(await User.findOne({ username: 'User' }).exec()).toBeFalsy();
});
});