UNPKG

backstrap-server

Version:

Backstrap is a lightweight framework for building APIs

1,485 lines (1,411 loc) 52 kB
const { Pool, Client } = require('pg'); const QueryStream = require('pg-query-stream'); const fs = require('fs'); const Stream = require('stream'); const path = require('path'); const rootDir = path.dirname(require.main.filename); var DataAccessExtension; try { DataAccessExtension = require(`${rootDir}/dataAccess_ext.js`); } catch(e) { console.error('INITIALIZATION ERROR -- dataAccess_ext.js'); throw(e); } class DataAccess { #pool = null; #multisource = null; dbs = null; constructor() { this.utilities = null; this.#pool = null; this.#multisource = false; this.dbs = {}; } init(dbConfig, u, s) { this.utilities = u; this.settings = s; try { // SINGLE DATABASE if(!Array.isArray(dbConfig.db)) { // ARE WE CONFIGURED FOR SSL let sslDesc = false; if(dbConfig.db.ssl === true) sslDesc = true; if(dbConfig.db.ssl != null && (dbConfig.db.ssl.ca || dbConfig.db.ssl.key || dbConfig.db.ssl.cert)) { sslDesc = { rejectUnauthorized: true, ca: dbConfig.db.ssl.ca == null ? null : fs.readFileSync(`${['/', '\\'].includes(dbConfig.db.ssl.ca.substring(0,1)) ? '' : rootDir + '/'}${dbConfig.db.ssl.ca}`), key: dbConfig.db.ssl.key == null ? null : fs.readFileSync(`${['/', '\\'].includes(dbConfig.db.ssl.key.substring(0,1)) ? '' : rootDir + '/'}${dbConfig.db.ssl.key}`), cert: dbConfig.db.ssl.cert == null ? null : fs.readFileSync(`${['/', '\\'].includes(dbConfig.db.ssl.cert.substring(0,1)) ? '' : rootDir + '/'}${dbConfig.db.ssl.cert}`) }; } // CONNECT TO THE DB this.#pool = new Pool({ user: dbConfig.db.user, host: dbConfig.db.host, database: dbConfig.db.name, password: dbConfig.db.pass, port: dbConfig.db.port, max: dbConfig.db.max_connections || 1000, ssl: sslDesc }); // SET AN ENTRY IN THE ARRAY OF DATABASE-SPECIFIC FUNCTIONS // THIS ALLOWS dataAccess.dbs['default'].getDbConnection() ETC // TO KEEP THINGS STANDARD BETWEEN SINGLE AND MULTI DATASOURCE // CONFIGURATIONS this.dbs['default'] = { getDbConnection: () => this.getDbConnection(), resolveDbConnection: (conn) => this.resolveDbConnection(conn), startTransaction: () => this.startTransaction(), runSql: (qry, params, conn, isStreaming) => this.runSql(qry, params, conn, isStreaming), ExecutePostgresQuery: (qry, params, conn, isStreaming) => this.runSql(qry, params, conn, isStreaming) } } // MULTIPLE DATABASES else { this.#multisource = true; this.#pool = {} let firstOne = true; for(let db of dbConfig.db) { // ARE WE CONFIGURED FOR SSL let sslDesc = false; if(db.ssl === true) sslDesc = true; if(db.ssl != null && (db.ssl.ca || db.ssl.key || db.ssl.cert)) { sslDesc = { rejectUnauthorized: true, ca: db.ssl.ca == null ? null : fs.readFileSync(`${['/', '\\'].includes(db.ssl.ca.substring(0,1)) ? '' : rootDir + '/'}${db.ssl.ca}`), key: db.ssl.key == null ? null : fs.readFileSync(`${['/', '\\'].includes(db.ssl.key.substring(0,1)) ? '' : rootDir + '/'}${db.ssl.key}`), cert: db.ssl.cert == null ? null : fs.readFileSync(`${['/', '\\'].includes(db.ssl.cert.substring(0,1)) ? '' : rootDir + '/'}${db.ssl.cert}`) }; } let dbName = firstOne === true ? 'default' : db.nickname || db.name; try { // CONNECT TO THE DB this.#pool[dbName] = new Pool({ user: db.user, host: db.host, database: db.name, password: db.pass, port: db.port, max: db.max_connections || 1000, ssl: sslDesc }); // SET AN ENTRY IN THE ARRAY OF DATABASE-SPECIFIC FUNCTIONS this.dbs[dbName] = { getDbConnection: () => this.getDbConnection(dbName), resolveDbConnection: (conn) => this.resolveDbConnection(conn, dbName), startTransaction: () => this.startTransaction(dbName), runSql: (qry, params, conn, isStreaming) => this.runSql(qry, params, conn, isStreaming, dbName), ExecutePostgresQuery: (qry, params, conn, isStreaming) => this.runSql(qry, params, conn, isStreaming, dbName) } } catch(dbConErr) { if(firstOne === true) { console.error(err); throw('Database Connection Failed'); } else { console.error(`Connection to non-primary database ${dbName} failed`); console.error(err); } } if(firstOne === true) firstOne = false; } } } catch(err) { console.error(err); throw('Database Connection Failed'); } this.extension = new DataAccessExtension(this); // IF THERE IS A SERVICES DIRECTORY SPECIFIED IN Settings.json // RUN THROUGH IT AND INSTANTIATE EACH SERVICE FILE let serviceDir = this.settings.data_service_directory; if(serviceDir != null) { serviceDir.replace(/^\.\//, ''); serviceDir.replace(/^\//, ''); let services = fs.readdirSync(serviceDir); services.forEach((serviceFile) => { // DON'T OVERWRITE dataAccess.extension OR dataAccess.dbs if(!['extension', 'dbs'].includes(serviceFile.toLowerCase())) { let fileNoExt = serviceFile.replace('.js', ''); try { let Service = require(`${rootDir}/${serviceDir}/${serviceFile}`); this[fileNoExt] = new Service(this, this.utilities); } catch(e) { let err = new Error(`Error starting services file: ${rootDir}/${serviceDir}/${serviceFile}`); throw err; } } }); } } async CheckForDatabase(db_name) { return new Promise((resolve, reject) => { var qry_params = []; var qry = " IF EXISTS (SELECT 1 FROM pg_database WHERE datname = '" + db_name + "') THEN" + " SELECT 'true';" + " ELSE" + " SELECT 'false';" + " END IF"; ExecutePostgresQuery(qry, qry_params, null) .then((connection) => { resolve(connection.results[0]); }) .catch((err) => { resolve(false); }) }); } async CreateDatabase(db_name) { return new Promise((resolve, reject) => { var qryString = 'CREATE DATABASE ' + db_name; var qryParams = []; this.ExecutePostgresQuery(qryString, qryParams, null) .then(res => { resolve(res); }) .catch(err => { resolve(err); }) }); } // START A CONNECTION TO THE DATABASE TO USE FUNCTIONS async getDbConnection(db) { return new Promise((resolve, reject) => { if(this.#multisource !== true) { this.#pool.connect((err, client, done) => { if (!err) { resolve({ 'client': client, 'release': done, 'transactional': false, 'results': [], isReleased: false }); } else { var errorObj = new ErrorObj(500, 'da9004', __filename, 'getDbConnection', 'error creating connection to postgres', 'Database error', err ); reject(errorObj); } }); } else { let thisDB = 'default' if(db != null) thisDB = db; this.#pool[thisDB].connect((err, client, done) => { if (!err) { resolve({ 'client': client, 'release': done, 'transactional': false, 'results': [], isReleased: false }); } else { var errorObj = new ErrorObj(500, 'da9004', __filename, 'getDbConnection', 'error creating connection to postgres', 'Database error', err ); reject(errorObj); } }); } }); } // CLOSE A CONNECTION TO THE DATABASE AFTER USING FUNCTIONS async closeDbConnection(connection) { return new Promise((resolve, reject) => { if(connection != null && !connection.isReleased) { try { connection.release(); connection.isReleased = true; resolve(true); } catch (err) { var errorObj = new ErrorObj(500, 'da10001', __filename, 'closeDbConnection', 'error closing postgres connection', 'Database error', err ); reject(errorObj); } } else { resolve(true); } }); } // GET A CONNECTION TO THE DATABASE AND START A TRANSACTION async startTransaction(db) { return new Promise((resolve, reject) => { this.getDbConnection(db) .then((connection) => { //SET TRANSACTIONAL connection['transactional'] = true; connection.client.query('BEGIN', (err) => { if (err) { var errorObj = new ErrorObj(500, 'da0005', __filename, 'startTransaction', 'error querying postgres', 'Database error', err ); reject(errorObj); } else { resolve(connection); } }); }) .catch((err) => { var errorObj = new ErrorObj(500, 'da9005', __filename, 'startTransaction', 'error creating postgres transaction connection', 'Database error', err ); reject(errorObj); }); }); } // COMMIT A TRANSACTION AND CLOSE THE DATABASE CONNECTION async commitTransaction(connection) { return new Promise((resolve, reject) => { connection.client.query('COMMIT', (err) => { if (err) { var errorObj = new ErrorObj(500, 'da0006', __filename, 'commitTransaction', 'error committing transaction in postgres', 'Database error', err ); this.releaseConnection(connection); reject(errorObj); } else { delete connection.transactional; try { connection.release(); } catch(e) { console.log('Problem releasing connection to db:'); console.log(e); } connection.isReleased = true; resolve(connection.results); } }); }); } // ROLLBACK A TRANSACTION AND CLOSE THE DATABASE CONNECTION async rollbackTransaction(connection) { return new Promise((resolve, reject) => { if(connection != null && !connection.isReleased) { if(connection.transactional) { connection.client.query('ROLLBACK', (err) => { if (err) { // THERE WAS AN ERROR ROLLING BACK TRY TO JUST RELEASE THE CONNECTION if(connection != null) { try { connection.release(); } catch(e) { console.log('Problem releasing connection to db:'); console.log(e); } connection.isReleased = true; } var errorObj = new ErrorObj(500, 'da0007', __filename, 'ExecutePostgresQuery', 'error rolling back transaction in postgres', 'Database error', err ); // PANIC....ROLLBACK FAILED...I GUESS JUST PRINT A MESSAGE FOR NOW // ...UNTIL WE GET A BETTER PLAN console.log('***************************************************'); console.log('\t\tROLLBACK ERROR') console.log('***************************************************'); console.log(errorObj); console.log('***************************************************'); reject(errorObj); } else { try { connection.release(); } catch(e) { console.log('Problem releasing connection to db:'); console.log(e); } connection.isReleased = true; resolve({ 'rollback_results': 'success' }); } }); } // THIS ISN'T ACTUALLY A TRANSACTION. IT'S JUST A CHAIN OF CALLS // ON A SINGLE CONNECTION. CLOSE THE CONNECTION, BUT DON'T WORRY // ABOUT ROLLING BACK. else { try { connection.release(); } catch(e) { console.log('Problem releasing connection to db:'); console.log(e); } connection.isReleased = true; resolve(); } } else { resolve(); } }); } // THIS FUNCTION IS USED SO ONE FUNCTION CAN RESOLVE THE CURRENT CONNECTION STATE AND RETURN A CONNECTION async resolveDbConnection(connection, db) { return new Promise((resolve, reject) => { if(connection == null) { this.getDbConnection(db) .then((db_connection) => { resolve(db_connection); }) .catch((err) => { var errorObj = new ErrorObj(500, 'da0500', __filename, 'resolveDbConnection', 'error creating a connection to postgres', 'Database error', err ); reject(errorObj); }) } else { resolve(connection); } }); } // RELEASES A CONNECTION (IF YOU NEED TO DO THAT MANUALLY) async releaseConnection(connection) { return new Promise((resolve, reject) => { if(connection != null && !connection.isReleased) { if(connection.transactional) { this.rollbackTransaction(connection) .then((rollback_res) => { delete connection.transactional; resolve(); }) .catch((rollback_err) => { try { connection.release(); } catch(e) { console.log('Problem releasing connection to db:'); console.log(e); } connection.isReleased = true; resolve(); }); } else { delete connection.transactional; try { connection.release(); } catch(e) { console.log('Problem releasing connection to db:'); console.log(e); } connection.isReleased = true; resolve(); } } else { resolve(); } }); } // ================================================================================ //THIS FUNCTION GLOBALIZES ALL QUERIES (SELECT) AND NON QUERIES (INSERT UPDATE DELETE ETC) //CONDITIONALLY CREATES AND DESTROYS CONNECTIONS DEPENDING IF THEY ARE TRANSACTIONAL OR NOT async ExecutePostgresQuery(query, params, connection, isStreaming, db) { return new Promise((resolve, reject) => { var pg_query = query; //THE QUERY CONFIG OBJECT DOES NOT WORK IF THERE IS AN EMPTY ARRAY OF PARAMS if (params != null && params.length > 0) { pg_query = { text: query, values: params, } } var that = this; this.resolveDbConnection(connection, db) .then((db_connection) => { // PERFORM THE QUERY if(!isStreaming) { db_connection.client.query(pg_query) .then((res) => { db_connection.results = res.rows; // IF THE ARG connection PASSED INTO THE FUNCTION IS null/undefined // THIS IS A ONE-OFF AND WE MUST SHUT DOWN THE CONNECTION WE MADE // BEFORE RETURNING THE RESULTS if(connection == null) { that.releaseConnection(db_connection) .then(() => { resolve(db_connection); }); } // OTHERWISE THIS IS ONE CALL IN A CHAIN ON A SINGLE CONNECTION // SO WE SHOULD PASS BACK THE CONNECTION WITH RESULTS else { resolve(db_connection); } }) .catch((qry_err) => { // IF THE ARG connection PASSED INTO THE FUNCTION IS null/undefined // THIS IS A ONE-OFF AND WE MUST SHUT DOWN THE CONNECTION WE MADE // AND FAIL OUT if(connection == null) { try { db_connection.release(); } catch(e) { console.log('Problem releasing connection to db:'); console.log(e); } db_connection.isReleased = true; var errorObj = new ErrorObj(500, 'da0501', __filename, 'ExecutePostgresQuery', 'error querying postgres', 'Database error', qry_err ); reject(errorObj); } // OTHERWISE, THIS IS ONE CALL IN A CHAIN ON A SINGLE CONNECTION else { // IF THIS IS PART OF A TRANSACTIONAL SEQUENCE, WE NEED TO ROLL BACK // AND FAIL OUT if(db_connection.transactional) { this.rollbackTransaction(db_connection) .then((rollback_res) => { var errorObj = new ErrorObj(500, 'da0502', __filename, 'ExecutePostgresQuery', 'error querying postgres--transaction rolled back', 'Database error', qry_err ); reject(errorObj); }) .catch((rollback_err) => { reject(rollback_err.AddToError(__filename, 'ExecutePostgresQuery')); }); } // OTHERWISE, JUST RELEASE THE CONNECTION AND FAIL OUT else { try { db_connection.release(); } catch(e) { console.log('Problem releasing connection to db:'); console.log(e); } db_connection.isReleased = true; var errorObj = new ErrorObj(500, 'da0504', __filename, 'ExecutePostgresQuery', 'error querying postgres', 'Database error', qry_err ); reject(errorObj); } } }); } else { // STREAMING RESULTS let streamQry; if(typeof(pg_query) === 'object') { streamQry = new QueryStream(pg_query.text, pg_query.values) } else { streamQry = new QueryStream(pg_query); } let stream = db_connection.client.query(streamQry); let outStream = new Stream.Readable(); outStream._read = () => {}; stream.on('data', (chunk) => { outStream.push(JSON.stringify(chunk)); }); stream.on('end', () => { outStream.push(null); try { db_connection.release(); } catch(e) { console.log('Problem releasing connection to db:'); console.log(e); } }); stream.on('error', (streamErr) => { if(db_connection.transactional === true) { this.rollbackTransaction(db_connection) .finally(() => { outStream.destroy(streamErr); }); } else { try { db_connection.release(); } catch(e) { console.log('Problem releasing connection to db:'); console.log(e); } outStream.destroy(streamErr); } }); db_connection.results = outStream; resolve(db_connection); } }) .catch((err) => { var errorObj = new ErrorObj(500, 'da0505', __filename, 'ExecutePostgresQuery', 'error connecting to postgres', 'Database error', err ); reject(errorObj); }); }); } // RUN ARBITRARY SQL STATEMENTS async runSql(sqlStatement, params, connection, isStreaming, db) { return new Promise((resolve, reject) => { this.ExecutePostgresQuery(sqlStatement, params, connection, isStreaming, db) .then((connection) => { resolve(connection.results); }) .catch((err) => { if(err && typeof(err.AddToError) === 'function') { reject(err.AddToError(__filename, 'runSql')); } else { let errorObj = new ErrorObj(500, 'da0090', __filename, 'runSql', 'psql error', 'There was a problem with your request.', err); reject(errorObj); } }); }); } async GetDeadSessions(timeOut, markAsEnded) { return new Promise((resolve, reject) => { var minutes = "'" + timeOut + " minutes'"; var qry = "select * from bs3_sessions where last_touch < (NOW() - INTERVAL " + minutes + ")"; var qry_params = []; if(markAsEnded) qry = "UPDATE bs3_sessions SET ended_at = NOW() WHERE last_touch < (NOW() - INTERVAL " + minutes + ") RETURNING *"; this.ExecutePostgresQuery(qry, qry_params, null) .then((connection) => { resolve(connection.results); }) .catch((err) => { reject(err.AddToError(__filename, 'GetDeadSessions')); }); }); } async DeleteSessions(dsIds) { return new Promise((resolve, reject) => { let qry = "DELETE FROM bs3_sessions WHERE id = ANY($1)"; this.ExecutePostgresQuery(qry, [dsIds]) .then((delRes) => { resolve(); }) .catch((err) => { reject(err); }); }); } async UpdateLastTouch(sid) { return new Promise((resolve) => { let sql = 'UPDATE bs3_sessions SET last_touch = NOW() WHERE id = $1 RETURNING id'; let params = [sid]; this.runSql(sql, params) .then((updRes) => { resolve({success:true}); }) .catch((updErr) => { console.error(updErr); // DON'T REJECT. IF THIS FAILS, WE STILL WANT TO RETURN SUCCESS // IF THE CALL WAS SUCCESSFUL resolve({success:false, message:updErr}); }); }) } async findUser(id, username, email, connection) { return new Promise((resolve, reject) => { if(!id) id = null; if(!username) username = null; if(!email) email = null; if(id || username || email) { let sql = "SELECT * FROM bs3_users WHERE (id = $1 OR LOWER(username) = LOWER($2) OR LOWER(email) = LOWER($3)) AND deleted_at IS NULL"; let params = [id, username, email]; this.ExecutePostgresQuery(sql, params, connection) .then((userRes) => { resolve(userRes.results) }) .catch((err) => { if(err && typeof(err.AddToError) === 'function') { reject(err.AddToError(__filename, 'find user')); } else { let errorObj = new ErrorObj(404, 'da0203', __filename, 'findUser', 'error finding user', 'There was a problem with your request', err ); reject(errorObj); } }); } else { resolve(null); } }); } async getAllUsers(connection) { return new Promise((resolve, reject) => { let sql = "SELECT * FROM bs3_users WHERE deleted_at IS NULL"; this.runSql(sql,[],connection) .then((userRes) => { resolve(userRes); }) .catch((allUsrErr) => { let errorObj = new ErrorObj(500, 'da0204', __filename, 'getAllUsers', 'error gettomg users', 'There was a problem with your request', allUsrErr ); reject(errorObj); }) }); } async GenerateForgotPasswordToken(email, username) { return new Promise((resolve, reject) => { this.findUser(null, username, email) .then((userObj) => { if (userObj.locked) { reject(new ErrorObj( 403, 'a2006', __filename, 'forgotPassword', 'bsuser is locked', 'Unauthorized' )); return; } else { return Promise.all([userObj, this.utilities.getHash(null, null, 48)]); } }) .then(([userObj, tkn]) => { if (userObj.forgot_password_tokens === undefined || userObj.forgot_password_tokens === null) { userObj.forgot_password_tokens = [tkn]; } else { userObj.forgot_password_tokens.push(tkn); } return Promise.all([tkn, this.updateCredentialsForUser(userObj.id, null, null, userObj.forgot_password_tokens)]); }) .then(([tkn]) => { resolve(tkn); }) .catch((err) => { reject(err); }); }); } async getActiveTokens() { return new Promise((resolve, reject) => { let sql = "SELECT token FROM bs3_sessions WHERE ended_at IS NULL"; this.runSql(sql, []) .then((tokenRes) => { resolve(tokenRes.map(tknObj => tknObj.token)); }) .catch((err) => { reject(err.AddToError(__filename, 'getActiveTokens')); }); }); } async startSession(token, userId, clientInfo, isAnonymous, connection) { return new Promise((resolve, reject) => { if(isAnonymous == null) isAnonymous = false; let sql = "INSERT INTO bs3_sessions(token, user_id, client_info, anonymous, created_at, last_touch) VALUES($1, $2, $3, $4, NOW(), NOW()) RETURNING *"; let params = [token, userId, clientInfo, isAnonymous] this.runSql(sql, params, connection) .then((sessRes) => { resolve(sessRes[0]); }) .catch((err) => { reject(err.AddToError(__filename, 'startSession', 'Problem starting up a new session')); }); }); } async getUserById(id, connection) { return new Promise((resolve, reject) => { if(id) { var qry = "SELECT * FROM bs3_users WHERE id = $1 AND deleted_at IS NULL"; var qry_params = [id]; this.ExecutePostgresQuery(qry, qry_params, connection) .then((connection) => { if (connection.results.length === 0) { var errorObj = new ErrorObj(404, 'da0160', __filename, 'getUserById', 'no user found', 'Cannot find user.', null ); reject(errorObj); } else if (connection.results.length === 1) { resolve(connection.results[0]); } else { console.log('found multiple users'); var errorObj = new ErrorObj(500, 'da0161', __filename, 'getUserById', '', 'Found multiple users with that id.', null ); reject(errorObj); } }) .catch((err) => { try { reject(err.AddToError(__filename, 'getUserById')); } catch(e) {console.log(e)} }); } else { resolve(null); } }); } async getUserByUserName(username, connection) { return new Promise((resolve, reject) => { if(username) { var qry = "SELECT * FROM bs3_users WHERE LOWER(username) = LOWER($1) AND deleted_at IS NULL"; var qry_params = [username]; this.ExecutePostgresQuery(qry, qry_params, connection) .then((connection) => { if (connection.results.length === 0) { var errorObj = new ErrorObj(404, 'da0162', __filename, 'getUserByUserName', 'no user found', 'Cannot find user.', null ); reject(errorObj); } else if (connection.results.length === 1) { resolve(connection.results[0]); } else { console.log('found multiple users'); var errorObj = new ErrorObj(500, 'da0163', __filename, 'getUserByUserName', '', 'Found multiple users with that user name.', null ); reject(errorObj); } }) .catch((err) => { try { reject(err.AddToError(__filename, 'getUserByUserName')); } catch(e) {console.log(e)} }); } else { resolve(null); } }); } async getUserByEmail(email, connection) { return new Promise((resolve, reject) => { if(email) { var qry = "SELECT * FROM bs3_users WHERE LOWER(email) = LOWER($1) AND deleted_at IS NULL"; var qry_params = [email]; this.ExecutePostgresQuery(qry, qry_params, connection) .then((connection) => { if (connection.results.length === 0) { var errorObj = new ErrorObj(404, 'da0164', __filename, 'getUserByEmail', 'no user found', 'Cannot find user.', null ); reject(errorObj); } else if (connection.results.length === 1) { resolve(connection.results[0]); } else { var errorObj = new ErrorObj(500, 'da0165', __filename, 'getUserByEmail', '', 'Found multiple users with that user name.', null ); reject(errorObj); } }) .catch((err) => { try { reject(err.AddToError(__filename, 'getUserByEmail')); } catch(e) {console.log(e)} }); } else { resolve(null); } }); } async getUserByClientId(cid, includeCreds, connection) { return new Promise((resolve, reject) => { if(cid) { var qry = "SELECT usr.*"; if(includeCreds) qry += ", creds.salt, creds.client_secret"; qry += " FROM bs3_users usr JOIN bs3_credentials creds ON creds.user_id = usr.id WHERE creds.client_id = $1 AND creds.deleted_at IS NULL AND usr.deleted_at IS NULL"; var qry_params = [cid]; this.ExecutePostgresQuery(qry, qry_params, connection) .then((connection) => { if (connection.results.length === 0) { var errorObj = new ErrorObj(404, 'da0166', __filename, 'getUserByClientId', 'no user found', 'Cannot find user.', null ); reject(errorObj); } else if (connection.results.length === 1) { resolve(connection.results[0]); } else { let errorObj = new ErrorObj(500, 'da0167', __filename, 'getUserByClientId', '', 'Found multiple users with that client id.', null ); reject(errorObj); } }) .catch((err) => { try { reject(err.AddToError(__filename, 'getUserByClientId')); } catch(e) {console.log(e)} }); } else { resolve(null); } }); } async getUserByExternalIdentityId(exid, connection) { return new Promise((resolve, reject) => { let sql = "SELECT * FROM bs3_users WHERE external_id = $1 AND deleted_at IS NULL"; let params = [exid]; this.runSql(sql, params, connection) .then((usersRes) => { if (usersRes.length === 0) { let errorObj = new ErrorObj(404, 'da3166', __filename, 'getUserByExternalIdentityId', 'no user found', 'Cannot find user.', null ); reject(errorObj); } else if (usersRes.length === 1) { resolve(usersRes[0]); } else { let errorObj = new ErrorObj(500, 'da3167', __filename, 'getUserByExternalIdentityId', '', 'Found multiple users with that external id.', null ); reject(errorObj); } }) .catch((err) => { var errorObj = new ErrorObj(500, 'da3170', __filename, 'getUserByExternalIdentityId', '', 'Found multiple users with that client id.', null ); reject(errorObj); }); }); } async getUserByForgotPasswordToken(fptkn, connection) { return new Promise((resolve, reject) => { if(fptkn) { var qry = "SELECT DISTINCT bu.* FROM bs3_users bu INNER JOIN bs3_credentials bc ON bc.user_id = bu.id WHERE bc.forgot_password ? $1 AND bc.deleted_at IS NULL"; var qry_params = [fptkn]; this.ExecutePostgresQuery(qry, qry_params, connection) .then((connection) => { if (connection.results.length === 0) { var errorObj = new ErrorObj(404, 'da0164', __filename, 'getUserByForgotPasswordToken', 'no user found', 'Cannot find user.', null ); reject(errorObj); } else if (connection.results.length === 1) { resolve(connection.results[0]); } else { var errorObj = new ErrorObj(500, 'da0165', __filename, 'getUserByForgotPasswordToken', 'forgot password token collision', 'Found multiple users with that token.', null ); reject(errorObj); } }) .catch((err) => { try { reject(err.AddToError(__filename, 'getUserByForgotPasswordToken')); } catch(e) {console.log(e)} }); } else { resolve(null); } }); } async deleteUser(uid, connection) { return new Promise((resolve, reject) => { let sql = "UPDATE bs3_users SET deleted_at = NOW() WHERE id = $1 RETURNING id"; let params = [uid]; this.runSql(sql, params, connection) .then((delRes) => { if(delRes.length > 0) { resolve({success: true}); } else { reject({success:false, message:'problem deleting user'}); } }) .catch((delErr) => { let errorObj = new ErrorObj(500, 'da0170', __filename, 'deleteUser', 'problem deleting user', 'There was a problem with your request.', delErr ); reject(errorObj); }) }); } async createUser(userObj) { return new Promise((resolve, reject) => { this.startTransaction() .then((dbHandle) => { let sql = `INSERT INTO bs3_users(account_type, username, email, roles, external_id, locked, created_at) VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING *`; let params = [userObj.account_type, userObj.username, userObj.email, JSON.stringify(userObj.roles), userObj.external_id, false, new Date().toISOString()]; return Promise.all([dbHandle, this.runSql(sql, params, dbHandle)]); }) .then(([dbHandle, userRes]) => { let sql = `INSERT INTO bs3_credentials(salt, password, client_id, client_secret, created_at, user_id) VALUES($1, $2, $3, $4, $5, $6)`; let params = [userObj.salt, userObj.password || null, userObj.client_id || null, userObj.client_secret || null, new Date().toISOString(), userRes[0].id]; let outUsr = userRes[0]; if(userObj.client_id) outUsr['client_id'] = userObj.client_id; if(userObj.client_id) outUsr['client_secret'] = userObj.client_secret; return Promise.all([dbHandle, outUsr, this.runSql(sql, params, dbHandle)]); }) .then(([dbHandle, usr, credRes]) => { return Promise.all([usr, this.commitTransaction(dbHandle)]); }) .then(([usr, commitRes]) => { resolve(usr); }) .catch((err) => { if(err && typeof(err.AddToError) === 'function') { reject(err.AddToError(__filename, 'createUser')); } else { let errorObj = new ErrorObj(500, 'da3000', __filename, 'createUser', 'problem creating user', 'There was a problem creating this user', err); reject(errorObj); } }) }); } async updateJsonbField(tableName, fieldname, updateObj, whereClause) { return new Promise((resolve, reject) => { let sql = `UPDATE ${tableName} SET ${fieldname} = ${fieldname} || $1`; if(whereClause) { sql += ` WHERE ${whereClause}`; } sql += ` RETURNING *`; let params = [JSON.stringify(updateObj)]; this.runSql(sql, params) .then((updRes) => { resolve(updRes); }) .catch((err) => { reject(err.AddToError(__filename, 'updateJsonbField')); }) }); } async saveApiCredentials(clientId, salt, hashedSecret, uid) { return new Promise((resolve, reject) => { if(clientId && salt && hashedSecret && uid) { let sql = 'INSERT INTO bs3_credentials(client_id, salt, client_secret, user_id) VALUES($1, $2, $3, $4) RETURNING *'; let params = [clientId, salt, hashedSecret, uid]; this.runSql(sql, params) .then((credRes) => { if(credRes.length > 0) { resolve(credRes[0]); } else { let errorObj = new ErrorObj(500, 'da3002', __filename, 'saveApiCredentials', 'problem saving to db', 'There was a problem saving credentials', null); reject(errorObj); } }) .catch((err) => { if(err && typeof(err.AddToError) === 'function') { reject(err.AddToError(__filename, 'saveApiCredentials')); } else { let errorObj = new ErrorObj(500, 'da3001', __filename, 'saveApiCredentials', 'problem saving to db', 'There was a problem saving credentials', err); reject(errorObj); } }) } else { let errorObj = new ErrorObj(500, 'da3000', __filename, 'saveApiCredentials', 'missing args', 'There was a problem saving credentials'); reject(errorObj); } }); } async updateApiCredentials(clientId, salt, hashedSecret) { return new Promise((resolve, reject) => { if(clientId && salt && hashedSecret && uid) { let sql = 'UPDATE bs3_credentials SET salt = $2, client_secret = $3 WHERE client_id = $1 RETURNING id'; let params = [clientId, salt, hashedSecret]; this.runSql(sql, params) .then((credRes) => { resolve(credRes); }) .catch((err) => { if(err && typeof(err.AddToError) === 'function') { reject(err.AddToError(__filename, 'updateApiCredentials')); } else { let errorObj = new ErrorObj(500, 'da3101', __filename, 'updateApiCredentials', 'problem saving to db', 'There was a problem saving credentials', err); reject(errorObj); } }) } else { let errorObj = new ErrorObj(500, 'da3000', __filename, 'saveApiCredentials', 'missing args', 'There was a problem saving credentials'); reject(errorObj); } }); } async getSession(sid, tkn) { return new Promise((resolve, reject) => { let sql = "SELECT * FROM bs3_sessions WHERE"; let params = []; if(sid) { sql += " id = $1"; params.push(sid); } else if(tkn) { sql += " token = $1"; params.push(tkn); } this.runSql(sql, params) .then((sessRes) => { if(sessRes.length === 1) { resolve(sessRes[0]); } else if(sessRes.length === 0) { resolve(null); } else { let errorObj = new ErrorObj(500, 'da0080', __filename, 'getSession', 'multiple sessions found', 'There was a problem with your request', null); reject(errorObj); } }) .catch((err) => { let errorObj = new ErrorObj(500, 'da0081', __filename, 'getSession', 'db error', 'There was a problem with your request', err); reject(errorObj); }); }); } async getUserBySession(sid, tkn) { return new Promise((resolve, reject) => { let sql = "SELECT bu.* FROM bs3_users bu JOIN bs3_sessions bs ON bu.id = bs.user_id WHERE bs.ended_at IS NULL AND" let params = []; if(sid) { sql += " bs.id = $1"; params.push(sid); } else if(tkn) { " bs.token = $1"; params.push(tkn); } this.runSql(sql, params) .then((sessRes) => { if(sessRes.length === 1) { resolve(sessRes[0]); } else if(sessRes.length === 0) { resolve(null); } else { let errorObj = new ErrorObj(500, 'da0082', __filename, 'getUserBySession', 'multiple sessions found', 'There was a problem with your request', null); reject(errorObj); } }) .catch((err) => { let errorObj = new ErrorObj(500, 'da0083', __filename, 'getUserBySession', 'db error', 'There was a problem with your request', err); reject(errorObj); }); }); } async attachUserToSession(uid, sid) { return new Promise((resolve, reject) => { let sql = "UPDATE bs3_sessions SET user_id = $1 WHERE id = $2"; this.runSql(sql, [uid, sid]) .then((updRes) => { resolve({success:true}); }) .catch((err) => { let errorObj = new ErrorObj(500, 'da0090', __filename, 'attachUserToSession', 'db error', 'There was a problem with your request', err); reject(errorObj); }); }); } async getCredentialsForUser(userId, connection) { return new Promise((resolve, reject) => { let sql = "SELECT * FROM bs3_credentials WHERE user_id = $1 AND deleted_at IS NULL"; let params = [userId]; this.runSql(sql, params, connection) .then((credRes) => { resolve(credRes); }) .catch((err) => { let errorObj = new ErrorObj(500, 'da0100', __filename, 'getCredentialsForUser', 'db error', 'There was a problem with your request', err); reject(errorObj); }); }); } async updateCredentialsForUser(userId, salt, password, forgotPasswordToken, connection) { return new Promise((resolve, reject) => { let params = [userId]; let sql = "UPDATE bs3_credentials SET modified_at = NOW()"; if(salt) { params.push(salt); sql += `, salt = $${params.length}`; } if(password) { params.push(password); sql += `, password = $${params.length}, forgot_password = '[]'::jsonb`; } else if(forgotPasswordToken) { sql += `, forgot_password = COALESCE(forgot_password, '[]'::JSONB) || '["${forgotPasswordToken}"]'::jsonb`; } sql += ` WHERE user_id = $1 AND deleted_at IS NULL AND password IS NOT NULL`; this.runSql(sql, params, connection) .then((updRes) => { resolve(updRes); }) .catch((err) => { let errorObj = new ErrorObj(500, 'da0110', __filename, 'updateCredentialsForUser', 'db error', 'There was a problem with your request', err); reject(errorObj); }); }); } async deleteCredentialsByClientId(clientId) { return new Promise((resolve, reject) => { let sql = `UPDATE bs3_credentials SET deleted_at = NOW() WHERE client_id = $1 RETURNING id`; this.runSql(sql, [clientId]) .then((res) => { if(res.length > 0) { resolve({success: true}); } else { let errorObj = new ErrorObj(500, 'da0120', __filename, 'deleteCredentialsByClientId', 'db error', 'There was a problem with your request', null); reject(errorObj); } }) .catch((err) => { let errorObj = new ErrorObj(500, 'da0121', __filename, 'deleteCredentialsByClientId', 'db error'