user applications
This commit is contained in:
parent
941e1f484c
commit
7da9adf8a5
@ -4,31 +4,40 @@ const { inspect } = require('node:util');
|
||||
|
||||
const { AbstractUserDatabase } = require("../interfaces/");
|
||||
const { User } = require("../structures");
|
||||
const UserApplicataion = require("../structures/UserApplication");
|
||||
|
||||
// MongoDB based user db
|
||||
class UserDatabase extends AbstractUserDatabase {
|
||||
|
||||
constructor (server, db, { userColllection = 'users', validUserTypes }) {
|
||||
constructor (server, db, {
|
||||
userColllection = 'users',
|
||||
appCollection = 'applications',
|
||||
validUserTypes
|
||||
}) {
|
||||
|
||||
super();
|
||||
this.db = db;
|
||||
this.collectionName = userColllection;
|
||||
this.logger = server.createLogger(this);
|
||||
this.cache = new Collection();
|
||||
this.collection = null;
|
||||
this.userCollection = null;
|
||||
User.setValidTypes(validUserTypes);
|
||||
|
||||
this._userColllection = userColllection;
|
||||
this._appCollection = appCollection;
|
||||
|
||||
}
|
||||
|
||||
init () {
|
||||
this.collection = this.db.collection(this.collectionName);
|
||||
// this.userCollection = this.db.collection(this._userColllection);
|
||||
}
|
||||
|
||||
async fetchUser (id, force = false) {
|
||||
|
||||
id = id.toString();
|
||||
if (!force && this.cache.has(id)) return this.cache.get(id);
|
||||
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;
|
||||
|
||||
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) {
|
||||
|
||||
let user = this.cache.find(u => u.username.toLowerCase() === username.toLowerCase());
|
||||
if (user) return Promise.resolve(user);
|
||||
|
||||
const data = await this.collection.findOne({ username }, { collation: { locale: 'en', strength: 2 } });
|
||||
if (!data) return Promise.resolve(null);
|
||||
const data = await this.db.findOne(this._userColllection, { username }, { collation: { locale: 'en', strength: 2 } });
|
||||
if (!data) return null;
|
||||
|
||||
user = this._createUser(data);
|
||||
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
|
||||
* @return {User}
|
||||
@ -63,16 +87,19 @@ class UserDatabase extends AbstractUserDatabase {
|
||||
async matchToken (token) {
|
||||
if (!token) throw new Error('Missing token');
|
||||
|
||||
let user = this.cache.find(u => u.apiKey === token);
|
||||
if (user) return Promise.resolve(user);
|
||||
let app = this.cache.find(a => a.token.encrypted === token);
|
||||
if (app) return Promise.resolve(app);
|
||||
|
||||
const data = await this.collection.findOne({ apiKey: token });
|
||||
if (!data) return Promise.resolve(null);
|
||||
const data = await this.db.findOne(this._appCollection, { 'token.encrypted': token });
|
||||
if (!data) return null;
|
||||
|
||||
user = this._createUser(data);
|
||||
this.cache.set(user.id, user);
|
||||
const user = await this.fetchUser(data.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);
|
||||
if (user) return Promise.resolve(user);
|
||||
|
||||
const data = await this.collection.findOne({ 'externalProfiles.discord.id': profile.id });
|
||||
if (data) return Promise.resolve(this._createUser(data));
|
||||
const data = await this.db.findOne(this._userColllection, { 'externalProfiles.discord.id': profile.id });
|
||||
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
|
||||
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) {
|
||||
const temp = this._createUser({ type: 'user', temporary: true, displayName: profile.username });
|
||||
temp.addExternalProfile('discord', profile);
|
||||
@ -138,15 +169,21 @@ class UserDatabase extends AbstractUserDatabase {
|
||||
*/
|
||||
async updateUser (user) {
|
||||
const { json } = user;
|
||||
this.logger.debug(`Storing user ${inspect(user.json)}`);
|
||||
if (user._id) await this.collection.updateOne({ _id: ObjectId(user._id) }, { $set: json });
|
||||
this.logger.debug(`Updating user ${inspect(json)}`);
|
||||
if (user._id) await this.db.updateOne(this._userColllection, { _id: ObjectId(user._id) }, json);
|
||||
else {
|
||||
const result = await this.collection.insertOne(json);
|
||||
const result = await this.db.insertOne(this._userColllection, json);
|
||||
user._id = result.insertedId;
|
||||
}
|
||||
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
|
||||
@ -161,6 +198,10 @@ class UserDatabase extends AbstractUserDatabase {
|
||||
return new User(this, data);
|
||||
}
|
||||
|
||||
_createApp (user, data) {
|
||||
return new UserApplicataion(user, data);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = UserDatabase;
|
@ -1,5 +1,6 @@
|
||||
const { ApiEndpoint } = require("../../interfaces");
|
||||
const pkg = require('../../../../package.json');
|
||||
const { UtilMiddleware } = require('../../middleware');
|
||||
|
||||
const qrcode = require('qrcode');
|
||||
const { authenticator } = require('otplib');
|
||||
@ -19,13 +20,30 @@ class UserEndpoint extends ApiEndpoint {
|
||||
this.subpaths = [
|
||||
[ '/2fa', 'get', this.twoFactor.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) {
|
||||
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) {
|
||||
|
@ -1,4 +1,7 @@
|
||||
const Argon2 = require('argon2');
|
||||
const { ObjectId } = require('mongodb');
|
||||
const { Util } = require('../../util');
|
||||
const UserApplicataion = require('./UserApplication');
|
||||
|
||||
class User {
|
||||
|
||||
@ -23,7 +26,24 @@ class User {
|
||||
if (User.validTypes.length && !User.validTypes.includes(data.type)) throw new Error('Invalid user 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.permissions = { ...User.defaultPermissions, ...data.permissions };
|
||||
@ -49,6 +69,33 @@ class User {
|
||||
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) {
|
||||
const hash = await Argon2.hash(passwd);
|
||||
this._passwordHash = hash;
|
||||
@ -93,6 +140,10 @@ class User {
|
||||
return this._db.updateUser(this);
|
||||
}
|
||||
|
||||
saveApplication (app) {
|
||||
return this._db.updateApplication(app);
|
||||
}
|
||||
|
||||
get json () {
|
||||
return {
|
||||
_id: this._id,
|
||||
@ -104,7 +155,22 @@ class User {
|
||||
password: this._passwordHash,
|
||||
otpSecret: this._otpSecret,
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
|
50
src/server/structures/UserApplication.js
Normal file
50
src/server/structures/UserApplication.js
Normal 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;
|
Loading…
Reference in New Issue
Block a user