From 7a917c1f6bfcb3e03f1f80a6a42807e79f4842bd Mon Sep 17 00:00:00 2001 From: VLE2FE Date: Fri, 24 Apr 2020 17:36:39 +0200 Subject: [PATCH] added PUT /user route --- oas/oas.yaml | 2 +- oas/user.yaml | 10 -- src/routes/user.spec.ts | 195 +++++++++++++++++++++++++++++++++++- src/routes/user.ts | 65 +++++++++++- src/routes/validate/user.ts | 74 +++++++++----- 5 files changed, 304 insertions(+), 42 deletions(-) diff --git a/oas/oas.yaml b/oas/oas.yaml index 03549c1..6e25698 100644 --- a/oas/oas.yaml +++ b/oas/oas.yaml @@ -23,7 +23,7 @@ info:
  • at least one digit
  • at least one lower case letter
  • at least one upper case letter
  • -
  • at least one of the following special characters: !"#$%&'()*+,-./:;<=>?@[\]^_`{|}~
  • +
  • at least one of the following special characters: !"#%&'()*+,-./:;<=>?@[\]^_`{|}~
  • no whitespace
  • at least 8 characters
  • diff --git a/oas/user.yaml b/oas/user.yaml index 763a051..6c2c3fc 100644 --- a/oas/user.yaml +++ b/oas/user.yaml @@ -36,14 +36,10 @@ application/json: schema: $ref: 'oas.yaml#/components/schemas/User' - 400: - $ref: 'oas.yaml#/components/responses/400' 401: $ref: 'oas.yaml#/components/responses/401' 403: $ref: 'oas.yaml#/components/responses/403' - 404: - $ref: 'oas.yaml#/components/responses/404' 500: $ref: 'oas.yaml#/components/responses/500' put: @@ -86,8 +82,6 @@ $ref: 'oas.yaml#/components/responses/401' 403: $ref: 'oas.yaml#/components/responses/403' - 404: - $ref: 'oas.yaml#/components/responses/404' 500: $ref: 'oas.yaml#/components/responses/500' delete: @@ -106,8 +100,6 @@ $ref: 'oas.yaml#/components/responses/401' 403: $ref: 'oas.yaml#/components/responses/403' - 404: - $ref: 'oas.yaml#/components/responses/404' 500: $ref: 'oas.yaml#/components/responses/500' /user/{name}: @@ -127,8 +119,6 @@ application/json: schema: $ref: 'oas.yaml#/components/schemas/User' - 400: - $ref: 'oas.yaml#/components/responses/400' 401: $ref: 'oas.yaml#/components/responses/401' 403: diff --git a/src/routes/user.spec.ts b/src/routes/user.spec.ts index 93b598b..46860ac 100644 --- a/src/routes/user.spec.ts +++ b/src/routes/user.spec.ts @@ -160,6 +160,18 @@ describe('GET /user/{name}', () => { done(); }); }); + it('returns 404 for an unknown user', done => { + supertest(server) + .get('/user/unknown') + .auth('admin', 'Abc123!#') + .expect('Content-type', /json/) + .expect(404) + .end((err, res) => { + if (err) done (err); + should(res.body).be.eql({status: 'Not found'}); + done(); + }); + }); }); @@ -184,7 +196,8 @@ describe('PUT /user/{name}', () => { }); it('returns own user details', done => { supertest(server) - .get('/user') + .put('/user') + .send({}) .auth('janedoe', 'Xyz890*)') .expect('Content-type', /json/) .expect(200) @@ -202,7 +215,8 @@ describe('PUT /user/{name}', () => { }); it('returns other user details for admin', done => { supertest(server) - .get('/user/janedoe') + .put('/user/janedoe') + .send({}) .auth('admin', 'Abc123!#') .expect('Content-type', /json/) .expect(200) @@ -218,9 +232,118 @@ describe('PUT /user/{name}', () => { done(); }); }); + it('changes user details as given', done => { + supertest(server) + .put('/user') + .auth('admin', 'Abc123!#') + .send({name: 'adminnew', email: 'adminnew@bosch.com', pass: 'Abc123##', location: 'Abt', device_name: 'test'}) + .expect(200) + .end(err => { + if (err) done (err); + UserModel.find({name: 'adminnew'}).lean().exec( 'find', (err, data) => { + if (err) return done(err); + should(data).have.lengthOf(1); + should(data[0]).have.only.keys('_id', 'name', 'pass', 'email', 'level', 'location', 'device_name', 'key', '__v'); + should(data[0]).have.property('_id'); + should(data[0]).have.property('name', 'adminnew'); + should(data[0]).have.property('email', 'adminnew@bosch.com'); + should(data[0]).have.property('pass').not.eql('Abc123##'); + should(data[0]).have.property('level', 'admin'); + should(data[0]).have.property('location', 'Abt'); + should(data[0]).have.property('device_name', 'test'); + done(); + }); + }); + }); + it('lets the admin change a user level', done => { + supertest(server) + .put('/user/janedoe') + .auth('admin', 'Abc123!#') + .send({level: 'read'}) + .expect(200) + .end(err => { + if (err) done (err); + UserModel.find({name: 'janedoe'}).lean().exec( 'find', (err, data) => { + if (err) return done(err); + should(data).have.lengthOf(1); + should(data[0]).have.property('level', 'read'); + done(); + }); + }); + }); + it('does not change the level', done => { + supertest(server) + .put('/user') + .auth('janedoe', 'Xyz890*)') + .send({level: 'read'}) + .expect(400) + .end((err, res) => { + if (err) done (err); + should(res.body).be.eql({status: 'Invalid body format'}); + UserModel.find({name: 'janedoe'}).lean().exec( 'find', (err, data) => { + if (err) return done(err); + should(data).have.lengthOf(1); + should(data[0]).have.property('level', 'write'); + done(); + }); + }); + }); + it('rejects a username already in use', done => { + supertest(server) + .put('/user') + .auth('admin', 'Abc123!#') + .send({name: 'janedoe'}) + .expect(400) + .end((err, res) => { + if (err) done (err); + should(res.body).be.eql({status: 'Username already taken'}); + UserModel.find({name: 'janedoe'}).lean().exec( 'find', (err, data) => { + if (err) return done(err); + should(data).have.lengthOf(1); + done(); + }); + }); + }); + it('rejects invalid user details', done => { + supertest(server) + .put('/user') + .auth('admin', 'Abc123!#') + .send({email: 'john.doe@bosch.com', name: 'johndoe', pass: 'Abc123!#', location: 44, device_name: 'Alpha II'}) + .expect(400) + .end((err, res) => { + if (err) done (err); + should(res.body).be.eql({status: 'Invalid body format'}); + done(); + }); + }); + it('rejects an invalid email address', done => { + supertest(server) + .put('/user') + .auth('admin', 'Abc123!#') + .send({email: 'john.doe'}) + .expect(400) + .end((err, res) => { + if (err) done (err); + should(res.body).be.eql({status: 'Invalid body format'}); + done(); + }); + }); + it('rejects an invalid password', done => { + supertest(server) + .put('/user') + .auth('admin', 'Abc123!#') + .send({pass: 'password'}) + .expect(400) + .end((err, res) => { + if (err) done (err); + should(res.body).be.eql({status: 'Invalid body format'}); + done(); + }); + }); it('rejects requests from non-admins for another user', done => { supertest(server) - .get('/user/admin') + .put('/user/admin') + .send({}) .auth('janedoe', 'Xyz890*)') .expect('Content-type', /json/) .expect(403) @@ -232,7 +355,8 @@ describe('PUT /user/{name}', () => { }); it('rejects requests from a user API key', done => { supertest(server) - .get('/user?key=5ea0450ed851c30a90e70899') + .put('/user?key=5ea0450ed851c30a90e70899') + .send({}) .expect('Content-type', /json/) .expect(401) .end((err, res) => { @@ -243,7 +367,8 @@ describe('PUT /user/{name}', () => { }); it('rejects requests from an admin API key', done => { supertest(server) - .get('/user/janedoe?key=5ea131671feb9c2ee0aafc9a') + .put('/user/janedoe?key=5ea131671feb9c2ee0aafc9a') + .send({}) .expect('Content-type', /json/) .expect(401) .end((err, res) => { @@ -252,6 +377,18 @@ describe('PUT /user/{name}', () => { done(); }); }); + it('returns 404 for an unknown user', done => { + supertest(server) + .put('/user/unknown') + .auth('admin', 'Abc123!#') + .send({}) + .expect(404) + .end((err, res) => { + if (err) done (err); + should(res.body).be.eql({status: 'Not found'}); + done(); + }); + }); }); @@ -332,6 +469,54 @@ describe('POST /user/new', () => { }); }); }); + it('rejects invalid user details', done => { + supertest(server) + .post('/user/new') + .auth('admin', 'Abc123!#') + .send({email: 'john.doe@bosch.com', name: 'johndoe', pass: 'Abc123!#', level: 'read', location: 44, device_name: 'Alpha II'}) + .expect(400) + .end((err, res) => { + if (err) done (err); + should(res.body).be.eql({status: 'Invalid body format'}); + done(); + }); + }); + it('rejects an invalid user level', done => { + supertest(server) + .post('/user/new') + .auth('admin', 'Abc123!#') + .send({email: 'john.doe@bosch.com', name: 'johndoe', pass: 'Abc123!#', level: 'xxx', location: 'Rng', device_name: 'Alpha II'}) + .expect(400) + .end((err, res) => { + if (err) done (err); + should(res.body).be.eql({status: 'Invalid body format'}); + done(); + }); + }); + it('rejects an invalid email address', done => { + supertest(server) + .post('/user/new') + .auth('admin', 'Abc123!#') + .send({email: 'john.doe', name: 'johndoe', pass: 'Abc123!#', level: 'read', location: 'Rng', device_name: 'Alpha II'}) + .expect(400) + .end((err, res) => { + if (err) done (err); + should(res.body).be.eql({status: 'Invalid body format'}); + done(); + }); + }); + it('rejects an invalid password', done => { + supertest(server) + .post('/user/new') + .auth('admin', 'Abc123!#') + .send({email: 'john.doe@bosch.com', name: 'johndoe', pass: 'password', level: 'read', location: 'Rng', device_name: 'Alpha II'}) + .expect(400) + .end((err, res) => { + if (err) done (err); + should(res.body).be.eql({status: 'Invalid body format'}); + done(); + }); + }); it('rejects requests from non-admins', done => { supertest(server) .post('/user/new') diff --git a/src/routes/user.ts b/src/routes/user.ts index 0865fac..d362c79 100644 --- a/src/routes/user.ts +++ b/src/routes/user.ts @@ -15,7 +15,7 @@ router.get('/users', (req, res) => { }); }); -router.get('/user/:username*?', (req, res) => { +router.get('/user/:username*?', (req, res, next) => { if (!req.auth(res, ['read', 'write', 'maintain', 'dev', 'admin'], 'basic')) return; let username = req.authDetails.username; if (req.params.username !== undefined) { @@ -24,15 +24,74 @@ router.get('/user/:username*?', (req, res) => { } UserModel.findOne({name: username}).lean().exec( (err, data:any) => { - res.json(UserValidate.output(data)); // validate all and filter null values from validation errors + if (err) next(err); + if (data) { + res.json(UserValidate.output(data)); // validate all and filter null values from validation errors + } + else { + res.status(404).json({status: 'Not found'}); + } }); }); +router.put('/user/:username*?', (req, res, next) => { + console.log(req.authDetails); + if (!req.auth(res, ['read', 'write', 'maintain', 'dev', 'admin'], 'basic')) return; + let username = req.authDetails.username; + if (req.params.username !== undefined) { + if (!req.auth(res, ['admin'], 'basic')) return; + username = req.params.username; + } + const {error, value: user} = UserValidate.input(req.body, 'change' + (req.authDetails.level === 'admin'? 'admin' : '')); + console.log(error); + console.log(user); + if(error !== undefined) { + res.status(400).json({status: 'Invalid body format'}); + return; + } + + if (user.hasOwnProperty('pass')) { + user.pass = bcrypt.hashSync(user.pass, 10); + } + + // check that user does not already exist if new name was specified + if (user.hasOwnProperty('name') && user.name !== username) { + UserModel.find({name: user.name}).lean().exec( (err, data:any) => { + if (err) next(err); + if (data.length > 0) { + res.status(400).json({status: 'Username already taken'}); + return; + } + + UserModel.findOneAndUpdate({name: username}, user, {new: true}).lean().exec( (err, data:any) => { + if (err) next(err); + if (data) { + res.json(UserValidate.output(data)); // validate all and filter null values from validation errors + } + else { + res.status(404).json({status: 'Not found'}); + } + }); + }); + } + else { + UserModel.findOneAndUpdate({name: username}, user, {new: true}).lean().exec( (err, data:any) => { + if (err) next(err); + if (data) { + res.json(UserValidate.output(data)); // validate all and filter null values from validation errors + } + else { + res.status(404).json({status: 'Not found'}); + } + }); + } +}); + router.post('/user/new', (req, res, next) => { if (!req.auth(res, ['admin'], 'basic')) return; // validate input - const {error, value: user} = UserValidate.input(req.body); + const {error, value: user} = UserValidate.input(req.body, 'new'); if(error !== undefined) { res.status(400).json({status: 'Invalid body format'}); return; diff --git a/src/routes/validate/user.ts b/src/routes/validate/user.ts index 26becdd..68f743d 100644 --- a/src/routes/validate/user.ts +++ b/src/routes/validate/user.ts @@ -2,34 +2,62 @@ import joi from '@hapi/joi'; import globals from '../../globals'; export default class UserValidate { // validate input for user - static input (data) { - return joi.object({ - name: joi.string() - .alphanum() - .lowercase() - .required(), + private static user = { + _id: joi.any(), + name: joi.string() + .alphanum() + .lowercase(), - email: joi.string() - .email({minDomainSegments: 2}) - .lowercase() - .required(), + email: joi.string() + .email({minDomainSegments: 2}) + .lowercase(), - pass: joi.string() - .pattern(new RegExp('^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[!"#$%&\'()*+,-.\\/:;<=>?@[\\]^_`{|}~])(?=\\S+$).{8,}$')) - .required(), + pass: joi.string() + .pattern(new RegExp('^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[!"#%&\'()*+,-.\\/:;<=>?@[\\]^_`{|}~])(?=\\S+$).{8,}$')), - level: joi.string() - .valid(...globals.levels) - .required(), + level: joi.string() + .valid(...globals.levels), - location: joi.string() - .alphanum() - .required(), + location: joi.string() + .alphanum(), - device_name: joi.string() - .allow('') - .required() - }).validate(data); + device_name: joi.string() + .allow('') + }; + + static input (data, param) { + if (param === 'new') { + return joi.object({ + name: this.user.name.required(), + email: this.user.email.required(), + pass: this.user.pass.required(), + level: this.user.level.required(), + location: this.user.location.required(), + device_name: this.user.device_name.required() + }).validate(data); + } + else if (param === 'change') { + return joi.object({ + name: this.user.name, + email: this.user.email, + pass: this.user.pass, + location: this.user.location, + device_name: this.user.device_name + }).validate(data); + } + else if (param === 'changeadmin') { + return joi.object({ + name: this.user.name, + email: this.user.email, + pass: this.user.pass, + level: this.user.level, + location: this.user.location, + device_name: this.user.device_name + }).validate(data); + } + else { + return{error: 'No parameter specified!', value: {}}; + } } static output (data) { // validate output from database for needed properties, strip everything else