Source: lib/general_auth.js

//
const {GeneralAuth,SessionManager_Lite} = require('./general_auth_session_lite')
// Top level of session managers, effectively supplying an interface for intialization
// Does some heavy lifting in controlling the flow of authorization and permission.
//


/** 
 * @extends SessionManager_Lite from general_auth_session_lite
 * Provide the methods that will process login, registration, logout, and session initalization.
 * Also, there are methods dealing with the existene of the user and possible database storage of the user.
 * 
 * NOTE: all the base classes in /lib return the class and do not return an instance. Explore the applications and 
 * the reader will find that the descendant modules export instances. The classes provided by copious-transitions must
 * be extended by an application.
 * 
 * @memberof base
 */
class SessionManager extends SessionManager_Lite {
    //
    constructor(exp_app,db_obj,business) {  // reference to the DB and initializes the a middleware vector
        super(exp_app,db_obj,business)
        //
        //
        let conf = this.conf
        this.current_auth_strategy = "local"
        //this.foreign_authorizer_api_enpoint = conf.foreign_auth.api_enpoint
        this.login_amelioration = conf.login_amelioration
        this.password_fail_amelioration = conf.failed_password_amelioration
        this.registration_fail_amelioration = conf.failed_registration_duplicate
        this.second_login_amelioration = conf.second_login_amelioration
    }

    // foreign_authorizer -- add the endpoint to the authorizer field
    /*
    foreign_authorizer(primary_key,tobj,body)  {
        tobj.foreign_authorizer_endpoint = this.foreign_authorizer_api_enpoint + '/start/' + body.strategy + '/' + body[primary_key]  + '/' + tobj.token
        return(tobj)
    }
    */

    /*
    async prep_foreign_auth(user,post_body,transtion_object) {
        let key_key = this.key_for_user()
        transtion_object._t_u_key = key_key
        transtion_object.user_key = user[key_key]
        this.loginTransitionFields(transtion_object,post_body,user)
        this.foreign_authorizer(primary_key,transtion_object,post_body)
        return(transtion_object)    
    }
    */


    /**
     *  By default this method just checks to see if two strings are equal.
     *  Applications will want to do something more sophisticated, e.g. checking a signature.
     * 
     * @param {string} db_password 
     * @param {string} client_password 
     * @returns {boolean} - true if the comparison passes 
     */
    async password_check(db_password,client_password) {
        return(db_password === client_password)  /// app should override this with a secure password check
    }


    // default behavior -- should override
    /**
     * By default, this method returns the parameter untouched. 
     * Applications may hash the password or may encrypt or decrypt in order to find a string for comparison.
     * 
     * @param {string} password 
     * @returns {string} - the hash of the password
     */
    hash_pass(password) {
        return(password)
    }


    /**
     * 
     * @param {object} transtion_object 
     * @param {object} post_body 
     * @param {object} user 
     */
    loginTransitionFields(transtion_object,post_body,user) {
        // make tokens (first the transition token and then the session token)
        let sess_tok = this.generate_session_token(post_body)
        let token = this.generate_transition_token('user+',sess_tok)
        // put the session token into the transition object in its special field
        transtion_object.set_session('sess+' + sess_tok)
        transtion_object.set_token(token) // this token identifies this transition object
        //
        transtion_object.strategy = post_body.strategy
        // 
        // Match the sessions's transition token later.
        transtion_object.elements = { "match" :  transtion_object.token }
        this.stash_session_token(user,transtion_object)   // stashing the token keeps the token with the server side transition object
    }


    /**
     * This method first checks the DB user password against password data obtained from the client.
     * Given the password check works, the transition object is flagged for a secondary action.
     * 
     * The `loginTransitionFields` is called in order to create the session identifier and store it in local in-memory 
     * hash tables.
     * 
     * @param {object} user 
     * @param {object} transtion_object 
     * @param {object} post_body 
     * @returns {boolean} - true if failed indicating that a corretive action must be done - false indicating no required action
     */
    async login_transition(user,transtion_object,post_body) {
        if ( user.password && post_body.password ) {
            let post_pass = await this.hash_pass(post_body.password)
            if ( await this.password_check(user.password,post_pass) ) {
                transtion_object.secondary_action = true
                let key_key = this.key_for_user() // a field name set by the application override of key_for_user.
                //
                transtion_object._t_u_key = key_key
                transtion_object.user_key = user[key_key]  // this data is to be cached for the secondary action
                //
                await this.loginTransitionFields(transtion_object,post_body,user)
                return false
            } else {
                return this.password_fail_amelioration
            }
        }
    }


