feat(streams api): scaffolded routes and tests for basic stream routes
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
const express = require( 'express' )
|
||||
const logger = require( 'morgan-debug' )
|
||||
const bodyParser = require( 'body-parser' )
|
||||
const debug = require( 'debug' )( 'speckle:errors' )
|
||||
|
||||
exports.init = ( ) => {
|
||||
const app = express( )
|
||||
@@ -15,13 +16,14 @@ exports.init = ( ) => {
|
||||
app.use( bodyParser.urlencoded( { extended: false } ) )
|
||||
|
||||
// Error responses
|
||||
// app.use( ( err, req, res, next ) => {
|
||||
// res.status( err.status || 500 )
|
||||
// res.json( {
|
||||
// message: err.message,
|
||||
// error: err
|
||||
// } )
|
||||
// } )
|
||||
app.use( ( err, req, res, next ) => {
|
||||
debug( err )
|
||||
res.status( err.status || 500 )
|
||||
res.json( {
|
||||
message: err.message,
|
||||
error: err
|
||||
} )
|
||||
} )
|
||||
|
||||
app.get( '/', ( req, res ) => {
|
||||
res.send( { fantastic: 'speckle' } )
|
||||
|
||||
@@ -10,7 +10,7 @@ exports.up = async knex => {
|
||||
table.uuid( 'id' ).defaultTo( knex.raw( 'gen_random_uuid()' ) ).unique( ).primary( )
|
||||
table.text( 'name' )
|
||||
table.text( 'description' )
|
||||
table.boolean( 'public' ).defaultTo( true )
|
||||
table.boolean( 'isPublic' ).defaultTo( true )
|
||||
table.uuid( 'cloned_from' ).references( 'id' ).inTable( 'streams' )
|
||||
table.timestamp( 'created_at' ).defaultTo( knex.fn.now( ) )
|
||||
table.timestamp( 'updated_at' ).defaultTo( knex.fn.now( ) )
|
||||
@@ -29,9 +29,9 @@ exports.up = async knex => {
|
||||
|
||||
await knex.schema.createTable( 'stream_acl', table => {
|
||||
table.uuid( 'user_id' ).references( 'id' ).inTable( 'users' ).notNullable( )
|
||||
table.uuid( 'stream_id' ).references( 'id' ).inTable( 'streams' ).notNullable( )
|
||||
table.primary( [ 'user_id', 'stream_id' ] )
|
||||
table.unique( [ 'user_id', 'stream_id' ] )
|
||||
table.uuid( 'resource_id' ).references( 'id' ).inTable( 'streams' ).notNullable( )
|
||||
table.primary( [ 'user_id', 'resource_id' ] )
|
||||
table.unique( [ 'user_id', 'resource_id' ] )
|
||||
table.specificType( 'role', 'speckle_acl_role_type' ).defaultTo( 'write' )
|
||||
} )
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use strict'
|
||||
|
||||
const { getStream, createStream, updateStream } = require( './services' )
|
||||
const debug = require( 'debug' )( 'speckle:test' )
|
||||
const { getStream, createStream, updateStream, grantPermissionsStream, revokePermissionsStream } = require( './services' )
|
||||
|
||||
module.exports = {
|
||||
|
||||
@@ -10,24 +10,63 @@ module.exports = {
|
||||
},
|
||||
|
||||
getStream: async ( req, res, next ) => {
|
||||
res.status( 418 ).send( "meeps" )
|
||||
next( )
|
||||
try {
|
||||
let stream = await getStream( req.params.resourceId )
|
||||
res.status( 200 ).send( stream )
|
||||
next( )
|
||||
} catch ( err ) {
|
||||
next( err )
|
||||
}
|
||||
},
|
||||
|
||||
createStream: async ( req, res, next ) => {
|
||||
try {
|
||||
let id = await createStream( req.body, req.user.userId )
|
||||
let id = await createStream( req.body, req.user.id )
|
||||
res.status( 201 ).send( { success: true, id: id } )
|
||||
|
||||
req.eventData = { id: id, userId: req.user.userId }
|
||||
next( )
|
||||
} catch ( err ) {
|
||||
console.log( err )
|
||||
next( err )
|
||||
}
|
||||
},
|
||||
|
||||
updateStream: async ( req, res, next ) => {
|
||||
res.send( { todo: true } )
|
||||
next( )
|
||||
try {
|
||||
let id = await updateStream( req.params.resourceId, req.body )
|
||||
res.status( 200 ).send( { success: true, id: id } )
|
||||
|
||||
req.eventData = { id: id, userId: req.user.userId }
|
||||
next( )
|
||||
} catch ( err ) {
|
||||
next( err )
|
||||
}
|
||||
},
|
||||
|
||||
grantPermissions: async ( req, res, next ) => {
|
||||
try {
|
||||
await grantPermissionsStream( req.params.resourceId, req.body.id, req.body.role )
|
||||
|
||||
next( )
|
||||
} catch ( err ) {
|
||||
next( err )
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
revokePermissions: async ( req, res, next ) => {
|
||||
try {
|
||||
|
||||
} catch ( err ) {
|
||||
next( err )
|
||||
}
|
||||
},
|
||||
|
||||
getStreamUsers: async ( req, res, next ) => {
|
||||
try {
|
||||
|
||||
} catch ( err ) {
|
||||
next( err )
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
'use strict'
|
||||
const root = require( 'app-root-path' )
|
||||
const { getStreams, getStream, createStream, updateStream } = require( './controllers' )
|
||||
const { getStreams, getStream, createStream, updateStream, grantPermissions, revokePermissions, getStreamUsers } = require( './controllers' )
|
||||
const { authenticate, authorize, announce } = require( `${root}/modules/shared` )
|
||||
|
||||
const streams = require( 'express' ).Router( { mergeParams: true } )
|
||||
@@ -13,21 +13,43 @@ streams.get(
|
||||
getStreams )
|
||||
|
||||
streams.get(
|
||||
'/streams/:streamId',
|
||||
authenticate( 'streams:read' ),
|
||||
authorize,
|
||||
'/streams/:resourceId',
|
||||
authenticate( 'streams:read', false ),
|
||||
authorize( 'stream_acl', 'streams', 'read' ),
|
||||
getStream )
|
||||
|
||||
streams.post(
|
||||
'/streams',
|
||||
authenticate( 'streams:write' ),
|
||||
authorize,
|
||||
createStream,
|
||||
announce( 'stream-created', 'user' ) )
|
||||
|
||||
streams.put(
|
||||
'/streams/:streamId',
|
||||
'/streams/:resourceId',
|
||||
authenticate( 'streams:write' ),
|
||||
authorize,
|
||||
authorize( 'stream_acl', 'streams', 'write' ),
|
||||
updateStream,
|
||||
announce( 'stream-updated', 'stream' ) )
|
||||
announce( 'stream-updated', 'stream' ) )
|
||||
|
||||
streams.post(
|
||||
'/streams/:resourceId/users',
|
||||
authenticate( 'streams:write' ),
|
||||
authorize( 'stream_acl', 'streams', 'owner' ),
|
||||
grantPermissions,
|
||||
announce( 'stream-created', 'user' )
|
||||
)
|
||||
|
||||
streams.get(
|
||||
'/streams/:resourceId/users',
|
||||
authenticate( 'streams:read' ),
|
||||
authorize( 'stream_acl', 'streams', 'read' ),
|
||||
getStreamUsers
|
||||
)
|
||||
|
||||
streams.delete(
|
||||
'/streams/:resourceId/users',
|
||||
authenticate( 'streams:write' ),
|
||||
authorize( 'stream_acl', 'streams', 'owner' ),
|
||||
revokePermissions,
|
||||
announce( 'stream-deleted', 'user' )
|
||||
)
|
||||
@@ -14,7 +14,7 @@ module.exports = {
|
||||
stream.updated_at = knex.fn.now( )
|
||||
|
||||
let [ res ] = await Streams( ).returning( 'id' ).insert( stream )
|
||||
await Acl( ).insert( { user_id: ownerId, stream_id: res, role: 'owner' } )
|
||||
await Acl( ).insert( { user_id: ownerId, resource_id: res, role: 'owner' } )
|
||||
|
||||
return res
|
||||
},
|
||||
@@ -32,19 +32,19 @@ module.exports = {
|
||||
|
||||
grantPermissionsStream: async ( streamId, userId, role ) => {
|
||||
if ( role === 'owner' ) {
|
||||
let [ ownerAcl ] = await Acl( ).where( { stream_id: streamId, role: 'owner' } ).returning( '*' ).del( )
|
||||
await Acl( ).insert( { stream_id: streamId, user_id: ownerAcl.user_id, role: 'write' } )
|
||||
let [ ownerAcl ] = await Acl( ).where( { resource_id: streamId, role: 'owner' } ).returning( '*' ).del( )
|
||||
await Acl( ).insert( { resource_id: streamId, user_id: ownerAcl.user_id, role: 'write' } )
|
||||
}
|
||||
|
||||
// upsert
|
||||
let query = Acl( ).insert( { user_id: userId, stream_id: streamId, role: role } ).toString( ) + ` on conflict on constraint stream_acl_pkey do update set role=excluded.role`
|
||||
let query = Acl( ).insert( { user_id: userId, resource_id: streamId, role: role } ).toString( ) + ` on conflict on constraint stream_acl_pkey do update set role=excluded.role`
|
||||
|
||||
await knex.raw( query )
|
||||
},
|
||||
|
||||
revokePermissionsStream: async ( streamId, userId ) => {
|
||||
let streamAclEntries = Acl( ).where( { stream_id: streamId } ).select( '*' )
|
||||
let delCount = await Acl( ).where( { stream_id: streamId, user_id: userId } ).whereNot( { role: 'owner' } ).del( )
|
||||
let streamAclEntries = Acl( ).where( { resource_id: streamId } ).select( '*' )
|
||||
let delCount = await Acl( ).where( { resource_id: streamId, user_id: userId } ).whereNot( { role: 'owner' } ).del( )
|
||||
if ( delCount === 0 )
|
||||
throw new Error( 'Could not revoke permissions for user. Is he an owner?' )
|
||||
},
|
||||
@@ -67,7 +67,7 @@ module.exports = {
|
||||
limit = limit || 100
|
||||
|
||||
return Acl( ).where( { user_id: userId } )
|
||||
.rightJoin( 'streams', { 'streams.id': 'stream_acl.stream_id' } )
|
||||
.rightJoin( 'streams', { 'streams.id': 'stream_acl.resource_id' } )
|
||||
.limit( limit ).offset( offset )
|
||||
},
|
||||
}
|
||||
@@ -112,7 +112,7 @@ describe( 'Streams', ( ) => {
|
||||
}
|
||||
} )
|
||||
|
||||
it( 'A stream should not have more than one owner', async ( ) => {
|
||||
it( '🤔 DUBIOUS: A stream should not have more than one owner', async ( ) => {
|
||||
let newStream = { name: 'XXX' }
|
||||
newStream.id = await createStream( newStream, userOne.id )
|
||||
await grantPermissionsStream( newStream.id, userTwo.id, 'owner' )
|
||||
@@ -134,44 +134,89 @@ describe( 'Streams', ( ) => {
|
||||
|
||||
// The express app
|
||||
let app
|
||||
let token
|
||||
|
||||
|
||||
let userA = { username: 'A', name: 'DimitrieA ', email: 'didimitrie+a@gmail.com', password: 'sn3aky-1337-b1m' }
|
||||
let userB = { username: 'B', name: 'DimitrieB ', email: 'didimitrie+b@gmail.com', password: 'sn3aky-1337-b1m' }
|
||||
|
||||
let tokenA
|
||||
let tokenB
|
||||
|
||||
before( async ( ) => {
|
||||
app = init( )
|
||||
token = await createToken( userOne.id, 'Generic Token', [ 'streams:read', 'streams:write' ] )
|
||||
userA.id = await createUser( userA )
|
||||
userB.id = await createUser( userB )
|
||||
|
||||
tokenA = await createToken( userA.id, 'Generic Token', [ 'streams:read', 'streams:write' ] )
|
||||
tokenB = await createToken( userB.id, 'Generic Token', [ 'streams:read', 'streams:write' ] )
|
||||
} )
|
||||
|
||||
let myTestStream = { name: 'woowowo', id: 'noids', description: 'wonderful test stream' }
|
||||
let privateStream = { name: 'woowowo', id: 'noids', description: 'wonderful test stream', isPublic: false }
|
||||
let publicStream = { name: 'i am public', isPublic: true }
|
||||
|
||||
it( 'Should create a stream', async ( ) => {
|
||||
const res = await chai.request( app ).post( '/streams' ).set( 'Authorization', `Bearer ${token}` ).send( myTestStream )
|
||||
const res = await chai.request( app ).post( '/streams' ).set( 'Authorization', `Bearer ${tokenA}` ).send( privateStream )
|
||||
expect( res ).to.have.status( 201 )
|
||||
expect( res.body ).to.have.property( 'id' )
|
||||
myTestStream.id = res.body.id
|
||||
privateStream.id = res.body.id
|
||||
|
||||
const second = await chai.request( app ).post( '/streams' ).set( 'Authorization', `Bearer ${tokenA}` ).send( publicStream )
|
||||
expect( second ).to.have.status( 201 )
|
||||
expect( second.body ).to.have.property( 'id' )
|
||||
publicStream.id = second.body.id
|
||||
|
||||
} )
|
||||
|
||||
it( 'Should get a stream', async ( ) => {
|
||||
assert.fail( 'Not implemented yet.' )
|
||||
|
||||
const res = await chai.request( app ).get( `/streams/${myTestStream.id}` ).set( 'Authorization', `Bearer ${token}` )
|
||||
const res = await chai.request( app ).get( `/streams/${privateStream.id}` ).set( 'Authorization', `Bearer ${tokenA}` )
|
||||
|
||||
expect( res ).to.have.status( 200 )
|
||||
expect( res.body ).to.have.property( 'id' )
|
||||
expect( res.body ).to.have.property( 'name' )
|
||||
} )
|
||||
|
||||
it( 'Should update a stream', async ( ) => {
|
||||
assert.fail( 'Not implemented yet.' )
|
||||
it( 'Should get a public stream, even if user is anonymous', async ( ) => {
|
||||
const res = await chai.request( app ).get( `/streams/${publicStream.id}` )
|
||||
expect( res ).to.have.status( 200 )
|
||||
expect( res.body ).to.have.property( 'id' )
|
||||
expect( res.body ).to.have.property( 'name' )
|
||||
} )
|
||||
|
||||
const res = await chai.request( app ).put( `/streams/${myTestStream.id}` ).send( { name: 'new name' } )
|
||||
const resUpdated = await chai.request( app ).get( `/streams/${myTestStream.id}` )
|
||||
it( 'Should not get a private stream if user is not authenticated', async ( ) => {
|
||||
const res = await chai.request( app ).get( `/streams/${privateStream.id}` )
|
||||
expect( res ).to.have.status( 401 )
|
||||
} )
|
||||
|
||||
it( 'Should not get a private stream if the user does not have access to it', async ( ) => {
|
||||
const res = await chai.request( app ).get( `/streams/${privateStream.id}` ).set( 'Authorization', `Bearer ${tokenB}` )
|
||||
expect( res ).to.have.status( 401 )
|
||||
} )
|
||||
|
||||
it( 'Should update a stream', async ( ) => {
|
||||
const res = await chai.request( app ).put( `/streams/${publicStream.id}` ).send( { name: 'new name' } ).set( 'Authorization', `Bearer ${tokenA}` )
|
||||
const resUpdated = await chai.request( app ).get( `/streams/${publicStream.id}` )
|
||||
|
||||
expect( res ).to.have.status( 200 )
|
||||
expect( res.body ).to.have.property( 'id' )
|
||||
|
||||
expect( resUpdated ).to.have.status( 200 )
|
||||
expect( resUpdated ).to.have.property( 'name' )
|
||||
expect( resUpdated.name ).to.equal( 'new name' )
|
||||
expect( resUpdated.body ).to.have.property( 'name' )
|
||||
expect( resUpdated.body.name ).to.equal( 'new name' )
|
||||
} )
|
||||
|
||||
it( 'Should grant permissions on a stream', async ( ) => {
|
||||
const shareRes = await chai.request( app ).post( `/streams/users/${privateStream.id}` ).send( { id: userB.id, role: 'read' } ).set( 'Authorization', `Bearer ${tokenA}` )
|
||||
console.log(shareRes)
|
||||
expect( shareRes ).to.have.status( 200 )
|
||||
const userBRes = await chai.request( app ).get( `/streams/${privateStream.id}` ).set( 'Authorization', `Bearer ${tokenB}` )
|
||||
console.log(userBRes.status)
|
||||
expect( userBRes ).to.have.status( 200 )
|
||||
expect( userBRes.body ).to.have.property( 'name' )
|
||||
expect( userBRes.body ).to.have.property( 'description' )
|
||||
} )
|
||||
|
||||
it( 'Should revoke permissions on a stream', async ( ) => {
|
||||
assert.fail( )
|
||||
|
||||
} )
|
||||
|
||||
it( 'Should delete a stream', async ( ) => {
|
||||
|
||||
+68
-18
@@ -1,46 +1,96 @@
|
||||
'use strict'
|
||||
|
||||
const debug = require( 'debug' )( 'speckle:test' )
|
||||
const debug = require( 'debug' )( 'speckle:middleware' )
|
||||
const root = require( 'app-root-path' )
|
||||
const knex = require( `${root}/db/knex` )
|
||||
const { validateToken } = require( `${root}/modules/core/users/services` )
|
||||
|
||||
/*
|
||||
|
||||
Authentication: checks bearer token validity and scope validation
|
||||
|
||||
*/
|
||||
|
||||
|
||||
// TODO: Cache results
|
||||
function authenticate( scope, mandatory ) {
|
||||
mandatory = mandatory || true
|
||||
mandatory = mandatory !== false
|
||||
|
||||
return async ( req, res, next ) => {
|
||||
debug( `🔑 authenticate middleware called` )
|
||||
|
||||
|
||||
if ( !req.headers.authorization && mandatory ) {
|
||||
return res.status( 403 ).send( { error: 'No credentials provided' } ) // next (err)?
|
||||
}
|
||||
|
||||
let token = req.headers.authorization.split( ' ' )[ 1 ]
|
||||
let { valid, scopes, userId } = await validateToken( token )
|
||||
if ( req.headers.authorization ) {
|
||||
let token = req.headers.authorization.split( ' ' )[ 1 ]
|
||||
let { valid, scopes, userId } = await validateToken( token )
|
||||
|
||||
if ( !valid && mandatory ) {
|
||||
return res.status( 403 ).send( { error: 'Invalid authorization' } )
|
||||
if ( !valid && mandatory ) {
|
||||
return res.status( 403 ).send( { error: 'Invalid authorization' } )
|
||||
}
|
||||
|
||||
if ( scopes.indexOf( scope ) === -1 && scopes.indexOf( '*' ) === -1 ) {
|
||||
return res.status( 403 ).send( { error: 'Invalid scope' } )
|
||||
}
|
||||
req.user = { id: userId, scopes: scopes }
|
||||
return next( )
|
||||
}
|
||||
|
||||
if ( scopes.indexOf( scope ) === -1 && scopes.indexOf( '*' ) === -1 ) {
|
||||
return res.status( 403 ).send( { error: 'Invalid scope' } )
|
||||
}
|
||||
|
||||
req.user = { userId: userId, scopes: scopes }
|
||||
next( )
|
||||
return next( )
|
||||
}
|
||||
}
|
||||
|
||||
let authorize = ( req, res, next ) => {
|
||||
// TODO
|
||||
// Authorizes the api call against permissions
|
||||
debug( '🔑 authorization middleware called; yes by default LOL' )
|
||||
next( )
|
||||
/*
|
||||
|
||||
Authorization: checks user id against access control lists
|
||||
|
||||
*/
|
||||
|
||||
let roles = { admin: 400, owner: 300, write: 200, read: 100 }
|
||||
|
||||
// TODO: Cache results
|
||||
function authorize( aclTable, resourceTable, requiredRole ) {
|
||||
let ACL = ( ) => knex( aclTable )
|
||||
let Resource = ( ) => knex( resourceTable )
|
||||
|
||||
return async ( req, res, next ) => {
|
||||
debug( '🔑 authorization middleware called' )
|
||||
|
||||
let { isPublic } = await Resource( ).where( { id: req.params.resourceId } ).select( 'isPublic' ).first( )
|
||||
|
||||
if ( isPublic ) return next( )
|
||||
|
||||
if ( !req.user ) return res.status( 401 ).send( { error: 'Unauthorized' } )
|
||||
|
||||
let [ entry ] = await ACL( ).where( { resource_id: req.params.resourceId, user_id: req.user.id } ).select( '*' )
|
||||
|
||||
if ( !entry ) {
|
||||
return res.status( 401 ).send( { error: 'Unauthorized' } )
|
||||
}
|
||||
|
||||
if ( roles[ entry.role ] >= roles[ requiredRole ] ) {
|
||||
req.user.role = entry.role
|
||||
return next( )
|
||||
} else {
|
||||
return res.status( 401 ).send( { error: 'Unauthorized' } )
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
Announcements: orchestrates pushing out events to any subscribers.
|
||||
(TODO: implement!)
|
||||
|
||||
*/
|
||||
|
||||
function announce( eventName, eventScope ) {
|
||||
return async ( req, res, next ) => {
|
||||
debug( `📣 announce middleware called: ${eventName}:${eventScope}` )
|
||||
debug( `Event data: ${JSON.stringify( req.eventData )}` )
|
||||
|
||||
next( )
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -5,8 +5,8 @@
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"dev": "NODE_ENV=development DEBUG=www:server,speckle:* nodemon ./bin/www --watch . --watch ./bin/www",
|
||||
"test": "DEBUG=speckle:test NODE_ENV=test mocha -s 0 --exit",
|
||||
"test-watch": "DEBUG=speckle:test NODE_ENV=test mocha --watch -s 0 --exit"
|
||||
"test": "DEBUG=speckle:test,speckle:errors NODE_ENV=test mocha -s 0 --exit",
|
||||
"test-watch": "DEBUG=speckle:test,speckle:errors NODE_ENV=test mocha --watch -s 0 --exit"
|
||||
},
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
|
||||
Reference in New Issue
Block a user