mirror of
https://github.com/usatiuk/ustk-todolist.git
synced 2025-10-28 15:47:48 +01:00
create, update, remove users
This commit is contained in:
28
app.js
28
app.js
@@ -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 });
|
||||||
|
|||||||
@@ -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;
|
|
||||||
@@ -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
8
config/passport.js
Normal 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
25
models/User.js
Normal 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
2938
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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
7
routes/auth.js
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
const jwt = require('express-jwt');
|
||||||
|
const { secret } = require('../config');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
required: jwt({ secret }),
|
||||||
|
optional: jwt({ secret, credentialsRequired: false }),
|
||||||
|
};
|
||||||
@@ -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
63
routes/users.js
Normal 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;
|
||||||
164
tests/integration/users.test.js
Normal file
164
tests/integration/users.test.js
Normal 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();
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user