-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
6 changed files
with
317 additions
and
229 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, '{}'); | ||
} | ||
}; | ||
|
||
|
||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
Oops, something went wrong.