From 50cc5c93a95874ab3f9fa0d07b07b1b2c874dbe5 Mon Sep 17 00:00:00 2001 From: rxbn_ Date: Mon, 31 Aug 2020 09:49:24 +0200 Subject: [PATCH] web,db - user groups using object ids now --- bin/config.js | 3 +- bin/database/models.js | 11 +- bin/database/module.js | 220 ++++++++++++++++++++++++++++++++--- bin/web/routes/api.js | 6 +- bin/web/routes/static.js | 44 ++++--- bin/web/views/blocks/nav.pug | 5 +- 6 files changed, 246 insertions(+), 43 deletions(-) diff --git a/bin/config.js b/bin/config.js index a52c0cc..95de709 100644 --- a/bin/config.js +++ b/bin/config.js @@ -28,7 +28,8 @@ module.exports = { app: { locale: 'de-DE', // default locale (de-DE & en-EN should be available) name: 'authRXBN', - passhashDelimiter: '|' + passhashDelimiter: '|', + adminGroupname: "Administration" }, mongoose: { uri: process.env.DB_URL, diff --git a/bin/database/models.js b/bin/database/models.js index 01b0087..0145af2 100644 --- a/bin/database/models.js +++ b/bin/database/models.js @@ -34,12 +34,17 @@ models.user = new Schema({ }, mfa: { // multi factor authentication active: {type: Boolean, default: false}, - type: {type: String, default: ""}, - data: {type: String, default: ""} // tel number or secret token + data: {type: Array, default: [ // add each mfa type + //{ + // no: 0, + // type: "TOTP"||"HOTP"||"WebAuthn", + // data: "32CharHex"||"32CharHex"||"UserPublicKey" + //}, ... + ]} }, settings: {type: Object, default: {}}, // custom settings (theme etc. pp.) roles: {type: String, default: ""}, // user-defined roles and permissions - group: {type: Number, default: 0}, // group-id for group-defined roles and permissions + group: Schema.Types.ObjectId, // reference to group reg_date: {type: Date, default: Date.now}, // registration date last_action: {type: Date, default: Date.now}, // last action (activity date) }); diff --git a/bin/database/module.js b/bin/database/module.js index 98b5bf9..daf7886 100644 --- a/bin/database/module.js +++ b/bin/database/module.js @@ -73,8 +73,8 @@ methods.getConnection = () => { * @param {String} nick nickname * @param {String} email email * @param {String} passhash hashed password - * @param {Number} group Group id (normally 0 -> user) - * @return {Array} async (reply, err) + * @param {Number} group Group ObjectId + * @return {Object} async (reply, err) */ methods.addUser = async (nick, email, passhash, group) => { if(typeof nick !== 'string') return {err: new TypeError('nick is not a string::database.addUser('+nick+','+email+','+passhash+','+group+')', module.filename)}; @@ -105,7 +105,7 @@ methods.addUser = async (nick, email, passhash, group) => { * @async * @TODO add functionality * @param {String} haystack email or nick - * @return {Array} async(reply, err) + * @return {Object} async(reply, err) */ methods.delUser = async (haystack) => { if(typeof haystack !== 'string') return {err: new TypeError('haystack is not a string::database.delUser('+haystack+')', module.filename)}; @@ -129,7 +129,7 @@ methods.delUser = async (haystack) => { * get all users * @author Ruben Meyer * @async - * @return {Array} async(reply, err) + * @return {Object} async(reply, err) */ methods.getUsers = async () => { let userModel = models.user; @@ -150,7 +150,7 @@ methods.getUsers = async () => { * @author Ruben Meyer * @async * @param {String|String[]} haystack email or nick - * @return async(reply, err) + * @return {Object} async(reply, err) */ methods.getUser = async (haystack) => { if(typeof haystack !== 'string' && typeof haystack !== 'object') return {err: new TypeError('email or nickname is not a string|object::database.getUser('+haystack+')', module.filename)}; @@ -161,14 +161,17 @@ methods.getUser = async (haystack) => { haystack = sanitize(haystack); let or = []; - if(typeof haystack === 'string') { + if(haystack instanceof mongoose.Types.ObjectId) { + or.push({_id: haystack}); + } + else if(typeof haystack === 'string') { or = [{nickname: haystack}, {email: haystack}, {token: haystack}]; - if(haystack.match(/^[0-9a-fA-F]{24}$/)) or.push({_id: haystack}); + if(haystack.match(/^[0-9a-fA-F]{24}$/)) or.push({_id: mongoose.Types.ObjectId(haystack)}); } else { or = []; for(let i = 0; i < haystack.length; i++) { - if(haystack[i].match(/^[0-9a-fA-F]{24}$/)) or.push({_id: haystack[i]}); + if(haystack[i].match(/^[0-9a-fA-F]{24}$/)) or.push({_id: mongoose.Types.ObjectId(haystack[i])}); or.push({nickname: haystack[i]}); or.push({email: haystack[i]}); or.push({token: haystack[i]}); @@ -178,8 +181,8 @@ methods.getUser = async (haystack) => { try { users = await userModel.find().or(or).exec(); - if(users.length > 0) - return {reply: users}; + if(users.length == 1) + return {reply: users[0]}; else return {reply: false}; } catch(err) { @@ -194,7 +197,7 @@ methods.getUser = async (haystack) => { * @async * @param {Number} id User ID * @param {Object} obj data - * @return {Array} async(reply, err) + * @return {Object} async(reply, err) */ methods.updateUser = async (id, obj) => { if(typeof id !== 'string') return {err: new TypeError('id is not a string::database.updateUser('+id+','+JSON.stringify(obj)+')', module.filename)}; @@ -219,7 +222,7 @@ methods.updateUser = async (id, obj) => { * @TODO UPDATE METHOD; PROBABLY OUTDATED * @param {Number} id User ID * @param {Object} data data JSON -> remember - * @return {Array} async({date => 'Login Date', token => 'RememberMe Cookie Token'}, err) + * @return {Object} async({date => 'Login Date', token => 'RememberMe Cookie Token'}, err) */ methods.addActivity = async (id, data) => { if(typeof id !== 'string') return {err: new TypeError('id is not a string::database.updateNewAction('+id+','+JSON.stringify(options)+')', module.filename)}; @@ -260,6 +263,179 @@ methods.addActivity = async (id, data) => { } }; +// //////// /////// //////// // // /////// ////// +// // // // // // // // // // // +// //////// /////// // // // // /////// ////// +// // // // // // // // // // // +// //////// // // //////// //////// // ////// +// +///////////////////////////////////////////////////////////////// + + +/** + * get all groups + * @author Ruben Meyer + * @async + * @return {Object} async(reply, err) + */ +methods.getGroups = async () => { + let groupModel = models.group; + + try { + groups = await groupModel.find({}).exec(); + if(groups.length > 0) + return {reply: groups}; + else + return {reply: false}; + } catch(err) { + return {err: err}; + } +}; + +/** + * query groups by UUID or name + * @author Ruben Meyer + * @async + * @param {String|String[]} haystack UUID or name + * @return {Object} async(reply, err) + */ +methods.getGroup = async (haystack) => { + if(typeof haystack !== 'string' && typeof haystack !== 'object') return {err: new TypeError('haystack is not a string|object::database.getGroup('+haystack+')', module.filename)}; + + let groupModel = models.group; + + // sanitize input + haystack = sanitize(haystack); + + let or = []; + if(haystack instanceof mongoose.Types.ObjectId) { + or.push({_id: haystack}); + } + else if(typeof haystack === 'string') { + or = [{name: haystack}]; + if(haystack.match(/^[0-9a-fA-F]{24}$/)) or.push({_id: mongoose.Types.ObjectId(haystack)}); + } + else { + or = []; + for(let i = 0; i < haystack.length; i++) { + if(haystack[i].match(/^[0-9a-fA-F]{24}$/)) or.push({_id: mongoose.Types.ObjectId(haystack[i])}); + or.push({name: haystack[i]}); + } + } + + try { + groups = await groupModel.find().or(or).exec(); + + if(groups.length == 1) + return {reply: groups[0]}; + else + return {reply: false}; + } catch(err) { + return {err: err}; + } +}; + +/** + * Adds Group to Database + * @author Ruben Meyer + * @async + * @param {String} name name + * @param {String} roles roles + * @return {Object} async (reply, err) + */ +methods.addGroup = async (name, roles) => { + if(typeof name !== 'string') return {err: new TypeError('name is not a string::database.addGroup('+name+','+roles+')', module.filename)}; + if(typeof roles !== 'string') return {err: new TypeError('name is not a string::database.addGroup('+name+','+roles+')', module.filename)}; + + let groupModel = models.group; + + let group = new groupModel(); + + // sanitize input + group.name = sanitize(nick); + group.roles = sanitize(email); + + try { + reply = await group.save(); + return {reply: 1}; + } catch(err) { + return {err: err}; + } +}; + +/** + * updates obj keys in group entry + * @author Ruben Meyer + * @async + * @param {Number} id Group ID + * @param {Object} obj data + * @return {Object} async(reply, err) + */ +methods.updateGroup = async (id, obj) => { + if(typeof id !== 'string') return {err: new TypeError('id is not a string::database.updateGroup('+id+','+JSON.stringify(obj)+')', module.filename)}; + if(typeof obj !== 'object') return {err: new TypeError('obj is not an object::database.updateGroup('+id+','+JSON.stringify(obj)+')', module.filename)}; + + let groupModel = models.group; + + try { + data = await groupModel.findByIdAndUpdate(id, obj).exec(); + return {data: data}; + } catch(err) { + return {err: err}; + } + +}; + +/** + * delete group and set fallback group for users + * @author Ruben Meyer + * @async + * @param {String} id group id + * @param {String} fallbackId fallback group id + * @return {Boolean} + */ +methods.delGroup = async (id, fallbackId) => { + if(typeof id !== 'string') return {err: new TypeError('id is not a string::database.delGroup('+id+','+ fallbackId +')', module.filename)}; + if(typeof fallbackId !== 'string') return {err: new TypeError('fallbackId is not a string::database.delGroup('+id+','+ fallbackId +')', module.filename)}; + + let groupModel = models.group; + let userModel = models.user; + let pathModel = models.pathRules; + + // sanitize input + id = sanitize(id); + fallbackId = sanitize(fallbackId); + + try { + + try { + // find users + users = await userModel.find({group: id}).exec(); + + // set fallback group for each user + users.forEach(async (user) => { + await userModel.findByIdAndUpdate(user._id, {group: fallbackId}).exec(); + }); + + // find rules + rules = await pathModel.find({group: id}).exec(); + + // set fallback group for each rule + rules.forEach(async (rule) => { + await pathModel.findByIdAndUpdate(rule._id, {group: fallbackId}).exec(); + }); + + // remove group + reply = await groupModel.findByIdAndRemove(id).exec(); + return {reply: 1}; + } catch (e) { + return {err: e}; + } + } catch(err) { + return {err: err}; + } +}; + // //////// //////// ////////// // // //////// // // // // // // // // // // //////// // // // // // // @@ -294,11 +470,12 @@ methods.getPathRules = async () => { // ////////////////////////////////////////////// + /** * get Applications * @author Ruben Meyer * @async - * @return {Array} async(apps, err) + * @return {Object} async(apps, err) */ methods.getApps = async () => { var Application = models.application; @@ -314,8 +491,9 @@ methods.getApps = async () => { * return auth obj * @author Ruben Meyer * @async + * @TODO * @param {Object} obj data obj (aId, uId) - * @return {Array} async({timestamp, token}, err) + * @return {Object} async({timestamp, token}, err) */ methods.setAuthCode = async (obj) => { if(typeof obj !== 'object') return {err: new TypeError('obj is not an object::database.setAuthCode('+JSON.stringify(obj)+')', module.filename)}; @@ -347,8 +525,9 @@ methods.setAuthCode = async (obj) => { * return auth obj * @author Ruben Meyer * @async + * @TODO * @param {Object} obj data obj (aId, aSecret, uId, token) - * @return {Array} async(bool, err) + * @return {Object} async(bool, err) */ methods.getAuth = async (obj) => { if(typeof obj !== 'object') return {err: new TypeError('obj is not an object::database.getAuthCode('+JSON.stringify(obj)+')', module.filename)}; @@ -369,9 +548,11 @@ methods.getAuth = async (obj) => { var Application = models.application; try { + if(!(obj.aId instanceof mongoose.Types.ObjectId)) obj.aId = mongoose.Types.ObjectId(obj.aId); + data1 = await Application.findOne({ $and: [ - {_id: mongoose.Types.ObjectId(obj.aId)}, + {_id: obj.aId}, {secret: obj.aSecret} ] }).exec(); @@ -398,11 +579,12 @@ methods.getAuth = async (obj) => { }; /** - * return if app is permitted to do access call + * return app permission * @author Ruben Meyer * @async + * @TODO * @param {Object} obj data obj (aId, redirectUrl) - * @return {Array} async(bool, err) + * @return {Object} async(bool, err) */ methods.verifyAppCall = async (obj) => { return {}; @@ -420,7 +602,7 @@ methods.verifyAppCall = async (obj) => { * returns user count * @author Ruben Meyer * @async - * @return {Array} async(int, err) + * @return {Object} async(int, err) */ methods.userCount = async () => { let userModel = models.user; diff --git a/bin/web/routes/api.js b/bin/web/routes/api.js index 5eb99a4..85b9b01 100644 --- a/bin/web/routes/api.js +++ b/bin/web/routes/api.js @@ -76,7 +76,7 @@ getRoutes = async () => { } // no reply (user does not exist) or password is wrong - if(!user.reply || user.reply === null || user.reply.length == 0 || user.reply.length > 1 || !global['requireModule']('auth').validateHash(user.reply[0].passhash, pass)) { + if(!user.reply || user.reply === null || user.reply.length == 0 || user.reply.length > 1 || !global['requireModule']('auth').validateHash(user.reply.passhash, pass)) { return res.type('json').status(401).end(JSON.stringify({ status: 401, message: 'msg.auth.login.failed' @@ -88,8 +88,8 @@ getRoutes = async () => { // add session data req.session.user = { - 'id': user.reply[0]._id, - 'group': user.reply[0].group + 'id': user.reply._id, + 'group': user.reply.group }; return res.type('json').end(JSON.stringify({ diff --git a/bin/web/routes/static.js b/bin/web/routes/static.js index 681c69d..9019c1f 100644 --- a/bin/web/routes/static.js +++ b/bin/web/routes/static.js @@ -20,22 +20,27 @@ let getRoutes = async () => { /** * main page * @url / - * @method all + * @method GET */ - route.all('/', asyncer(async (req, res, next) => { - // TODO: show login page or dashboard - // res.end('login or dashboard'); - apps = await db.getApps(); - res.render('index', { + route.get('/', asyncer(async (req, res, next) => { + obj = { session: req.session, - apps: apps.reply, cfg: cfg - }); + }; + + // if user is logged in + if(req.session && req.session.user) { + obj.user = (await db.getUser(req.session.user.id)).reply; + obj.group = (await db.getGroup(obj.user.group)).reply; + apps = await db.getApps(); + obj.apps = apps.reply; + } + res.render('index', obj); })); /** * login page or apprequest page - * @url / + * @url /authenticate * @method GET */ route.get('/authenticate', asyncer(async (req, res) => { @@ -55,25 +60,29 @@ let getRoutes = async () => { // req.query.appId // verify appId (if in rep) req.session.appRequest.appId = req.query.appId; - - // TODO: on accept, setAuthCode and redirect with token - // on cancel, redirect to dashboard } + } else { + return res.redirect('/'); } // if user is logged in, show request page if(req.session && req.session.user) { - res.render('request', { + user = await db.getUser(req.session.user.id); + group = await db.getGroup(user.reply.group); + + return res.render('request', { session: req.session, appRequest: req.session.appRequest, apps: apps.reply, - cfg: cfg + cfg: cfg, + user: user.reply, + group: group.reply }); // if user isnt logged in, show login page } else { if(!req.query.appId) req.session.appRequest = {}; - let view_obj = { session: req.session }; + let view_obj = { session: req.session, cfg: cfg }; if(req.query.appId) { apps.reply.forEach((app) => { if(app._id == req.query.appId) @@ -82,7 +91,10 @@ let getRoutes = async () => { } - res.render('login', view_obj); + return res.render('login', view_obj); + } + })); + } })); diff --git a/bin/web/views/blocks/nav.pug b/bin/web/views/blocks/nav.pug index 61eb5d9..6eedd58 100644 --- a/bin/web/views/blocks/nav.pug +++ b/bin/web/views/blocks/nav.pug @@ -1,7 +1,8 @@ mixin navItem(name, id, symbol, href) li(title=name, id=id) a(href=href) - i.fa-fw(class=symbol) + if(symbol !== "") + i.fa-fw(class=symbol) span= name // Navigation @@ -14,6 +15,8 @@ nav(uk-navbar).uk-navbar-container .uk-navbar-right.uk-margin-right ul.uk-navbar-nav if(session && session.user) + if(group && group.name == cfg.app.adminGroupname) + +navItem("Administration", "admin", "", "/admin") +navItem("Apps", "apps", "fas fa-tachometer-alt", "/") +navItem("Settings", "settings", "fas fa-wrench", "/settings") +navItem("Logout", "logout", "fas fa-sign-out-alt", "/logout")