    /**
     * This method stores a user in the data base for the first time.
     * 
     * In some applications this storing of the user is something perpetual.
     * In other applications, the user is stored newly each time with permanent identity storage happening elsewhere.
     * 
     * The registration can also create a session by calling `generate_session_token`
     * This also creates an elements map with a `match` field for the session.
     * 
     * @param {object} post_body 
     * @param {object} transtion_object 
     * @returns {boolean} The result is true if the user can be stored in the DB, false otherwise.
     */
    async registration_transition(post_body,transtion_object) {
        if ( post_body.password ) {
            post_body.password = await this.hash_pass(post_body.password)
            if ( post_body.verify_password ) {
                delete post_body.verify_password
            }
            await this.db.store_user(post_body)         /// CREATE USER ENTRY IN DB (store a relationship object)
            if ( this.business ) this.business.process('new-user',post_body)
            //
            let token = this.generate_transition_token('user+')   // transaction token
            let registration_token = this.generate_session_token(post_body)
            transtion_object.set_token(token)
            transtion_object.set_session('sess+' + registration_token)
            //
            transtion_object.add_to_elements({ "match" : 'sess+' + registration_token })
            return true;
        }
        return false
    }



    /**
     * This method is the target of user processing classes, that can be found in the contractual directory.
     * 
     * The user processing classes help with the existence of users and the management of their sessions.
     * 
     * There operations that this method handles are `login`, `logout` and `registration`.
     * 
     * Handling `forgot`, for forgotten passwords is deprecated and may become be handled by transition processing at a later date.
     * 
     * 
     * The `logout` operation leads to a call to `destroySession`.
     * 
     * The operations `login` and `register` lead to calls to their `transition` 
     * 
     * 
     * @param {string} user_op 
     * @param {object} post_body 
     * @param {object} res 
     * @param {string} primary_key 
     * @returns {object} - the transition object made for continuing the requested operation
     */
    async process_user(user_op,post_body,res,primary_key) {
        let use_ammelioration = false
        //
        let transtion_object = this.create_transition_record('user',user_op)
        //
        post_body = this.post_body_decode(post_body)

        switch ( user_op ) {
            case 'login' : {
                if ( this.db ) {
                    let all = true
                    let user = await this.db.fetch_user(post_body,all)
                    if ( user ) {
                        if ( user.logged_in ) {
                            //transtion_object.amelioritive_action = this.second_login_amelioration
                            //break;
                        }
                        //
                        use_ammelioration = await this.login_transition(user,transtion_object,post_body)
                    } else {
                        use_ammelioration = this.login_amelioration
                    }
                }
                break;
                // removed foreign auth 6/27/23
            }
            case 'logout' : {
                if ( this.db ) {
                    let key = this.token_to_key(post_body.token)  // get an ownership key (e.g. ucwid)
                    if ( key === undefined ) return;   // Logout can be requested for dead sessions...
                    let query = {}
                    query[primary_key] = key
                    let user = await this.db.fetch_user(query)
                    if ( user ) {
                        user.logged_in = false
                        this.db.update_user(user)
                    }
                    //
                    let token = post_body.token     // going to take the transition token (state graph) and get the session from it for now
                    transtion_object.clear_session()  // going to whip out the session idenitifer in the rest of execution
                    //
                    if ( token && this.sessionCurrent(token,key) ) {  // there has to be a transition token associated with managing session state transisions
                        this.destroySession(token)      // still use the transition token to get the sesion and destroy the actual sesssion references.
                        if ( res ) this.app_user_release_cookie(res)
                    }
                }
                break;
            }
            case 'register' : {
                if ( this.db ) {
                    let user = await this.db.fetch_user(post_body)
                    if ( !(user) ) {
                        let processed = await this.registration_transition(post_body,transtion_object)
                        if ( processed ) break;
                    }
                }
                use_ammelioration = this.registration_fail_amelioration
                break;
            }
            case 'forgot' : {
                if ( this.db ) {
                    let user = await this.db.fetch_user(post_body)
                    if ( user ) {
                        if ( this.business && this.business.process('forgot',post_body) ) {
                            let update_password = await this.hash_pass(post_body.password)
                            user.updates = { 'pass' : update_password }
                            this.db.update_user(user)
                            this.ok_forgetfulness(true,transtion_object)
                        } else {
                            this.ok_forgetfulness(false,transtion_object)
                        }
                    } else {
                        use_ammelioration = this.login_amelioration
                    }
                }
                break;
            }
            default : {
                break;
            }
        }

        //
        if ( use_ammelioration ) {
            transtion_object.amelioritive_action = use_ammelioration
            return(transtion_object.to_object(['amelioritive_action']))
        }
        return(transtion_object.to_object())
    }


