mirror of
https://gitlab.com/wgp/dougal/software.git
synced 2025-12-06 10:27:09 +00:00
Add class ServerUser derived from User.
Used on the backend. Adds methods to hash and check passwords and to read from and save user data to the database.
This commit is contained in:
@@ -13,5 +13,6 @@ module.exports = {
|
||||
meta: require('./meta'),
|
||||
navdata: require('./navdata'),
|
||||
queue: require('./queue'),
|
||||
qc: require('./qc')
|
||||
qc: require('./qc'),
|
||||
user: require('./user'),
|
||||
};
|
||||
|
||||
216
lib/www/server/lib/db/user/User.js
Normal file
216
lib/www/server/lib/db/user/User.js
Normal file
@@ -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
|
||||
}
|
||||
9
lib/www/server/lib/db/user/hashPassword.js
Normal file
9
lib/www/server/lib/db/user/hashPassword.js
Normal file
@@ -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;
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user