diff --git a/lib/www/server/lib/db/index.js b/lib/www/server/lib/db/index.js index ae7c2f0..e2010e2 100644 --- a/lib/www/server/lib/db/index.js +++ b/lib/www/server/lib/db/index.js @@ -13,5 +13,6 @@ module.exports = { meta: require('./meta'), navdata: require('./navdata'), queue: require('./queue'), - qc: require('./qc') + qc: require('./qc'), + user: require('./user'), }; diff --git a/lib/www/server/lib/db/user/User.js b/lib/www/server/lib/db/user/User.js new file mode 100644 index 0000000..810f9b4 --- /dev/null +++ b/lib/www/server/lib/db/user/User.js @@ -0,0 +1,216 @@ +const util = require('util'); +const hashPassword = require('./hashPassword'); +const { User } = require('@dougal/user'); +const { pool } = require('../connection'); +// const Organisations = require('../../organisations/Organisations'); + + +class ServerUser extends User { + + client // PostgreSQL client connection + + constructor (data, client) { + super (data); + if (data?.password) this.password = data.password; + if (data?.hash) this.values.hash = data.hash; + this.client = client; + } + + /* + * Getters + */ + + get hash () { + return this.values.hash; + } + + /* + * Setters + */ + + set password (v) { + if (v?.length) { + this.values.hash = hashPassword(this.id, v); + } else { + delete this.values.hash; + } + } + + /* + * Validation methods + */ + + /* + * Filtering methods + */ + + /* + * General methods + */ + + checkPassword (text) { + return Boolean(this.hash && hashPassword(this.id, text) == this.hash); + } + + /* + * Conversion and presentation methods + */ + + toJSON () { + return { + ...super.toJSON(), + hash: this.hash + } + } + + [util.inspect.custom] (depth, options) { + return `User ${this.id} (${this.name}) { ${this.organisations.entries().map( ([k, v]) => { + let str = ""; + str += k+": "; + str += v.read ? "r" : "-"; + str += v.write ? "w" : "-"; + str += v.edit ? "e" : "-"; + return str; + }).join("; ")} }`; + } + + /* + * SQL methods + */ + + /** Read user(s) from the database + * + * @a client is a database connection + * @a id is an optional user ID + * + * If `id` is not provided, it will return + * all users in the database. + */ + static async fromSQL (client, id) { + if (!client) { + client = pool; + } + + if (id) { + // Get a specific user + const text = ` + SELECT key, data, last_modified + FROM public.keystore + WHERE type = 'user' AND key = $1; + `; + + const res = await client.query(text, [ id ]); + const obj = res.rows[0]; + + if (obj) { + return new ServerUser({...obj.data, id: obj.key, last_modified: obj.last_modified}, client); + } + } else { + // Get all users + const text = ` + SELECT key, data, last_modified + FROM public.keystore + WHERE type = 'user'; + `; + + const res = await client.query(text); + + return res.rows?.map(row => new ServerUser({ + ...row.data, + id: row.key, + last_modified: row.last_modified + }, client)); + } + } + + static async authenticateSQL (name, password, client) { + if (!client) { + client = pool; + } + + const text = ` + SELECT key, data, last_modified + FROM public.keystore + WHERE type = 'user' + AND data->>'name' = $1 + ORDER BY key; + `; + + const res = await client.query(text, [name]); + for (const row of res.rows) { + const user = new ServerUser({ + ...row.data, + id: row.key, + last_modified: row.last_modified + }, client); + if (user.checkPassword(password)) { + user.password = null; // Remove the hash from the reply + return user; + } + } + } + + /** Save this user to the database + */ + async save () { + if (!this.client) { + this.client = pool; + } + + const data = this.toJSON(); + const id = data.id; + delete data.id; + + const text = ` + INSERT INTO public.keystore AS t + (type, key, data) + VALUES ('user', $1, $2::jsonb) + ON CONFLICT (type, key) + DO UPDATE SET + data = EXCLUDED.data + WHERE + t.type = EXCLUDED.type AND t.key = EXCLUDED.key + RETURNING key, data, last_modified; + `; + + const res = await this.client?.query(text, [ id, data ]); + + if (res?.rows?.length) { + const row = res.rows[0]; + return new ServerUser({ + ...row.data, + id: row.key, + last_modified: row.last_modified + }, this.client); + } + } + + /** Delete this user from the database + */ + async remove () { + if (!this.client) { + this.client = pool; + } + + const text = ` + DELETE FROM public.keystore + WHERE type = 'user' AND key = $1; + `; + + const res = await this.client?.query(text, [ this.id ]); + console.log("ID", this.id); + console.log(res); + return res.rowCount; + } + +} + + +if (typeof module !== 'undefined' && module.exports) { + module.exports = ServerUser; // CJS export +} + +// ESM export +if (typeof exports !== 'undefined' && !exports.default) { + exports.default = ServerUser; // ESM export +} diff --git a/lib/www/server/lib/db/user/hashPassword.js b/lib/www/server/lib/db/user/hashPassword.js new file mode 100644 index 0000000..ac22466 --- /dev/null +++ b/lib/www/server/lib/db/user/hashPassword.js @@ -0,0 +1,9 @@ +const crypto = require('crypto'); + +function hashPassword (userID, password) { + return crypto + .pbkdf2Sync(password, 'Dougal'+userID, 10712, 48, 'sha512') + .toString('base64'); +} + +module.exports = hashPassword; diff --git a/lib/www/server/lib/db/user/index.js b/lib/www/server/lib/db/user/index.js index 2a54c7d..da470ea 100644 --- a/lib/www/server/lib/db/user/index.js +++ b/lib/www/server/lib/db/user/index.js @@ -1,4 +1,5 @@ const { pool } = require('../connection'); +const hashPassword = require('./hashPassword'); const IPUser = Symbol("IPUser"); const HostUser = Symbol("HostUser"); @@ -33,7 +34,7 @@ async function userOfType(type, opts = {}) { const users = res.rows.map( row => ({ ...row.data, id: row.key, - hash: undefined + hash: (opts.insecure === true ? row.data.hash : undefined) }) ).filter( row => opts.active == null || opts.active === row.active ); return users; @@ -53,8 +54,35 @@ async function named (opts = {}) { return await userOfType(NamedUser, opts); } +async function authenticate (handle, password) { + const namedUsers = await named({insecure: true, active: true}); + + for (const namedUser of namedUsers) { + if (handle == namedUser.name) { + const hash = hashPassword(namedUser.id, password); + if (hash == namedUser.hash) { + delete namedUser.hash; + return namedUser; + } + } + } + // return undefined; +}; + + + module.exports = { ip, host, - named + named, + + authenticate, + hashPassword, + + list: require('./list'), + get: require('./get'), + post: require('./post'), + put: require('./put'), + delete: require('./delete'), + create: require('./post'), // synonym }