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:
D. Berge
2025-07-24 18:28:54 +02:00
parent 825530c1fe
commit 065f6617af
4 changed files with 257 additions and 3 deletions

View File

@@ -13,5 +13,6 @@ module.exports = {
meta: require('./meta'),
navdata: require('./navdata'),
queue: require('./queue'),
qc: require('./qc')
qc: require('./qc'),
user: require('./user'),
};

View 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
}

View 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;

View File

@@ -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
}