ltijs
Version:
Easily turn your web application into a LTI 1.3 Learning Tool.
393 lines (380 loc) • 12.9 kB
JavaScript
"use strict";
function _classPrivateFieldInitSpec(e, t, a) { _checkPrivateRedeclaration(e, t), t.set(e, a); }
function _checkPrivateRedeclaration(e, t) { if (t.has(e)) throw new TypeError("Cannot initialize the same private elements twice on an object"); }
function _classPrivateFieldGet(s, a) { return s.get(_assertClassBrand(s, a)); }
function _classPrivateFieldSet(s, a, r) { return s.set(_assertClassBrand(s, a), r), r; }
function _assertClassBrand(e, t, n) { if ("function" == typeof e ? e === t : e.has(t)) return arguments.length < 3 ? t : n; throw new TypeError("Private element is not present on this object"); }
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const crypto = require('crypto');
const provDatabaseDebug = require('debug')('provider:database');
/**
* @description Collection of static methods to manipulate the database.
*/
var _dbUrl = /*#__PURE__*/new WeakMap();
var _dbConnection = /*#__PURE__*/new WeakMap();
var _deploy = /*#__PURE__*/new WeakMap();
class Database {
/**
* @description Mongodb configuration setup
* @param {Object} database - Configuration object
*/
constructor(database) {
_classPrivateFieldInitSpec(this, _dbUrl, void 0);
_classPrivateFieldInitSpec(this, _dbConnection, {
useNewUrlParser: true,
connectTimeoutMS: 300000,
useUnifiedTopology: true
});
_classPrivateFieldInitSpec(this, _deploy, false);
if (!database || !database.url) throw new Error('MISSING_DATABASE_CONFIG');
// Configures database connection
_classPrivateFieldSet(_dbUrl, this, database.url);
if (database.debug) mongoose.set('debug', true);
_classPrivateFieldSet(_dbConnection, this, {
..._classPrivateFieldGet(_dbConnection, this),
...database.connection
});
// Creating database schemas
const idTokenSchema = new Schema({
iss: String,
user: String,
userInfo: JSON,
platformInfo: JSON,
clientId: String,
platformId: String,
deploymentId: String,
createdAt: {
type: Date,
expires: 3600 * 24,
default: Date.now
}
});
idTokenSchema.index({
iss: 1,
clientId: 1,
deploymentId: 1,
user: 1
});
const contextTokenSchema = new Schema({
contextId: String,
user: String,
roles: [String],
path: String,
targetLinkUri: String,
context: JSON,
resource: JSON,
custom: JSON,
launchPresentation: JSON,
messageType: String,
version: String,
deepLinkingSettings: JSON,
lis: JSON,
endpoint: JSON,
namesRoles: JSON,
createdAt: {
type: Date,
expires: 3600 * 24,
default: Date.now
}
});
contextTokenSchema.index({
contextId: 1,
user: 1
});
const platformSchema = new Schema({
platformUrl: String,
platformName: String,
clientId: String,
authEndpoint: String,
accesstokenEndpoint: String,
authorizationServer: String,
kid: String,
authConfig: {
method: String,
key: String
}
});
platformSchema.index({
platformUrl: 1
});
platformSchema.index({
kid: 1
}, {
unique: true
});
platformSchema.index({
platformUrl: 1,
clientId: 1
}, {
unique: true
});
const platformStatusSchema = new Schema({
id: String,
active: {
type: Boolean,
default: false
}
});
platformStatusSchema.index({
id: 1
}, {
unique: true
});
const keySchema = new Schema({
kid: String,
platformUrl: String,
clientId: String,
iv: String,
data: String
});
keySchema.index({
kid: 1
}, {
unique: true
});
const accessTokenSchema = new Schema({
platformUrl: String,
clientId: String,
scopes: String,
iv: String,
data: String,
createdAt: {
type: Date,
expires: 3600,
default: Date.now
}
});
accessTokenSchema.index({
platformUrl: 1,
clientId: 1,
scopes: 1
}, {
unique: true
});
const nonceSchema = new Schema({
nonce: String,
createdAt: {
type: Date,
expires: 10,
default: Date.now
}
});
nonceSchema.index({
nonce: 1
});
const stateSchema = new Schema({
state: String,
query: JSON,
createdAt: {
type: Date,
expires: 600,
default: Date.now
}
});
stateSchema.index({
state: 1
}, {
unique: true
});
try {
mongoose.model('idtoken', idTokenSchema);
mongoose.model('contexttoken', contextTokenSchema);
mongoose.model('platform', platformSchema);
mongoose.model('platformStatus', platformStatusSchema);
mongoose.model('privatekey', keySchema);
mongoose.model('publickey', keySchema);
mongoose.model('accesstoken', accessTokenSchema);
mongoose.model('nonce', nonceSchema);
mongoose.model('state', stateSchema);
} catch (err) {
provDatabaseDebug('Model already registered. Continuing');
}
this.db = mongoose.connection;
}
/**
* @description Opens connection to database
*/
async setup() {
this.db.on('connected', async () => {
provDatabaseDebug('Database connected');
});
this.db.once('open', async () => {
provDatabaseDebug('Database connection open');
});
this.db.on('error', async () => {
mongoose.disconnect();
});
this.db.on('reconnected', async () => {
provDatabaseDebug('Database reconnected');
});
this.db.on('disconnected', async () => {
provDatabaseDebug('Database disconnected');
provDatabaseDebug('Attempting to reconnect');
setTimeout(async () => {
if (this.db.readyState === 0) {
try {
await mongoose.connect(_classPrivateFieldGet(_dbUrl, this), _classPrivateFieldGet(_dbConnection, this));
} catch (err) {
provDatabaseDebug('Error in MongoDb connection: ' + err);
}
}
}, 1000);
});
if (this.db.readyState === 0) await mongoose.connect(_classPrivateFieldGet(_dbUrl, this), _classPrivateFieldGet(_dbConnection, this));
_classPrivateFieldSet(_deploy, this, true);
return true;
}
// Closes connection to the database
async Close() {
mongoose.connection.removeAllListeners();
await mongoose.connection.close();
_classPrivateFieldSet(_deploy, this, false);
return true;
}
/**
* @description Get item or entire database.
* @param {String} ENCRYPTIONKEY - Encryptionkey of the database, false if none
* @param {String} collection - The collection to be accessed inside the database.
* @param {Object} [query] - Query for the item you are looking for in the format {type: "type1"}.
*/
async Get(ENCRYPTIONKEY, collection, query) {
if (!_classPrivateFieldGet(_deploy, this)) throw new Error('PROVIDER_NOT_DEPLOYED');
if (!collection) throw new Error('MISSING_COLLECTION');
const Model = mongoose.model(collection);
const result = await Model.find(query).select('-__v -_id');
if (ENCRYPTIONKEY) {
for (const i in result) {
const temp = result[i];
result[i] = JSON.parse(await this.Decrypt(result[i].data, result[i].iv, ENCRYPTIONKEY));
if (temp.createdAt) {
const createdAt = Date.parse(temp.createdAt);
result[i].createdAt = createdAt;
}
}
}
if (result.length === 0) return false;
return result;
}
/**
* @description Insert item in database.
* @param {String} ENCRYPTIONKEY - Encryptionkey of the database, false if none.
* @param {String} collection - The collection to be accessed inside the database.
* @param {Object} item - The item Object you want to insert in the database.
* @param {Object} [index] - Key that should be used as index in case of Encrypted document.
*/
async Insert(ENCRYPTIONKEY, collection, item, index) {
if (!_classPrivateFieldGet(_deploy, this)) throw new Error('PROVIDER_NOT_DEPLOYED');
if (!collection || !item || ENCRYPTIONKEY && !index) throw new Error('MISSING_PARAMS');
const Model = mongoose.model(collection);
let newDocData = item;
if (ENCRYPTIONKEY) {
const encrypted = await this.Encrypt(JSON.stringify(item), ENCRYPTIONKEY);
newDocData = {
...index,
iv: encrypted.iv,
data: encrypted.data
};
}
const newDoc = new Model(newDocData);
await newDoc.save();
return true;
}
/**
* @description Replace item in database. Creates a new document if it does not exist.
* @param {String} ENCRYPTIONKEY - Encryptionkey of the database, false if none.
* @param {String} collection - The collection to be accessed inside the database.
* @param {Object} query - Query for the item you are looking for in the format {type: "type1"}.
* @param {Object} item - The item Object you want to insert in the database.
* @param {Object} [index] - Key that should be used as index in case of Encrypted document.
*/
async Replace(ENCRYPTIONKEY, collection, query, item, index) {
if (!_classPrivateFieldGet(_deploy, this)) throw new Error('PROVIDER_NOT_DEPLOYED');
if (!collection || !item || ENCRYPTIONKEY && !index) throw new Error('MISSING_PARAMS');
const Model = mongoose.model(collection);
let newDocData = item;
if (ENCRYPTIONKEY) {
const encrypted = await this.Encrypt(JSON.stringify(item), ENCRYPTIONKEY);
newDocData = {
...index,
iv: encrypted.iv,
data: encrypted.data
};
}
await Model.replaceOne(query, newDocData, {
upsert: true
});
return true;
}
/**
* @description Assign value to item in database
* @param {String} ENCRYPTIONKEY - Encryptionkey of the database, false if none.
* @param {String} collection - The collection to be accessed inside the database.
* @param {Object} query - The entry you want to modify in the format {type: "type1"}.
* @param {Object} modification - The modification you want to make in the format {type: "type2"}.
*/
async Modify(ENCRYPTIONKEY, collection, query, modification) {
if (!_classPrivateFieldGet(_deploy, this)) throw new Error('PROVIDER_NOT_DEPLOYED');
if (!collection || !query || !modification) throw new Error('MISSING_PARAMS');
const Model = mongoose.model(collection);
let newMod = modification;
if (ENCRYPTIONKEY) {
let result = await Model.findOne(query);
if (result) {
result = JSON.parse(await this.Decrypt(result.data, result.iv, ENCRYPTIONKEY));
result[Object.keys(modification)[0]] = Object.values(modification)[0];
newMod = await this.Encrypt(JSON.stringify(result), ENCRYPTIONKEY);
}
}
await Model.updateOne(query, newMod);
return true;
}
/**
* @description Delete item in database
* @param {String} collection - The collection to be accessed inside the database.
* @param {Object} query - The entry you want to delete in the format {type: "type1"}.
*/
async Delete(collection, query) {
if (!_classPrivateFieldGet(_deploy, this)) throw new Error('PROVIDER_NOT_DEPLOYED');
if (!collection || !query) throw new Error('MISSING_PARAMS');
const Model = mongoose.model(collection);
await Model.deleteMany(query);
return true;
}
/**
* @description Encrypts data.
* @param {String} data - Data to be encrypted
* @param {String} secret - Secret used in the encryption
*/
async Encrypt(data, secret) {
const hash = crypto.createHash('sha256');
hash.update(secret);
const key = hash.digest().slice(0, 32);
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv('aes-256-cbc', key, iv);
let encrypted = cipher.update(data);
encrypted = Buffer.concat([encrypted, cipher.final()]);
return {
iv: iv.toString('hex'),
data: encrypted.toString('hex')
};
}
/**
* @description Decrypts data.
* @param {String} data - Data to be decrypted
* @param {String} _iv - Encryption iv
* @param {String} secret - Secret used in the encryption
*/
async Decrypt(data, _iv, secret) {
const hash = crypto.createHash('sha256');
hash.update(secret);
const key = hash.digest().slice(0, 32);
const iv = Buffer.from(_iv, 'hex');
const encryptedText = Buffer.from(data, 'hex');
const decipher = crypto.createDecipheriv('aes-256-cbc', Buffer.from(key), iv);
let decrypted = decipher.update(encryptedText);
decrypted = Buffer.concat([decrypted, decipher.final()]);
return decrypted.toString();
}
}
module.exports = Database;