diff --git a/oas/schemas.yaml b/oas/schemas.yaml index d4151d1..4d8a805 100644 --- a/oas/schemas.yaml +++ b/oas/schemas.yaml @@ -1,5 +1,6 @@ Id: type: string + example: 5ea0450ed851c30a90e70894 _Id: properties: _id: diff --git a/oas/user.yaml b/oas/user.yaml index 4042901..763a051 100644 --- a/oas/user.yaml +++ b/oas/user.yaml @@ -1,6 +1,6 @@ /users: get: - summary: TODO lists all users + summary: lists all users description: 'Auth: basic, levels: admin' tags: - /user @@ -21,12 +21,10 @@ $ref: 'oas.yaml#/components/responses/403' 500: $ref: 'oas.yaml#/components/responses/500' -/user/{name}: - parameters: - - $ref: 'oas.yaml#/components/parameters/Name' +/user: get: - summary: TODO list user details - description: 'Auth: basic, levels: read, write, maintain, dev get their own information without a name property specified, level: admin can get any user using the name parameter' + summary: list own user details + description: 'Auth: basic, levels: read, write, maintain, admin' tags: - /user security: @@ -37,9 +35,7 @@ content: application/json: schema: - type: array - items: - $ref: 'oas.yaml#/components/schemas/User' + $ref: 'oas.yaml#/components/schemas/User' 400: $ref: 'oas.yaml#/components/responses/400' 401: @@ -52,7 +48,98 @@ $ref: 'oas.yaml#/components/responses/500' put: summary: TODO change user details - description: 'Auth: basic, levels: read, write, maintain, dev can change their own information (except level) without a name property specified, level: admin can change any user using the name parameter' + description: 'Auth: basic, levels: read, write, maintain, admin' + tags: + - /user + security: + - BasicAuth: [] + requestBody: + required: true + content: + application/json: + schema: + allOf: + - $ref: 'oas.yaml#/components/schemas/_Id' + - $ref: 'oas.yaml#/components/schemas/UserName' + - $ref: 'oas.yaml#/components/schemas/Email' + properties: + pass: + type: string + writeOnly: true + example: Abc123!# + location: + type: string + example: Rng + device_name: + type: string + example: Alpha II + responses: + 200: + description: user details + content: + 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' + delete: + summary: TODO delete user + description: 'Auth: basic, levels: read, write, maintain, admin' + tags: + - /user + security: + - BasicAuth: [] + responses: + 200: + $ref: 'oas.yaml#/components/responses/Ok' + 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' +/user/{name}: + parameters: + - $ref: 'oas.yaml#/components/parameters/Name' + get: + summary: list user details + description: 'Auth: basic, levels: admin' + tags: + - /user + security: + - BasicAuth: [] + responses: + 200: + description: user details + content: + 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: + summary: TODO change user details + description: 'Auth: basic, levels: admin' tags: - /user security: @@ -69,9 +156,7 @@ content: application/json: schema: - type: array - items: - $ref: 'oas.yaml#/components/schemas/User' + $ref: 'oas.yaml#/components/schemas/User' 400: $ref: 'oas.yaml#/components/responses/400' 401: @@ -84,7 +169,7 @@ $ref: 'oas.yaml#/components/responses/500' delete: summary: TODO delete user - description: 'Auth: basic, levels: read, write, maintain, dev can delete their own account, level: admin can delete any user using the name parameter' + description: 'Auth: basic, levels: admin' tags: - /user security: @@ -123,7 +208,7 @@ $ref: 'oas.yaml#/components/responses/500' /user/new: post: - summary: TODO add new user + summary: add new user description: 'Auth: basic, levels: admin' tags: - /user @@ -160,7 +245,7 @@ $ref: 'oas.yaml#/components/responses/500' /user/passreset: post: - summary: TODO reset password and send mail to restore + summary: reset password and send mail to restore description: 'Auth: none' tags: - /user diff --git a/src/db.ts b/src/db.ts index 00477da..e624696 100644 --- a/src/db.ts +++ b/src/db.ts @@ -35,7 +35,7 @@ export default class db { } // connect to db - mongoose.connect(connectionString, {useNewUrlParser: true, useUnifiedTopology: true}, err => { + mongoose.connect(connectionString, {useNewUrlParser: true, useUnifiedTopology: true, useCreateIndex: true}, err => { if (err) done(err); }); mongoose.connection.on('error', console.error.bind(console, 'connection error:')); @@ -89,7 +89,6 @@ export default class db { let loadCounter = 0; // count number of loaded collections to know when to return done() Object.keys(json.collections).forEach(collectionName => { // create each collection for(let i in json.collections[collectionName]) { // convert $oid fields to actual ObjectIds - console.log(json.collections[collectionName][i]); Object.keys(json.collections[collectionName][i]).forEach(key => { json.collections[collectionName][i][key] = json.collections[collectionName][i][key].hasOwnProperty('$oid') ? mongoose.Types.ObjectId(json.collections[collectionName][i][key].$oid) : json.collections[collectionName][i][key]; }) diff --git a/src/models/user.ts b/src/models/user.ts index b72dabb..50178a6 100644 --- a/src/models/user.ts +++ b/src/models/user.ts @@ -1,7 +1,7 @@ import mongoose from 'mongoose'; const UserSchema = new mongoose.Schema({ - name: String, + name: {type: String, index: {unique: true}}, email: String, pass: String, key: String, diff --git a/src/routes/root.spec.ts b/src/routes/root.spec.ts index 61544a8..9372259 100644 --- a/src/routes/root.spec.ts +++ b/src/routes/root.spec.ts @@ -3,7 +3,7 @@ import should from 'should/as-function'; import db from '../db'; -describe('/', () => { +describe('GET /', () => { let server; before(done => { diff --git a/src/routes/user.spec.ts b/src/routes/user.spec.ts index c4511ec..93b598b 100644 --- a/src/routes/user.spec.ts +++ b/src/routes/user.spec.ts @@ -4,7 +4,258 @@ import db from '../db'; import UserModel from '../models/user'; -describe('/user/new', () => { +describe('GET /users', () => { + let server; + + before(done => { + process.env.port = '2999'; + process.env.NODE_ENV = 'test'; + db.connect('test', done); + }); + beforeEach(done => { + delete require.cache[require.resolve('../index')]; // prevent loading from cache + server = require('../index'); + db.drop(err => { // reset database + if (err) return done(err); + db.loadJson(require('../test/db.json'), done); + }); + }); + afterEach(done => { + server.close(done); + }); + it('returns all users', done => { + supertest(server) + .get('/users') + .auth('admin', 'Abc123!#') + .expect('Content-type', /json/) + .expect(200) + .end((err, res) => { + if (err) done (err); + const json = require('../test/db.json'); + should(res.body).have.lengthOf(json.collections.users.length); + should(res.body).matchEach(user => { + should(user).have.only.keys('_id', 'email', 'name', 'level', 'location', 'device_name'); + should(user).have.property('_id').be.type('string'); + should(user).have.property('email').be.type('string'); + should(user).have.property('name').be.type('string'); + should(user).have.property('level').be.type('string'); + should(user).have.property('location').be.type('string'); + should(user).have.property('device_name').be.type('string'); + }); + done(); + }); + }); + it('rejects requests from non-admins', done => { + supertest(server) + .get('/users') + .auth('janedoe', 'Xyz890*)') + .expect('Content-type', /json/) + .expect(403) + .end((err, res) => { + if (err) done (err); + should(res.body).be.eql({status: 'Forbidden'}); + done(); + }); + }); + it('rejects requests from an admin API key', done => { + supertest(server) + .get('/users?key=5ea131671feb9c2ee0aafc9a') + .expect('Content-type', /json/) + .expect(401) + .end((err, res) => { + if (err) done (err); + should(res.body).be.eql({status: 'Unauthorized'}); + done(); + }); + }); +}); + + +describe('GET /user/{name}', () => { + let server; + + before(done => { + process.env.port = '2999'; + process.env.NODE_ENV = 'test'; + db.connect('test', done); + }); + beforeEach(done => { + delete require.cache[require.resolve('../index')]; // prevent loading from cache + server = require('../index'); + db.drop(err => { // reset database + if (err) return done(err); + db.loadJson(require('../test/db.json'), done); + }); + }); + afterEach(done => { + server.close(done); + }); + it('returns own user details', done => { + supertest(server) + .get('/user') + .auth('janedoe', 'Xyz890*)') + .expect('Content-type', /json/) + .expect(200) + .end((err, res) => { + if (err) done (err); + should(res.body).have.only.keys('_id', 'email', 'name', 'level', 'location', 'device_name'); + should(res.body).have.property('_id').be.type('string'); + should(res.body).have.property('email', 'jane.doe@bosch.com'); + should(res.body).have.property('name', 'janedoe'); + should(res.body).have.property('level', 'write'); + should(res.body).have.property('location', 'Rng'); + should(res.body).have.property('device_name', 'Alpha I'); + done(); + }); + }); + it('returns other user details for admin', done => { + supertest(server) + .get('/user/janedoe') + .auth('admin', 'Abc123!#') + .expect('Content-type', /json/) + .expect(200) + .end((err, res) => { + if (err) done (err); + should(res.body).have.only.keys('_id', 'email', 'name', 'level', 'location', 'device_name'); + should(res.body).have.property('_id').be.type('string'); + should(res.body).have.property('email', 'jane.doe@bosch.com'); + should(res.body).have.property('name', 'janedoe'); + should(res.body).have.property('level', 'write'); + should(res.body).have.property('location', 'Rng'); + should(res.body).have.property('device_name', 'Alpha I'); + done(); + }); + }); + it('rejects requests from non-admins for another user', done => { + supertest(server) + .get('/user/admin') + .auth('janedoe', 'Xyz890*)') + .expect('Content-type', /json/) + .expect(403) + .end((err, res) => { + if (err) done (err); + should(res.body).be.eql({status: 'Forbidden'}); + done(); + }); + }); + it('rejects requests from a user API key', done => { + supertest(server) + .get('/user?key=5ea0450ed851c30a90e70899') + .expect('Content-type', /json/) + .expect(401) + .end((err, res) => { + if (err) done (err); + should(res.body).be.eql({status: 'Unauthorized'}); + done(); + }); + }); + it('rejects requests from an admin API key', done => { + supertest(server) + .get('/user/janedoe?key=5ea131671feb9c2ee0aafc9a') + .expect('Content-type', /json/) + .expect(401) + .end((err, res) => { + if (err) done (err); + should(res.body).be.eql({status: 'Unauthorized'}); + done(); + }); + }); +}); + + +describe('PUT /user/{name}', () => { + let server; + + before(done => { + process.env.port = '2999'; + process.env.NODE_ENV = 'test'; + db.connect('test', done); + }); + beforeEach(done => { + delete require.cache[require.resolve('../index')]; // prevent loading from cache + server = require('../index'); + db.drop(err => { // reset database + if (err) return done(err); + db.loadJson(require('../test/db.json'), done); + }); + }); + afterEach(done => { + server.close(done); + }); + it('returns own user details', done => { + supertest(server) + .get('/user') + .auth('janedoe', 'Xyz890*)') + .expect('Content-type', /json/) + .expect(200) + .end((err, res) => { + if (err) done (err); + should(res.body).have.only.keys('_id', 'email', 'name', 'level', 'location', 'device_name'); + should(res.body).have.property('_id').be.type('string'); + should(res.body).have.property('email', 'jane.doe@bosch.com'); + should(res.body).have.property('name', 'janedoe'); + should(res.body).have.property('level', 'write'); + should(res.body).have.property('location', 'Rng'); + should(res.body).have.property('device_name', 'Alpha I'); + done(); + }); + }); + it('returns other user details for admin', done => { + supertest(server) + .get('/user/janedoe') + .auth('admin', 'Abc123!#') + .expect('Content-type', /json/) + .expect(200) + .end((err, res) => { + if (err) done (err); + should(res.body).have.only.keys('_id', 'email', 'name', 'level', 'location', 'device_name'); + should(res.body).have.property('_id').be.type('string'); + should(res.body).have.property('email', 'jane.doe@bosch.com'); + should(res.body).have.property('name', 'janedoe'); + should(res.body).have.property('level', 'write'); + should(res.body).have.property('location', 'Rng'); + should(res.body).have.property('device_name', 'Alpha I'); + done(); + }); + }); + it('rejects requests from non-admins for another user', done => { + supertest(server) + .get('/user/admin') + .auth('janedoe', 'Xyz890*)') + .expect('Content-type', /json/) + .expect(403) + .end((err, res) => { + if (err) done (err); + should(res.body).be.eql({status: 'Forbidden'}); + done(); + }); + }); + it('rejects requests from a user API key', done => { + supertest(server) + .get('/user?key=5ea0450ed851c30a90e70899') + .expect('Content-type', /json/) + .expect(401) + .end((err, res) => { + if (err) done (err); + should(res.body).be.eql({status: 'Unauthorized'}); + done(); + }); + }); + it('rejects requests from an admin API key', done => { + supertest(server) + .get('/user/janedoe?key=5ea131671feb9c2ee0aafc9a') + .expect('Content-type', /json/) + .expect(401) + .end((err, res) => { + if (err) done (err); + should(res.body).be.eql({status: 'Unauthorized'}); + done(); + }); + }); +}); + + +describe('POST /user/new', () => { let server; before(done => { @@ -32,6 +283,7 @@ describe('/user/new', () => { .expect(200) .end((err, res) => { if (err) done (err); + should(res.body).have.only.keys('_id', 'email', 'name', 'level', 'location', 'device_name'); should(res.body).have.property('_id').be.type('string'); should(res.body).have.property('email', 'john.doe@bosch.com'); should(res.body).have.property('name', 'johndoe'); @@ -83,7 +335,7 @@ describe('/user/new', () => { it('rejects requests from non-admins', done => { supertest(server) .post('/user/new') - .auth('janedoe', 'Abc123!#') + .auth('janedoe', 'Xyz890*)') .send({email: 'john.doe@bosch.com', name: 'johndoe', pass: 'Abc123!#', level: 'read', location: 'Rng', device_name: 'Alpha II'}) .expect('Content-type', /json/) .expect(403) @@ -108,9 +360,7 @@ describe('/user/new', () => { }); - - -describe('/user/passreset', () => { +describe('POST /user/passreset', () => { let server; before(done => { @@ -191,11 +441,10 @@ describe('/user/passreset', () => { should(res.body).be.eql({status: 'OK'}); UserModel.find({name: 'janedoe'}).lean().exec( (err, data: any) => { if (err) return done(err); - console.log(data); should(data[0].pass).not.eql(oldpass); done(); }); }); }); }); -}); \ No newline at end of file +}); diff --git a/src/routes/user.ts b/src/routes/user.ts index cd67d14..0865fac 100644 --- a/src/routes/user.ts +++ b/src/routes/user.ts @@ -8,7 +8,24 @@ import mail from '../helpers/mail'; const router = express.Router(); router.get('/users', (req, res) => { - res.json({message: 'users up and running!'}); + if (!req.auth(res, ['admin'], 'basic')) return; + + UserModel.find({}).lean().exec( (err, data:any) => { + res.json(data.map(e => UserValidate.output(e)).filter(e => e !== null)); // validate all and filter null values from validation errors + }); +}); + +router.get('/user/:username*?', (req, res) => { + 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; + } + + UserModel.findOne({name: username}).lean().exec( (err, data:any) => { + res.json(UserValidate.output(data)); // validate all and filter null values from validation errors + }); }); router.post('/user/new', (req, res, next) => { @@ -22,7 +39,7 @@ router.post('/user/new', (req, res, next) => { } // check that user does not already exist - UserModel.find({name: user.name}).lean().exec( 'find', (err, data) => { + 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'}); diff --git a/src/routes/validate/user.ts b/src/routes/validate/user.ts index 1cccf41..26becdd 100644 --- a/src/routes/validate/user.ts +++ b/src/routes/validate/user.ts @@ -33,13 +33,14 @@ export default class UserValidate { // validate input for user } static output (data) { // validate output from database for needed properties, strip everything else - return joi.object({ + const {value, error} = joi.object({ _id: joi.any(), name: joi.string(), email: joi.string(), level: joi.string(), location: joi.string(), - device_name: joi.string() - }).validate(data, {stripUnknown: true}).value; + device_name: joi.string().allow('') + }).validate(data, {stripUnknown: true}) + return error !== undefined? null : value; } } diff --git a/src/test/db.json b/src/test/db.json index af2d78f..4c2c645 100644 --- a/src/test/db.json +++ b/src/test/db.json @@ -5,7 +5,7 @@ "_id": {"$oid":"5ea0450ed851c30a90e70894"}, "email": "jane.doe@bosch.com", "name": "janedoe", - "pass": "$2a$10$i872o3qR5V3JnbDArD8Z.eDo.BNPDBaR7dUX9KSEtl9pUjLyucy2K", + "pass": "$2a$10$di26XKF63OG0V00PL1kSK.ceCcTxDExBMOg.jkHiCnXcY7cN7DlPi", "level": "write", "location": "Rng", "device_name": "Alpha I",