    /**
     * This method responds to a client request by upd
     * 
     * `finalize_transition` in some applications use this method. The invokation of `finalize_transition` is typically during a 
     * secondary action and part of an active session. The token, from the stached transition object is expected in the client 
     * request data.
     * 
     * The token will map, via DB query on the key-value DB to a value to be used in the query for the user object 
     * in the user table of the DB. If the keyed value and the user can be found, then the password introduced by the 
     * client request's post body will be hashed and stored if the user object has the field `updates` set to an object 
     * containing the desired password. The update password and the post body password must check. 
     * 
     * If the passwords check out, then the user password can be updated and stored in the DB for future transactions.
     * 
     * The value this returns may be passed onto a business process, if the application is so condigured.
     * 
     * @param {object} post_body 
     * @returns {object} - the value `keyed_val` retrieved form the DB and that is mapped to the `token`.
     */
    async update_user_password(post_body) {
        if ( this.db ) {
            try {
                if ( post_body.token ) {
                    let keyed_val = await this.db.get_key_value(post_body.token)  // information that links the user to the reset password
                    if ( keyed_val ) {
                        let query = {}
                        query[post_body._t_u_key] = keyed_val
                        let user = await this.db.fetch_user(query)
                        if ( user ) {
                            if ( user.password && post_body.password ) {
                                let post_pass = await this.hash_pass(post_body.password)
                                let updates = user.updates
                                if ( updates ) {
                                    if ( await this.password_check(updates.pass,post_pass) ) {
                                        user.password = updates.pass
                                        delete user.updates.pass
                                        if ( Object.keys(user.updates).length == 0 ) delete user.updates
                                        this.db.update_user(user)
                                        return(keyed_val)
                                    }
                                }
                            }
                        }
                    }
                }
            } catch(e) {
                console.log(e)
            }
        }
        return(false)
    }


    /**
     * Set the `forgetfulness_proceed` for the applications that handle forgotten passwords.
     * 
     * @param {boolean} boolVal 
     * @param {object} transtion_object 
     */
    ok_forgetfulness(boolVal,transtion_object) {
        transtion_object.forgetfulness_proceed = boolVal
    }

    /**
     * Applications will want to override this method in order to work with cookies used by the authorization process.
     * 
     * @param {object} res - this is the HTTP request response object
     * @param {string} session_token 
     */
    app_set_user_cookie(res,session_token) {
        // application override
    }

    /**
     * Applications will want to override this method in order to work with cookies used by the authorization process.
     * 
     * @param {object} res - this is the HTTP request response object 
     */
    app_user_release_cookie(res) {/* application only */}

    /**
     * 
     * This method will start a user's session, enabling calls to asset delivery and to processess state transitions.
     * This method is called only after the session has been authorized and a session token has been created for it.
     * Also, this method only handles transition objects that have a `user_op` set to `login`.
     * 
     * Given the user object is still in the DB, the user `logged_in` field will be set to true.
     * And, this state change will be stored in the DB for reference by this and other processes.
     * 
     * Finally, the session for this user will be recorded in the session tables, and data created for the session will be released.
     * 
     * 
     * 
     * Only available in the general_auth which should only be used in processes that are processing users.
     * This is not in the auth_session_lite, which is used by processes checking user ownership and permissions,
     * but those processes do not initiate user sessions or offer processing for new entries....
     * 
     * if the user session state can be initialized, this returns the elements map required by the cached transition object.
     * 
     * @param {string} transition 
     * @param {string} session_token 
     * @param {object} transtionObj 
     * @param {object} res 
     * @returns {object|undefined} 
     */
    async initialize_session_state(transition,session_token,transtionObj,res) {
        if ( transition === 'user' ) {
            if ( transtionObj ) {
                if ( transtionObj.user_op === 'login' ) {       // finalize login....
                    let user_key_val = transtionObj.user_key
                    let key_key = transtionObj._t_u_key
                    let query = {}
                    query[key_key] = user_key_val       // find the user from the application db
                    let user = await this.db.fetch_user(query)
                    if ( user ) {       // if finding it, the log the user in (also the result of registration...)
                        user.logged_in = true
                        user.strategy = transtionObj.strategy       // foreign login may be a strategy... so is local...
                        // SEND USER TO DB as logged in...
                        this.db.update_user(user)   // update the user record with "logged_in" set to true..
                        //
                        if ( transtionObj._db_session_key ) {     // uuid --> and the sha255 hash  -- e.g. the ucwid itself
                            await this.addSession(transtionObj._db_session_key,session_token,transtionObj.token)  // goes to shared memory, key-value, etc.
                        }
                        //
                        if ( res ) this.app_set_user_cookie(res,session_token)   // leave it to the app to manage this construct, perhaps express, perhaps not
                        //
                        let elements = {}
                        let app_sess_data_access = this.sess_data_accessor()
                        elements[app_sess_data_access] = this.release_session_data[session_token]  // end of the transition...
                        delete this.release_session_data[session_token]
                        //
                        return elements        
                    }
                }
            }
        }
        return undefined
    }
}


module.exports.GeneralAuth = GeneralAuth
module.exports.SessionManager = SessionManager