user applications

This commit is contained in:
Erik 2022-11-13 21:11:01 +02:00
parent 941e1f484c
commit 7da9adf8a5
Signed by: Navy.gif
GPG Key ID: 811EC0CD80E7E5FB
4 changed files with 200 additions and 25 deletions

View File

@ -4,31 +4,40 @@ const { inspect } = require('node:util');
const { AbstractUserDatabase } = require("../interfaces/"); const { AbstractUserDatabase } = require("../interfaces/");
const { User } = require("../structures"); const { User } = require("../structures");
const UserApplicataion = require("../structures/UserApplication");
// MongoDB based user db // MongoDB based user db
class UserDatabase extends AbstractUserDatabase { class UserDatabase extends AbstractUserDatabase {
constructor (server, db, { userColllection = 'users', validUserTypes }) { constructor (server, db, {
userColllection = 'users',
appCollection = 'applications',
validUserTypes
}) {
super(); super();
this.db = db; this.db = db;
this.collectionName = userColllection;
this.logger = server.createLogger(this); this.logger = server.createLogger(this);
this.cache = new Collection(); this.cache = new Collection();
this.collection = null; this.userCollection = null;
User.setValidTypes(validUserTypes); User.setValidTypes(validUserTypes);
this._userColllection = userColllection;
this._appCollection = appCollection;
} }
init () { init () {
this.collection = this.db.collection(this.collectionName); // this.userCollection = this.db.collection(this._userColllection);
} }
async fetchUser (id, force = false) { async fetchUser (id, force = false) {
id = id.toString();
if (!force && this.cache.has(id)) return this.cache.get(id); if (!force && this.cache.has(id)) return this.cache.get(id);
if (id.includes('temp')) return null; if (id.includes('temp')) return null;
const data = await this.collection.findOne({ _id: ObjectId(id) }); const data = await this.db.findOne(this._userColllection, { _id: ObjectId(id) });
if (!data) return null; if (!data) return null;
const user = this._createUser(data); const user = this._createUser(data);
@ -38,13 +47,28 @@ class UserDatabase extends AbstractUserDatabase {
} }
async fetchApplication (id, force = false) {
id = id.toString();
if (!force && this.cache.has(id)) return this.cache.get(id);
const data = await this.db.findOne(this._appCollection, { _id: ObjectId(id) });
if (!data) return null;
const user = await this.fetchUser(data.user);
const app = this._createApp(user, data);
this.cache.set(id, app);
return app;
}
async findUser (username) { async findUser (username) {
let user = this.cache.find(u => u.username.toLowerCase() === username.toLowerCase()); let user = this.cache.find(u => u.username.toLowerCase() === username.toLowerCase());
if (user) return Promise.resolve(user); if (user) return Promise.resolve(user);
const data = await this.collection.findOne({ username }, { collation: { locale: 'en', strength: 2 } }); const data = await this.db.findOne(this._userColllection, { username }, { collation: { locale: 'en', strength: 2 } });
if (!data) return Promise.resolve(null); if (!data) return null;
user = this._createUser(data); user = this._createUser(data);
this.cache.set(user.id, user); this.cache.set(user.id, user);
@ -54,7 +78,7 @@ class UserDatabase extends AbstractUserDatabase {
} }
/** /**
* Retrieves the user to whom the token belongs * Matches a token against an application
* *
* @param {string} token * @param {string} token
* @return {User} * @return {User}
@ -63,16 +87,19 @@ class UserDatabase extends AbstractUserDatabase {
async matchToken (token) { async matchToken (token) {
if (!token) throw new Error('Missing token'); if (!token) throw new Error('Missing token');
let user = this.cache.find(u => u.apiKey === token); let app = this.cache.find(a => a.token.encrypted === token);
if (user) return Promise.resolve(user); if (app) return Promise.resolve(app);
const data = await this.collection.findOne({ apiKey: token }); const data = await this.db.findOne(this._appCollection, { 'token.encrypted': token });
if (!data) return Promise.resolve(null); if (!data) return null;
user = this._createUser(data); const user = await this.fetchUser(data.user);
this.cache.set(user.id, user); app = this._createApp(user, data);
return user; this.cache.set(app.id, app);
user.attachApplication(app);
return app;
} }
/** /**
@ -87,11 +114,15 @@ class UserDatabase extends AbstractUserDatabase {
let user = this.cache.find((u) => u.externalProfiles.discord?.id === profile.id); let user = this.cache.find((u) => u.externalProfiles.discord?.id === profile.id);
if (user) return Promise.resolve(user); if (user) return Promise.resolve(user);
const data = await this.collection.findOne({ 'externalProfiles.discord.id': profile.id }); const data = await this.db.findOne(this._userColllection, { 'externalProfiles.discord.id': profile.id });
if (data) return Promise.resolve(this._createUser(data)); if (data) {
user = this._createUser(data);
user.addExternalProfile('discord', profile);
return Promise.resolve(user);
}
// If a user with the same username already exists, force the holder of the discord account to create a new account or log in to the other account and link them // If a user with the same username already exists, force the holder of the discord account to create a new account or log in to the other account and link them
const existing = await this.collection.findOne({ username: profile.username }, { collation: { locale: 'en', strength: 2 } }); const existing = await this.db.findOne(this._userColllection, { username: profile.username }, { collation: { locale: 'en', strength: 2 } });
if (existing) { if (existing) {
const temp = this._createUser({ type: 'user', temporary: true, displayName: profile.username }); const temp = this._createUser({ type: 'user', temporary: true, displayName: profile.username });
temp.addExternalProfile('discord', profile); temp.addExternalProfile('discord', profile);
@ -138,15 +169,21 @@ class UserDatabase extends AbstractUserDatabase {
*/ */
async updateUser (user) { async updateUser (user) {
const { json } = user; const { json } = user;
this.logger.debug(`Storing user ${inspect(user.json)}`); this.logger.debug(`Updating user ${inspect(json)}`);
if (user._id) await this.collection.updateOne({ _id: ObjectId(user._id) }, { $set: json }); if (user._id) await this.db.updateOne(this._userColllection, { _id: ObjectId(user._id) }, json);
else { else {
const result = await this.collection.insertOne(json); const result = await this.db.insertOne(this._userColllection, json);
user._id = result.insertedId; user._id = result.insertedId;
} }
return user; return user;
} }
async updateApplication (app) {
const { json } = app;
this.logger.debug(`Updating application ${inspect(json)}`);
await this.db.updateOne(this._appCollection, { _id: ObjectId(json._id) }, json, true);
}
/** /**
* Helper function for creating user object * Helper function for creating user object
@ -161,6 +198,10 @@ class UserDatabase extends AbstractUserDatabase {
return new User(this, data); return new User(this, data);
} }
_createApp (user, data) {
return new UserApplicataion(user, data);
}
} }
module.exports = UserDatabase; module.exports = UserDatabase;

View File

@ -1,5 +1,6 @@
const { ApiEndpoint } = require("../../interfaces"); const { ApiEndpoint } = require("../../interfaces");
const pkg = require('../../../../package.json'); const pkg = require('../../../../package.json');
const { UtilMiddleware } = require('../../middleware');
const qrcode = require('qrcode'); const qrcode = require('qrcode');
const { authenticator } = require('otplib'); const { authenticator } = require('otplib');
@ -19,13 +20,30 @@ class UserEndpoint extends ApiEndpoint {
this.subpaths = [ this.subpaths = [
[ '/2fa', 'get', this.twoFactor.bind(this) ], [ '/2fa', 'get', this.twoFactor.bind(this) ],
[ '/2fa/verify', 'post', this.verify2fa.bind(this) ], [ '/2fa/verify', 'post', this.verify2fa.bind(this) ],
[ '/applications', 'get', this.applications.bind(this) ],
[ '/applications', 'post', this.createApplication.bind(this) ],
]; ];
this.middleware = [ server.authenticator.authenticate ]; this.middleware = [ server.authenticator.authenticate, UtilMiddleware.requireBody ];
} }
user (req, res) { user (req, res) {
return res.json(req.user.json); return res.json(req.user.safeJson);
}
async applications (req, res) {
const { user } = req;
const applications = await user.fetchApplications();
res.json(Object.values(applications).map(app => app.safeJson));
}
async createApplication (req, res) {
const { body, user } = req;
const { name } = body;
if (!name) return res.status(400).send('Missing name');
const application = await user.createApplication(name);
res.json(application.safeJson);
} }
async twoFactor (req, res) { async twoFactor (req, res) {

View File

@ -1,4 +1,7 @@
const Argon2 = require('argon2'); const Argon2 = require('argon2');
const { ObjectId } = require('mongodb');
const { Util } = require('../../util');
const UserApplicataion = require('./UserApplication');
class User { class User {
@ -23,7 +26,24 @@ class User {
if (User.validTypes.length && !User.validTypes.includes(data.type)) throw new Error('Invalid user type'); if (User.validTypes.length && !User.validTypes.includes(data.type)) throw new Error('Invalid user type');
this.type = data.type; this.type = data.type;
this.apiKey = data.apiKey; // TODO: this
this.applications = {};
this._applications = data.applications || [];
// Object.defineProperty(this.applications, 'json', {
// get: () => {
// const keys = Object.keys(this.applications);
// const obj = [];
// for (const key of keys) {
// obj[key] = this.applications[key].json;
// }
// return obj;
// }
// });
// const apps = Object.keys(data.applications);
// for (const id of apps) {
// const app = data.applications[id];
// this.applications[id] = new UserApplicataion(this, app);
// }
this.externalProfiles = data.externalProfiles || {}; this.externalProfiles = data.externalProfiles || {};
this.permissions = { ...User.defaultPermissions, ...data.permissions }; this.permissions = { ...User.defaultPermissions, ...data.permissions };
@ -49,6 +69,33 @@ class User {
return this._passwordHash !== null; return this._passwordHash !== null;
} }
async fetchApplications () {
const apps = await Promise.all(this._applications.map(id => this._db.fetchApplication(id)));
for (const app of apps) this.applications[app.id] = app;
return this.applications;
}
async createApplication (name) {
const _id = new ObjectId();
const rawToken = `${this.id}:${_id}:${Util.randomString()}`;
const token = Util.encrypt(rawToken, Buffer.from(process.env.ENCRYPTION_KEY, 'base64'));
const opts = {
name,
_id,
token
};
const application = new UserApplicataion(this, opts);
await application.save();
this.applications[_id] = application;
this._applications.push(_id);
await this.save();
return application;
}
attachApplication (app) {
this.applications[app.id] = app;
}
async setPassword (passwd, save = false) { async setPassword (passwd, save = false) {
const hash = await Argon2.hash(passwd); const hash = await Argon2.hash(passwd);
this._passwordHash = hash; this._passwordHash = hash;
@ -93,6 +140,10 @@ class User {
return this._db.updateUser(this); return this._db.updateUser(this);
} }
saveApplication (app) {
return this._db.updateApplication(app);
}
get json () { get json () {
return { return {
_id: this._id, _id: this._id,
@ -104,7 +155,22 @@ class User {
password: this._passwordHash, password: this._passwordHash,
otpSecret: this._otpSecret, otpSecret: this._otpSecret,
twoFactor: this._2fa, twoFactor: this._2fa,
apiKey: this.apiKey, applications: this._applications,
};
}
get safeJson () {
return {
id: this.id,
username: this.username,
displayName: this._displayName,
type: this.type,
permissions: this.permissions,
externalProfiles: Object.values(this.externalProfiles).map(prof => {
return { id: prof.id, provider: prof.provider, username: prof.username };
}),
twoFactor: this._2fa,
applications: this._applications,
}; };
} }

View File

@ -0,0 +1,50 @@
class UserApplicataion {
constructor (user, options) {
if (!user) throw new Error('Missing user for user application');
this.user = user;
if (!options._id) throw new Error('Missing id for application');
this.id = options._id;
if (!options.name) throw new Error('Missing name for application');
this.name = options.name;
if (!options.token) throw new Error('Missing token for appliaction');
this.token = options.token;
this.permissions = options.permissions || {};
this.createdAt = options.createdAt || Date.now();
}
save () {
return this.user.saveApplication(this);
}
get json () {
return {
_id: this.id,
name: this.name,
permissions: this.permissions,
token: this.token,
createdAt: this.createdAt,
user: this.user.id,
};
}
get safeJson () {
return {
name: this.name,
id: this.id,
permissions: this.permissions,
token: this.token.encrypted,
user: this.user.id,
createdAt: this.createdAt
};
}
}
module.exports = UserApplicataion;