Skip to content

Commit

Permalink
Rewrote Database Module #30
Browse files Browse the repository at this point in the history
  • Loading branch information
Capevace committed May 28, 2021
1 parent 344f17e commit 21f0fb5
Show file tree
Hide file tree
Showing 6 changed files with 317 additions and 229 deletions.
111 changes: 111 additions & 0 deletions app/database/Database.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
/**
* @module @database
* @since 2.0.0
*/

const fs = require('fs/promises');
const autoBind = require('auto-bind');

const UsersAPI = require('@database/api/UsersAPI');

/**
* The Database object
* Currently the "Database" is pretty much a JSON key value store.
* However this will be updated when switching to SQLite
* https://github.com/Capevace/mission-control/issues/17
*/
class Database {
constructor(databasePath) {
/**
* The databases data. This will change and is temporary
* @protected
* @type {object}
*/
this.data = {};

/**
* The path to the database file
* @type {string}
*/
this.path = databasePath;

/**
* The available Database API's for safe db usage
* @typedef {DatabaseAPIList}
*/
this.api = {
/**
* The users API
* @type {UsersAPI}
*/
users: new UsersAPI(this)
};

autoBind(this);
}

/**
* Init connection to database etc.
*/
async init() {
this.data = require(this.path);
}

/**
* Save the data to disk.
* TODO: This is bad but will be fixed with the switch to SQLite,
* so it's a very temporary solution.
* @protected
*/
_saveData() {
fs.writeFileSync(
config.databasePath,
JSON.stringify(database, null, 2),
err => {
if (err) logger.error('Error writing database file', err);
}
);
}

/**
* Set a value for a key in the database.
*
* The function synchronously saves the new data to disk as well.
* Quite the performance bottleneck for now and DEFINIETLY not safe at all
* like wtf. But works for now.
*
* @param {string} key - The key to set
* @param {any} value - The value to set
*/
set(key, value) {
this.data[key] = value;

// Save the database to disk
this._saveData();
}

/**
* Retrieve data from a database with a given key.
*
* @todo multi-level getting
*
* @param {string} key - The key to retrieve from the database. (1-level deep)
* @param {any} [defaultValue = null] - The default value if no value was found.
* @return {any} - The data from the database.
*/
get(key, defaultValue = null) {
return database[key] || defaultValue;
}
}

Database.verifyOrCreateDatabaseAt = async function verifyOrCreateDatabaseAt(databasePath) {
// If the database.json file doenst exist, create it
// Otherwise, read from it and populate the database.
if (!(await fs.exists(config.databasePath))) {
await fs.writeFile(config.databasePath, '{}');
}
};




24 changes: 24 additions & 0 deletions app/database/api/DatabaseAPI.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/**
* @module @database
* @since 2.0.0
*/

const autoBind = require('auto-bind');

/**
* The base class to build database API's on
*/
class DatabaseAPI {
constructor(database) {
/**
* Reference to the database class
* @type {Database}
*/
this.database = database;

// Automatically bind all methods to "this"
autoBind(this);
}
}

module.exports = DatabaseAPI;
177 changes: 177 additions & 0 deletions app/database/api/UsersAPI.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
/**
* @module @database
* @since 2.0.0
*/

const DatabaseAPI = require('@database/api/DatabaseAPI');

const User = require('@models/User');

const UserError = require('@helpers/UserError');
const crypto = require('@helpers/crypto');

/**
* Database API for the User model
*/
class UsersAPI extends DatabaseAPI {
constructor(database) {
super(database);

/**
* The temporary password to use for the temporary user.
* @protected
* @type {string}
*/
this._tempPassword = Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);

// TODO: Temporary user creation should not be handled in the UsersAPI class
// Instead a new Defaults class or something should be created that handles all these edge cases.
const foundUsers = db.get('users', {});

// If no users are present, we generate a temporary user account and tell the user in the console
if (Object.keys(foundUsers).length === 0) {
logger.newLine();
logger.warn('=========================================================');
logger.newLine();
logger.warn('No registered users found in DB!');
logger.warn(`Enabling admin account 'temp' with password '${this._tempPassword}'`);
logger.newLine();
logger.warn('=========================================================');
logger.newLine();
}
}

/**
* Get all users from DB
* @return {Object<string, User>} The users map
*/
async all() {
return this.database
.get('users', { 'temp': User.composeTempUser(this._tempPassword) })
}

/**
* Set all users in DB
* @protected
* @param {Object<string, User>} users
*/
async _setUsers(users) {
this.database.set('users', users);
}

/**
* Set a User in DB
* @protected
* @param {string} username
* @param {User} user
*/
async _setUser(username, user) {
const users = await this.all();

users[username] = User.validate({
...user,
username // we make sure, the key we set is the same username as in the user object
});

await this._setUsers(users);
}

/**
* Find User by username
* @param {string} username The username
* @return {User | null}
*/
async findUnsafe(username) {
return (await this.all())[username] || null;
}

/**
* Find User by username and remove sensitive information
* @param {string} username The username
* @return {User | null}
*/
async find(username) {
const user = await this.findUnsafe(username);

return user
? { ...user, password: undefined }
: null;
}

/**
* Create a new user
* @param {string} username
* @param {User} userData
*/
async create(username, userData) {
if (await this.find(username))
throw new UserError(`Username ${username} unavailable`);

// Password can not be changed in the update method
await this._setUser(username, {
...userData,
password: await crypto.hashPassword(userData.password)
});
}

/**
* Delete a User
* @param {string} username
*/
async delete(username) {
// If user does't exist
if (!await this.find(username)) {
throw new UserError(`User ${username} doesn't exists`);
}

let users = await this.all();
delete users[username];
await this._setUsers(users);
}

/**
* Update the user meta in DB (display name and avatar url).
*
* This method does NOT allow changing the password.
* Only do this with `usersApi.users.updatePassword(<username>, <unhashed-password>)`.
*
* @param {string} username Username
* @param {User} user The user object
* @param {boolean} shouldCreate Should the user be created if he's not found
* @return {void}
*/
async update(username, userData) {
const oldUser = await this.findUnsafe(username);

if (!oldUser)
throw new UserError(`User ${username} could not be found`);

// Password can not be changed in the update method
await this._setUser(username, {
...oldUser, // First the old user data is the basis
...userData, // New user data overrides the old (doesn't have to be complete)
username: oldUser.username, // Make sure username stays the same
password: oldUser.password // Make sure password wasn't changed
});
}

/**
* Hash a users password and save it to DB.
*
* @param {string} username
* @param {string} password
*/
async updatePassword(username, password) {
const oldUser = await this.findUnsafe(username);

if (!oldUser)
throw new UserError(`User ${username} could not be found`);

await this._setUser(username, {
...oldUser,
password: await crypto.hashPassword(password)
});
}
}

module.exports = UsersAPI;
Loading

0 comments on commit 21f0fb5

Please sign in to comment.