feat(users): users & api tokens logic + tests for services
This commit is contained in:
@@ -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 => {
|
||||
|
||||
@@ -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 ) => {
|
||||
|
||||
@@ -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( )
|
||||
} )
|
||||
} )
|
||||
|
||||
} )
|
||||
|
||||
@@ -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' )
|
||||
|
||||
@@ -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' )
|
||||
}
|
||||
}
|
||||
Generated
+13
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user