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 { 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;
|
@ -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) {
|
||||||
|
@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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