import { Collection , ComponentRequestTable , ConceptDb , ConceptServer , ConceptServerBuilder , Config , Context , getReturnFields , Validation } from '@deja-vu/concept-server' ; import * as bcrypt from 'bcryptjs' ; import { IResolvers } from 'graphql-tools' ; import * as jwt from 'jsonwebtoken' ; import * as _ from 'lodash' ; import { v4 as uuid } from 'uuid' ; import { ChangePasswordInput , RegisterInput , SignInInput , SignInOutput , User , UserDoc , VerifyInput } from './schema' ; const USERNAME_MIN_LENGTH = 3 ; const USERNAME_MAX_LENGTH = 15 ; const USERNAME_REGEX = new RegExp ( '^(?![_.-])(?!.*[_.-]{2})[a-zA-Z0-9._-]+$' ) ; const USERNAME_PATTERN_MSG = 'alphanumeric and special characters ._-' ; const PASSWORD_MIN_LENGTH = 8 ; const PASSWORD_MAX_LENGTH = 20 ; const PASSWORD_REGEX = new RegExp ( [ '^.*(?=.*[a-zA-Z])(?=.*[0-9])' , '(?=.*[!@#$%^&*])(?!.*[`~()\\-_=+[{\\]}\\\|;:\\\'",.<>/? ]).*$' ] . join ( '' ) ) ; const PASSWORD_PATTERN_MSG = 'at least 1 lowercase letter, 1 uppercase ' + 'letter, 1 special character (!@#$%^*&) and 1 number (0-9)' ; const SECRET_KEY = 'ultra-secret-key' ; const SALT_ROUNDS = 10 ; class UserValidation { static async userExistsById ( users , userId ) { return Validation . existsOrFail ( users , userId , 'User' ) ; } static async userExistsByUsername ( users , username ) { return await users . findOne ( { username : username } ) ; } static async verifyPassword ( inputPassword , savedPassword ) { const passwordVerified = await bcrypt . compare ( inputPassword , savedPassword ) ; if ( ! passwordVerified ) { throw new Error ( 'Incorrect password.' ) ; } return passwordVerified ; } static isUsernameValid ( username ) { return ( UserValidation . isLengthValid ( username , USERNAME_MIN_LENGTH , USERNAME_MAX_LENGTH , 'Username' ) && UserValidation . isPatternValid ( username , USERNAME_REGEX , 'Username' , USERNAME_PATTERN_MSG ) ) ; } static isPasswordValid ( password ) { return ( UserValidation . isLengthValid ( password , PASSWORD_MIN_LENGTH , PASSWORD_MAX_LENGTH , 'Password' ) && UserValidation . isPatternValid ( password , PASSWORD_REGEX , 'Password' , PASSWORD_PATTERN_MSG ) ) ; } static isLengthValid ( value , minLength , maxLength , type ) { const length = value . length ; if ( length < minLength || length > maxLength ) { throw new Error ( `${ type } must be ${ minLength }-${ maxLength } characters
      long.` ) ; } return true ; } static isPatternValid ( value , regExp , type , msg ) { const valid = regExp . test ( value ) ; if ( ! valid ) { throw new Error ( `${ type } must contain ${ msg }` ) ; } return valid ; } } const componentRequestTable = { authenticate : ( extraInfo ) => `
    query Authenticate($input: VerifyInput!) {
      verify(input: $input) ${ getReturnFields ( extraInfo ) }
    }
  ` , 'change-password' : ( extraInfo ) => `
    mutation ChangePassword($input: ChangePasswordInput!) {
      changePassword (input: $input) ${ getReturnFields ( extraInfo ) }
    }
  ` , 'register-user' : ( extraInfo ) => { switch ( extraInfo . action ) { case 'login' : return `
          mutation Register($input: RegisterInput!) {
            registerAndSignIn(input: $input) ${ getReturnFields ( extraInfo ) }
          }
        ` ; case 'register-only' : return `
          mutation Register($input: RegisterInput!) {
            register(input: $input) ${ getReturnFields ( extraInfo ) }
          }
        ` ; default : throw new Error ( 'Need to specify extraInfo.action' ) ; } } , 'show-user' : ( extraInfo ) => `
    query ShowUser($id: String!) {
      userById(id: $id) ${ getReturnFields ( extraInfo ) }
    }
  ` , 'show-user-count' : ( extraInfo ) => `
    query ShowUserCount {
      userCount ${ getReturnFields ( extraInfo ) }
    }
  ` , 'show-users' : ( extraInfo ) => `
    query ShowUsers($input: UsersInput!) {
      users(input: $input) ${ getReturnFields ( extraInfo ) }
    }
  ` , 'sign-in' : ( extraInfo ) => `
    mutation SignIn($input: SignInInput!) {
      signIn (input: $input) ${ getReturnFields ( extraInfo ) }
    }
  ` } ; async function getNewUserDoc ( input ) { UserValidation . isUsernameValid ( input . username ) ; UserValidation . isPasswordValid ( input . password ) ; const id = input . id ? input . id : uuid ( ) ; const hash = await bcrypt . hash ( input . password , SALT_ROUNDS ) ; return { id : id , username : input . username , password : hash } ; } async function register ( users , input , context ) { const newUser = await getNewUserDoc ( input ) ; try { return await users . insertOne ( context , newUser ) ; } catch ( e ) { throw new Error ( 'User already exists' ) ; } } function sign ( userId ) { return jwt . sign ( userId , SECRET_KEY ) ; } function verify ( token , userId ) { if ( _ . isNil ( token ) ) { return false ; } const tokenUserId = jwt . verify ( token , SECRET_KEY ) ; return tokenUserId === userId ; } function resolvers ( db , _config ) { const users = db . collection ( 'users' ) ; return { Query : { users : async ( ) => await users . find ( ) , user : async ( _root , { username } ) => await users . findOne ( { username } ) , userById : async ( _root , { id } ) => await users . findOne ( { id : id } ) , verify : async ( _root , { input } ) => { let id ; if ( _ . isNil ( input . id ) ) { if ( _ . isNil ( input . username ) ) { throw new Error ( `Verification failed, no id or username given` ) ; } const user = await users . findOne ( { username : input . username } ) ; id = user . id ; } else { id = input . id ; } if ( verify ( input . token , id ) ) { return true ; } throw new Error ( `Verification for id ${ input . id }, username ${ input . username } failed` ) ; } } , User : { id : ( user ) => user . id , username : ( user ) => user . username } , SignInOutput : { token : ( signInOutput ) => signInOutput . token , user : ( signInOutput ) => signInOutput . user } , Mutation : { register : ( _root , { input } , context ) => { return register ( users , input , context ) ; } , registerAndSignIn : async ( _root , { input } , context ) => { const user = await register ( users , input , context ) ; const token = sign ( user ! . id ) ; return { token : token , user : user } ; } , signIn : async ( _root , { input } ) => { const user = await UserValidation . userExistsByUsername ( users , input . username ) ; await UserValidation . verifyPassword ( input . password , user . password ) ; const token = sign ( user . id ) ; return { token : token , user : user } ; } , changePassword : async ( _root , { input } , context ) => { UserValidation . isPasswordValid ( input . newPassword ) ; const user = await UserValidation . userExistsById ( users , input . id ) ; await UserValidation . verifyPassword ( input . oldPassword , user . password ) ; const newPasswordHash = await bcrypt . hash ( input . newPassword , SALT_ROUNDS ) ; const updateOp = { $set : { password : newPasswordHash } } ; return await users . updateOne ( context , { id : input . id } , updateOp ) ; } } } ; } const authenticationConcept = new ConceptServerBuilder ( 'authentication' ) . initDb ( ( db , _config ) : Promise < any > => { const users = db . collection ( 'users' ) ; return Promise . all ( [ users . createIndex ( { id : 1 } , { unique : true , sparse : true } ) , users . createIndex ( { username : 1 } , { unique : true , sparse : true } ) ] ) ; } ) . componentRequestTable ( componentRequestTable ) . resolvers ( resolvers ) . build ( ) ; authenticationConcept . start ( ) ;