feat(users): users & api tokens logic + tests for services

This commit is contained in:
Dimitrie Stefanescu
2020-03-28 20:08:40 +00:00
parent 789f5945fe
commit 89932ab651
7 changed files with 160 additions and 44 deletions
+6 -4
View File
@@ -16,15 +16,17 @@ exports.up = async knex => {
} )
await knex.schema.createTable( 'api_token', table => {
table.uuid( 'id' ).defaultTo( knex.raw( 'gen_random_uuid()' ) ).unique( )
table.string( 'token_digest' ).unique( ).primary( )
table.text( 'id' ).unique( ).primary( )
table.text( 'token_digest' ).unique( )
table.uuid( 'owner_id' ).references( 'id' ).inTable( 'users' ).notNullable( )
table.text( 'name' )
table.text( 'last_chars' )
table.specificType( 'scopes', 'text[]' )
table.boolean( 'revoked' ).defaultTo( false )
table.text( 'revoke_reason' )
table.timestamp( 'created_at' ).defaultTo( knex.fn.now( ) )
table.timestamp( 'last_used' ).defaultTo( knex.fn.now( ) )
} )
}
exports.down = async knex => {
+1 -1
View File
@@ -10,7 +10,7 @@ module.exports = {
createStream: ( stream ) => {
delete stream.id
delete stream.created_at
return Streams( ).returning( 'id' ).insert( stream )
// return Streams( ).returning( 'id' ).insert( stream )
},
getStream: ( id ) => {
+88 -27
View File
@@ -10,13 +10,14 @@ chai.use( chaiHttp )
const knex = require( `${root}/db/knex` )
const { createUser, getUser, updateUser, deleteUser, createToken, revokeToken } = require( '../users/queries' )
const { createUser, getUser, updateUser, deleteUser, validatePasssword, createToken, revokeToken, revokeTokenById, validateToken, getUserTokens } = require( '../users/queries' )
describe( 'Actors & Tokens', ( ) => {
let myTestActor = {
username: 'dim',
name: 'Dimitrie Stefanescu',
email: 'didimitrie@gmail.com'
email: 'didimitrie@gmail.com',
password: 'sn3aky-1337-b1m'
}
before( async ( ) => {
@@ -34,40 +35,100 @@ describe( 'Actors & Tokens', ( ) => {
describe( 'Services/Queries', ( ) => {
it( 'Should create an actor', async ( ) => {
let newUser = { ...myTestActor }
newUser.name = 'Bill Gates'
newUser.email = 'bill@gates.com'
newUser.username = 'bill'
newUser.password = 'testthebest'
describe( 'Users', ( ) => {
let actor = await createUser( newUser )
newUser.id = actor.id
it( 'Should create an actor', async ( ) => {
let newUser = { ...myTestActor }
newUser.name = 'Bill Gates'
newUser.email = 'bill@gates.com'
newUser.username = 'bill'
newUser.password = 'testthebest'
let actor = await createUser( newUser )
newUser.id = actor.id
} )
it( 'Should get an actor', async ( ) => {
let actor = await getUser( myTestActor.id )
expect( actor ).to.not.have.property( 'password_digest' )
} )
it( 'Should update an actor', async ( ) => {
let updatedActor = { ...myTestActor }
updatedActor.username = 'didimitrie'
await updateUser( myTestActor.id, updatedActor )
let actor = await getUser( myTestActor.id )
expect( actor.username ).to.equal( updatedActor.username )
} )
it( 'Should not update password', async ( ) => {
let updatedActor = { ...myTestActor }
updatedActor.password = "failwhale"
await updateUser( myTestActor.id, updatedActor )
let match = await validatePasssword( myTestActor.id, 'failwhale' )
expect( match ).to.equal( false )
} )
it( 'Should validate user password', async ( ) => {
let actor = {}
actor.password = 'super-test-200'
actor.email = 'e@ma.il'
actor.username = 'dimitrie'
actor.name = 'Bob Gates'
let id = await createUser( actor )
let match = await validatePasssword( id, 'super-test-200' )
expect( match ).to.equal( true )
let match_wrong = await validatePasssword( id, 'super-test-2000' )
expect( match_wrong ).to.equal( false )
} )
} )
it( 'Should get an actor', async ( ) => {
let actor = await getUser( myTestActor.id )
describe( 'API Tokens', ( ) => {
let myFirstToken
let pregeneratedToken
let revokedToken
} )
before( async ( ) => {
pregeneratedToken = await createToken( myTestActor.id, 'Whabadub', [ 'useless', 'scope:useless' ] )
revokedToken = await createToken( myTestActor.id, 'Mr. Revoked', [ ] )
} )
it( 'Should update an actor', async ( ) => {
let updatedActor = { ...myTestActor }
updatedActor.username = 'didimitrie'
it( 'Should create an api token', async ( ) => {
let scopes = [ 'streams', 'user:read' ]
let name = 'My Test Token'
await updateUser( myTestActor.id, updatedActor )
myFirstToken = await createToken( myTestActor.id, name, scopes )
expect( myFirstToken ).to.have.lengthOf( 42 )
} )
let actor = await getUser( myTestActor.id )
expect( actor.username ).to.equal( updatedActor.username )
// assert.equal( myTestActor.username, actor.username )
} )
it( 'Should validate a token', async ( ) => {
let res = await validateToken( pregeneratedToken )
expect( res ).to.have.property( 'valid' )
expect( res.valid ).to.equal( true )
expect( res ).to.have.property( 'scopes' )
expect( res ).to.have.property( 'userId' )
} )
it( 'Should create an api_token', async ( ) => {
assert.fail( )
} )
it( 'Should revoke an api_token', async ( ) => {
assert.fail( )
it( 'Should revoke an api token', async ( ) => {
await revokeToken( revokedToken )
let res = await validateToken( revokedToken )
expect( res ).to.have.property( 'valid' )
expect( res.valid ).to.equal( false )
} )
it( 'Should get the tokens of an user', async ( ) => {
let userTokens = await getUserTokens( myTestActor.id )
expect( userTokens ).to.be.an( 'array' )
expect( userTokens ).to.have.lengthOf( 2 )
// assert.fail( )
} )
} )
} )
+8 -2
View File
@@ -24,20 +24,25 @@ describe( 'Streams', ( ) => {
// await knex.migrate.rollback( )
} )
describe( 'Services/Queries', ( ) => {
describe( 'CRUD', ( ) => {
} )
describe( 'Integration', ( ) => {
let myTestStream = { name: 'woowowo', id: 'noids', description: 'wonderful test stream' }
it( 'Should create a stream', async ( ) => {
const res = await chai.request( app ).post( '/streams' ).send( myTestStream )
assert.fail( )
expect( res ).to.have.status( 200 )
expect( res.body ).to.have.property( 'id' )
} )
it( 'Should get a stream', async ( ) => {
const res = await chai.request( app ).get( `/streams/${myTestStream.id}` )
assert.fail( )
expect( res ).to.have.status( 200 )
expect( res.body ).to.have.property( 'id' )
@@ -47,6 +52,7 @@ describe( 'Streams', ( ) => {
it( 'Should update a stream', async ( ) => {
const res = await chai.request( app ).put( `/streams/${myTestStream.id}` ).send( { name: 'new name' } )
const resUpdated = await chai.request( app ).get( `/streams/${myTestStream.id}` )
assert.fail( )
expect( res ).to.have.status( 200 )
expect( res.body ).to.have.property( 'id' )
+43 -10
View File
@@ -1,11 +1,9 @@
'use strict'
const bcrypt = require( 'bcrypt' )
const crs = require( 'crypto-random-string' )
const root = require( 'app-root-path' )
const knex = require( `${root}/db/knex` )
// const Streams = ( ) => knex( 'streams' )
// const References = () => knex('references')
const Users = ( ) => knex( 'users' )
const Keys = ( ) => knex( 'api_token' )
@@ -24,28 +22,63 @@ module.exports = {
},
getUser: async ( id ) => {
let res = await Users( ).returning( 'id username name email profiles verified' ).where( { id: id } ).first( )
return res
return Users( ).where( { id: id } ).select( 'id', 'username', 'name', 'email', 'profiles', 'verified' ).first( )
},
updateUser: async ( id, user ) => {
delete user.id
delete user.password_digest
delete user.password
delete user.email
await Users( ).where( { id: id } ).update( user )
},
// throw new Error( 'not implemented' )
validatePasssword: async ( userId, password ) => {
var { password_digest } = await Users( ).where( { id: userId } ).select( 'password_digest' ).first( )
return bcrypt.compare( password, password_digest )
},
deleteUser: ( id ) => {
throw new Error( 'not implemented' )
},
createToken: ( userId, name, scopes ) => {
throw new Error( 'not implemented' )
createToken: async ( userId, name, scopes ) => {
let tokenId = crs( { length: 10 } )
let tokenString = crs( { length: 32 } )
let tokenHash = await bcrypt.hash( tokenString, 10 )
let last_chars = tokenString.slice( tokenString.length - 6, tokenString.length )
let res = await Keys( ).returning( 'id' ).insert( { id: tokenId, token_digest: tokenHash, last_chars: last_chars, owner_id: userId, name: name, scopes: scopes } )
return tokenId + tokenString
},
revokeToken: ( ) => {
throw new Error( 'not implemented' )
validateToken: async ( tokenString ) => {
let tokenId = tokenString.slice( 0, 10 )
let tokenContent = tokenString.slice( 10, 32 )
let token = await Keys( ).where( { id: tokenId } ).select( '*' ).first( )
if ( !token ) {
return { valid: false }
}
let valid = bcrypt.compare( tokenContent, token.token_digest )
if ( valid ) {
await Keys( ).where( { id: tokenId } ).update( { last_used: knex.fn.now( ) } )
return { valid: true, userId: token.owner_id, scopes: token.scopes }
} else
return { valid: false }
},
revokeToken: async ( tokenId ) => {
tokenId = tokenId.slice( 0, 10 )
await Keys( ).where( { id: tokenId } ).del( )
},
getUserTokens: async ( userId ) => {
return Keys( ).where( { owner_id: userId } ).select( 'id', 'name', 'last_chars', 'scopes', 'created_at', 'last_used' )
}
}
+13
View File
@@ -854,6 +854,14 @@
}
}
},
"crypto-random-string": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-3.2.0.tgz",
"integrity": "sha512-8vPu5bsKaq2uKRy3OL7h1Oo7RayAWB8sYexLKAqvCXVib8SxgbmoF1IN4QMKjBv8uI8mp5gPPMbiRah25GMrVQ==",
"requires": {
"type-fest": "^0.8.1"
}
},
"cz-conventional-changelog": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/cz-conventional-changelog/-/cz-conventional-changelog-3.1.0.tgz",
@@ -3631,6 +3639,11 @@
"integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==",
"dev": true
},
"type-fest": {
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz",
"integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA=="
},
"type-is": {
"version": "1.6.18",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
+1
View File
@@ -14,6 +14,7 @@
"app-root-path": "^3.0.0",
"bcrypt": "^4.0.1",
"body-parser": "^1.19.0",
"crypto-random-string": "^3.2.0",
"debug": "^4.1.1",
"express": "^4.17.1",
"knex": "^0.20.12",