From 3f1532e128ff6ec36faa9bc30f463d10a5e7dc57 Mon Sep 17 00:00:00 2001 From: Dev Singh Date: Tue, 22 Aug 2023 14:50:13 +0000 Subject: [PATCH] add admin panel --- config.js | 2 +- index.js | 258 +++++++++++++++++--------------- view/components/footer.html | 2 +- view/components/fullNavbar.html | 5 + view/unauthorized.html | 3 +- 5 files changed, 148 insertions(+), 122 deletions(-) diff --git a/config.js b/config.js index ad04ecf..43bc0f1 100644 --- a/config.js +++ b/config.js @@ -3,7 +3,7 @@ require('dotenv').config() exports.groups_permitted = process.env.GROUPS_PERMITTED ? process.env.GROUPS_PERMITTED.split(',') : ['ACM Link Shortener Managers', 'ACM Exec', 'ACM Officers', 'ACM Infra Leadership']; -exports.admin_groups = process.env.ADMIN_GROUPS ? process.env.ADMIN_GROUPS.split(',') : ['ACM Infra Leadership']; +exports.admin_groups = process.env.ADMIN_GROUPS ? process.env.ADMIN_GROUPS.split(',') : ['ACM Infra Leadership', 'ACM Officers']; exports.branding = { title: process.env.brandTitle || "ACM Link Shortener", diff --git a/index.js b/index.js index 018d4f8..99a7d51 100644 --- a/index.js +++ b/index.js @@ -12,7 +12,7 @@ const atob = require('atob'); const config = require('./config'); require('dotenv').config() -const {BASE_PROTO} = process.env; +const { BASE_PROTO } = process.env; const baseURL = process.env.BASE_URL; if (!baseURL || !BASE_PROTO) { @@ -50,11 +50,11 @@ const partials = { function getRandomURL() { const length = 6; - let result = ''; - const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; + let result = ''; + const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; const charactersLength = characters.length; - for ( var i = 0; i < length; i++ ) { - result += characters.charAt(Math.floor(Math.random() * charactersLength)); + for (var i = 0; i < length; i++) { + result += characters.charAt(Math.floor(Math.random() * charactersLength)); } return result; } @@ -69,11 +69,11 @@ app.use(session({ // this will be as simple as storing the user ID when serializing, and finding // the user by ID when deserializing. //----------------------------------------------------------------------------- -passport.serializeUser(function(user, done) { +passport.serializeUser(function (user, done) { done(null, user.oid); }); -passport.deserializeUser(function(oid, done) { +passport.deserializeUser(function (oid, done) { findByOid(oid, function (err, user) { done(err, user); }); @@ -82,7 +82,7 @@ passport.deserializeUser(function(oid, done) { // array to hold logged in users var users = []; -var findByOid = function(oid, fn) { +var findByOid = function (oid, fn) { for (var i = 0, len = users.length; i < len; i++) { var user = users[i]; if (user.oid === oid) { @@ -102,45 +102,45 @@ getUserGroups = async (oid, accessToken) => { redirect: 'follow' }; return await fetch(`https://graph.microsoft.com/v1.0/users/${oid}/transitiveMemberOf/microsoft.graph.group?$select=displayName`, requestOptions) - .then(response => response.json()) - .then(result => { - let groups; - let cleanGroups; - try { - groups = result.value; - cleanGroups = groups.map(x => x["displayName"]) - return cleanGroups - } catch (e) { - console.error(e); - return []; - } - - }) - .catch(error => console.log('error', error)); + .then(response => response.json()) + .then(result => { + let groups; + let cleanGroups; + try { + groups = result.value; + cleanGroups = groups.map(x => x["displayName"]) + return cleanGroups + } catch (e) { + console.error(e); + return []; + } + + }) + .catch(error => console.log('error', error)); } var gat = ""; passport.use(new OIDCStrategy(config.creds, -function(iss, sub, profile, accessToken, refreshToken, done) { - if (!profile.oid) { - return done(new Error("No oid found"), null); - } - // asynchronous verification, for effect... - process.nextTick(function () { - findByOid(profile.oid, async function(err, user) { - if (err) { - return done(err); - } - gat = accessToken; - profile._json.groups = await getUserGroups(profile.oid, accessToken) - users.push(profile); - return done(null, profile); + function (iss, sub, profile, accessToken, refreshToken, done) { + if (!profile.oid) { + return done(new Error("No oid found"), null); + } + // asynchronous verification, for effect... + process.nextTick(function () { + findByOid(profile.oid, async function (err, user) { + if (err) { + return done(err); + } + gat = accessToken; + profile._json.groups = await getUserGroups(profile.oid, accessToken) + users.push(profile); + return done(null, profile); + }); }); - }); -} + } )); app.use(cookieParser()); -app.use(express.urlencoded({ extended : true })); +app.use(express.urlencoded({ extended: true })); app.use(express.json()) app.use(passport.initialize()); app.use(passport.session()); @@ -150,17 +150,31 @@ function ensureAuthenticated(req, res, next) { if (req.isAuthenticated()) { return next(); } res.redirect('/login'); }; - +function checkIfAdmin(req) { + const userGroups = new Set(req.user._json.groups !== undefined ? req.user._json.groups : []); + const adminGroups = new Set(config.admin_groups); + for (const key of userGroups) { + if (adminGroups.has(key)) { + return true; + } + } + return false; +} +function ensureAdmin(req, res, next) { + if (!req.isAuthenticated()) { return res.redirect("/login"); } + if (checkIfAdmin(req)) { return next() } + return res.redirect('/unauthorized') +}; async function addURLToDB(name, url, email, groups) { - return new Promise(function(resolve, reject) { - db.serialize(function() { + return new Promise(function (resolve, reject) { + db.serialize(function () { const stmt = db.prepare("INSERT INTO urlData (name, url, email, groups) VALUES (?, ?, ?, ?)"); - stmt.run([name, url, email, groups], function(err) { + stmt.run([name, url, email, groups], function (err) { if (err) { reject(err) } else { - resolve({name, url, email}) + resolve({ name, url, email }) } }) }) @@ -168,10 +182,10 @@ async function addURLToDB(name, url, email, groups) { } async function getDataForEmail(email) { - return new Promise(function(resolve, reject) { - db.serialize(function() { + return new Promise(function (resolve, reject) { + db.serialize(function () { const stmt = db.prepare("SELECT * FROM urlData WHERE email=?"); - stmt.all([email], function(err, data) { + stmt.all([email], function (err, data) { if (err) { reject(err) } else { @@ -183,10 +197,10 @@ async function getDataForEmail(email) { } async function getAllLinks() { - return new Promise(function(resolve, reject) { - db.serialize(function() { + return new Promise(function (resolve, reject) { + db.serialize(function () { const stmt = db.prepare("SELECT * FROM urlData"); - stmt.all([], function(err, data) { + stmt.all([], function (err, data) { if (err) { reject(err) } else { @@ -199,18 +213,18 @@ async function getAllLinks() { async function getDelegatedLinks(userGroups) { - return new Promise(function(resolve, reject) { - db.serialize(function() { + return new Promise(function (resolve, reject) { + db.serialize(function () { const stmt = db.prepare("SELECT * FROM urlData;"); - stmt.all([], function(err, allData) { + stmt.all([], function (err, allData) { if (err) { reject(err) } else { allData = allData.map(item => { if (item.groups === null) { - return item; + return item; } - item.groups = item.groups.split(','); + item.groups = item.groups.split(','); return item; }) const data = allData.filter(item => { @@ -219,7 +233,7 @@ async function getDelegatedLinks(userGroups) { compareGroups = item.groups } const mergedArray = userGroups.filter(value => compareGroups.includes(value)); - return mergedArray.length > 0 + return mergedArray.length > 0 }) resolve(data) } @@ -229,10 +243,10 @@ async function getDelegatedLinks(userGroups) { } async function removeURLfromDB(name) { - return new Promise(function(resolve, reject) { - db.serialize(function() { + return new Promise(function (resolve, reject) { + db.serialize(function () { const stmt = db.prepare("DELETE FROM urlData WHERE name=?"); - stmt.run([name], function(err) { + stmt.run([name], function (err) { if (err) { reject(err) } else { @@ -243,10 +257,10 @@ async function removeURLfromDB(name) { }) } async function getRedirectURL(name) { - return new Promise(function(resolve, reject) { - db.serialize(function() { + return new Promise(function (resolve, reject) { + db.serialize(function () { const stmt = db.prepare("SELECT url FROM urlData WHERE name=?"); - stmt.all([name], function(err, data) { + stmt.all([name], function (err, data) { if (err) { reject(err) } else { @@ -257,14 +271,14 @@ async function getRedirectURL(name) { }) } async function updateRecord(name, url) { - return new Promise(function(resolve, reject) { - db.serialize(function() { + return new Promise(function (resolve, reject) { + db.serialize(function () { const stmt = db.prepare("UPDATE urlData SET url=?, name=? WHERE name=?"); - stmt.run([url, name, name], function(err) { + stmt.run([url, name, name], function (err) { if (err) { reject(err) } else { - resolve({name, url}) + resolve({ name, url }) } }) }) @@ -272,9 +286,9 @@ async function updateRecord(name, url) { } app.get('/login', - function(req, res, next) { - passport.authenticate('azuread-openidconnect', - { + function (req, res, next) { + passport.authenticate('azuread-openidconnect', + { response: res, // required resourceURL: config.resourceURL, // optional. Provide a value if you want to specify the resource. customState: 'my_state', // optional. Provide a value if you want to provide custom state value. @@ -283,29 +297,29 @@ app.get('/login', } )(req, res, next); }, - function(req, res) { + function (req, res) { res.redirect('/'); -}); + }); app.get('/error', (req, res) => { res.status(500).send("An error occurred.") }); app.get('/unauthorized', (req, res) => { - return res.status(401).render('unauthorized.html', {partials, productName: config.branding.title, logoPath: config.branding.logoPath, copyrightOwner: config.branding.copyrightOwner, statusURL: config.branding.statusURL, orgHome: config.branding.orgHome,groups: config.groups_permitted.toString().replaceAll(",", "
")}); + return res.status(401).render('unauthorized.html', { partials, productName: config.branding.title, logoPath: config.branding.logoPath, copyrightOwner: config.branding.copyrightOwner, statusURL: config.branding.statusURL, orgHome: config.branding.orgHome, groups: config.groups_permitted.toString().replaceAll(",", "
"), adminGroups: config.admin_groups.toString().replaceAll(",", "
") }); }); // 'GET returnURL' // `passport.authenticate` will try to authenticate the content returned in // query (such as authorization code). If authentication fails, user will be // redirected to '/' (home page); otherwise, it passes to the next middleware. app.get('/auth/openid/return', - function(req, res, next) { - passport.authenticate('azuread-openidconnect', - { + function (req, res, next) { + passport.authenticate('azuread-openidconnect', + { response: res, // required - failureRedirect: '/' + failureRedirect: '/' } )(req, res, next); }, - function(req, res) { + function (req, res) { res.redirect('/'); }); @@ -314,24 +328,24 @@ app.get('/auth/openid/return', // body (such as authorization code). If authentication fails, user will be // redirected to '/' (home page); otherwise, it passes to the next middleware. app.post('/auth/openid/return', - function(req, res, next) { - passport.authenticate('azuread-openidconnect', - { + function (req, res, next) { + passport.authenticate('azuread-openidconnect', + { response: res, // required - failureRedirect: '/' + failureRedirect: '/' } )(req, res, next); }, - function(req, res) { + function (req, res) { res.redirect('/create'); }); // 'logout' route, logout from passport, and destroy the session with AAD. -app.get('/logout', function(req, res){ - res.clearCookie('connect.sid', {path:'/'}); - res.clearCookie('session', {path:'/'}); - res.clearCookie('session.sig', {path:'/'}); - req.session=null; +app.get('/logout', function (req, res) { + res.clearCookie('connect.sid', { path: '/' }); + res.clearCookie('session', { path: '/' }); + res.clearCookie('session.sig', { path: '/' }); + req.session = null; res.redirect('/'); }); @@ -346,36 +360,40 @@ function validateArray(userGroups, accessGroups) { // group access check app.use(async (req, res, next) => { - if (!req.user) {return next();} + if (!req.user) { return next(); } req.user._json.groups = await getUserGroups(req.user.oid, gat); const intserect = validateArray(config.groups_permitted, req.user._json.groups); const intersect2 = validateArray(config.admin_groups, req.user._json.groups) - if (!intserect && !intersect2){ + if (!intserect && !intersect2) { return res.status(401).redirect("/unauthorized"); } next(); }) -app.use('/admin/', async (req, res, next) => { - if (!req.user) {return next();} - req.user._json.groups = await getUserGroups(req.user.oid, gat); - const intersect2 = validateArray(config.admin_groups, req.user._json.groups) - if (!intersect2){ - return res.status(401).redirect("/unauthorized"); - } - next(); -}) +app.use('/admin/', ensureAdmin) // begin business logic app.get('/', async function (req, res) { - + if (req.isAuthenticated()) { return res.redirect('/create') } - res.render('home.html', {partials, productName: config.branding.title, logoPath: config.branding.logoPath, copyrightOwner: config.branding.copyrightOwner, statusURL: config.branding.statusURL, orgHome: config.branding.orgHome,loginProvider: config.branding.loginProvider}); + res.render('home.html', { partials, productName: config.branding.title, logoPath: config.branding.logoPath, copyrightOwner: config.branding.copyrightOwner, statusURL: config.branding.statusURL, orgHome: config.branding.orgHome, loginProvider: config.branding.loginProvider }); return }) app.get('/create', ensureAuthenticated, async function (req, res) { - res.render('index.html', {partials, productName: config.branding.title, logoPath: config.branding.logoPath, copyrightOwner: config.branding.copyrightOwner, statusURL: config.branding.statusURL, orgHome: config.branding.orgHome,email: req.user._json.preferred_username, name: req.user.displayName, baseURL, userGroups: req.user._json.groups !== undefined ? req.user._json.groups.map((item) => {return {group: item}}) : {}}) + res.render('index.html', { + partials, + productName: config.branding.title, + logoPath: config.branding.logoPath, + copyrightOwner: config.branding.copyrightOwner, + statusURL: config.branding.statusURL, + orgHome: config.branding.orgHome, + email: req.user._json.preferred_username, + name: req.user.displayName, + baseURL, + userGroups: req.user._json.groups !== undefined ? req.user._json.groups.map((item) => { return { group: item } }) : {}, + isAdminUser: checkIfAdmin(req) + }) return }) @@ -384,7 +402,7 @@ app.post('/addURL', ensureAuthenticated, async function (req, res) { const url = req.query.url; const name = req.query.name; const groups = req.body.groups - if (url.indexOf(baseURL) > -1 ) { + if (url.indexOf(baseURL) > -1) { res.json({ message: `The origin URL cannot be a path of ${baseURL}` }) @@ -422,15 +440,15 @@ app.post('/addURL', ensureAuthenticated, async function (req, res) { app.get('/mylinks', ensureAuthenticated, async function (req, res) { const email = req.user._json.preferred_username; const name = req.user.displayName; - const userGroups = req.user._json.groups !== undefined ? req.user._json.groups : []; - let data = await getDataForEmail(email).catch(() => {res.status(500).render('500', {productName: config.branding.title, logoPath: config.branding.logoPath, copyrightOwner: config.branding.copyrightOwner, statusURL: config.branding.statusURL,}); return}); + const userGroups = req.user._json.groups !== undefined ? req.user._json.groups : []; + let data = await getDataForEmail(email).catch(() => { res.status(500).render('500', { productName: config.branding.title, logoPath: config.branding.logoPath, copyrightOwner: config.branding.copyrightOwner, statusURL: config.branding.statusURL, }); return }); data = data.map((item) => { const d = item; d.url = atob(d.url); d.groups = d.groups.replace(',', "
") return d; }) - let delegatedLinks = await getDelegatedLinks(userGroups).catch(() => {res.status(500).render('500', {productName: config.branding.title, logoPath: config.branding.logoPath, copyrightOwner: config.branding.copyrightOwner, statusURL: config.branding.statusURL,}); return}); + let delegatedLinks = await getDelegatedLinks(userGroups).catch(() => { res.status(500).render('500', { productName: config.branding.title, logoPath: config.branding.logoPath, copyrightOwner: config.branding.copyrightOwner, statusURL: config.branding.statusURL, }); return }); delegatedLinks = delegatedLinks.map((item) => { const d = item; d.url = atob(d.url); @@ -439,7 +457,7 @@ app.get('/mylinks', ensureAuthenticated, async function (req, res) { delegatedLinks = delegatedLinks.filter(word => word.email != email); res.render('mylinks', { partials, - productName: config.branding.title, + productName: config.branding.title, logoPath: config.branding.logoPath, copyrightOwner: config.branding.copyrightOwner, statusURL: config.branding.statusURL, @@ -449,15 +467,16 @@ app.get('/mylinks', ensureAuthenticated, async function (req, res) { email, baseURL, delegatedLinks, - productName: config.branding.title + productName: config.branding.title, + isAdminUser: checkIfAdmin(req) }) }) app.get('/admin/links', ensureAuthenticated, async function (req, res) { const email = req.user._json.preferred_username; const name = req.user.displayName; - const userGroups = req.user._json.groups !== undefined ? req.user._json.groups : []; - let data = await getAllLinks().catch(() => {res.status(500).render('500', {productName: config.branding.title, logoPath: config.branding.logoPath, copyrightOwner: config.branding.copyrightOwner, statusURL: config.branding.statusURL,}); return}); + const userGroups = req.user._json.groups !== undefined ? req.user._json.groups : []; + let data = await getAllLinks().catch(() => { res.status(500).render('500', { productName: config.branding.title, logoPath: config.branding.logoPath, copyrightOwner: config.branding.copyrightOwner, statusURL: config.branding.statusURL, }); return }); data = data.map((item) => { const d = item; d.url = atob(d.url); @@ -466,7 +485,7 @@ app.get('/admin/links', ensureAuthenticated, async function (req, res) { }) res.render('adminlinks', { partials, - productName: config.branding.title, + productName: config.branding.title, logoPath: config.branding.logoPath, copyrightOwner: config.branding.copyrightOwner, statusURL: config.branding.statusURL, @@ -475,7 +494,8 @@ app.get('/admin/links', ensureAuthenticated, async function (req, res) { name, email, baseURL, - productName: config.branding.title + productName: config.branding.title, + isAdminUser: checkIfAdmin(req) }) }) @@ -496,7 +516,7 @@ app.delete('/deleteLink', ensureAuthenticated, async function (req, res) { app.put('/updateLink', ensureAuthenticated, async function (req, res) { const name = req.query.name; const url = req.query.url; - if (url.indexOf(baseURL) > -1 ) { + if (url.indexOf(baseURL) > -1) { res.json({ message: `The origin URL cannot be a path of ${baseURL}` }) @@ -516,7 +536,7 @@ app.get('/getRandomURL', ensureAuthenticated, async function (req, res) { let exists = true; let generatedURL = ''; let i = 0; - while(exists) { + while (exists) { try { generatedURL = getRandomURL(); const url = await getRedirectURL(generatedURL); @@ -525,18 +545,18 @@ app.get('/getRandomURL', ensureAuthenticated, async function (req, res) { throw new Error("In a generation loop, must exit.") } } catch { - res.status(500).json({success: false}) + res.status(500).json({ success: false }) return } } try { if (generatedURL !== '') { - res.json({success: true, generatedURL}) + res.json({ success: true, generatedURL }) return } throw new Error("Did not actually generate a new URL.") } catch { - res.status(500).json({success: false}) + res.status(500).json({ success: false }) return } }) @@ -549,11 +569,11 @@ app.get('/:id', async function (req, res) { res.redirect(atob(url[0].url)) return } else { - res.status(404).render('404', {partials, productName: config.branding.title, logoPath: config.branding.logoPath, copyrightOwner: config.branding.copyrightOwner, statusURL: config.branding.statusURL,}) + res.status(404).render('404', { partials, productName: config.branding.title, logoPath: config.branding.logoPath, copyrightOwner: config.branding.copyrightOwner, statusURL: config.branding.statusURL, }) return } } catch { - res.status(500).render('500', {partials, productName: config.branding.title, logoPath: config.branding.logoPath, copyrightOwner: config.branding.copyrightOwner, statusURL: config.branding.statusURL,}) + res.status(500).render('500', { partials, productName: config.branding.title, logoPath: config.branding.logoPath, copyrightOwner: config.branding.copyrightOwner, statusURL: config.branding.statusURL, }) return } diff --git a/view/components/footer.html b/view/components/footer.html index 0419c8c..00b28f4 100644 --- a/view/components/footer.html +++ b/view/components/footer.html @@ -1,5 +1,5 @@
-