Skip to content

Commit

Permalink
add basic dashboard
Browse files Browse the repository at this point in the history
  • Loading branch information
pkage committed Oct 20, 2020
1 parent 41e89ae commit 8d312c7
Show file tree
Hide file tree
Showing 9 changed files with 2,293 additions and 13 deletions.
1,791 changes: 1,791 additions & 0 deletions package-lock.json

Large diffs are not rendered by default.

6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,12 @@
"author": "patrick kage",
"license": "MIT",
"dependencies": {
"ejs": "^3.1.5",
"express": "^4.17.1",
"express-session": "^1.17.1",
"luxon": "^1.19.3",
"nightmare": "^3.0.2"
"nightmare": "^3.0.2",
"passport": "^0.4.1",
"passport-google-oauth20": "^2.0.0"
}
}
145 changes: 133 additions & 12 deletions server.js
Original file line number Diff line number Diff line change
@@ -1,21 +1,29 @@
const express = require('express')
const scrape_members = require('./scrape.js')
const fs = require('fs')
const fsAsync = require('fs').promises
const fsAsync = require('fs').promises
const { DateTime } = require('luxon')

// authentication
const passport = require('passport')
const GoogleStrategy = require('passport-google-oauth20').Strategy
const session = require('express-session')

// some secrets
const readJSON = file => JSON.parse(fs.readFileSync(file))
const config = readJSON('./instance/config.json')
const secrets = readJSON('./instance/secret.json')

const app = express()
const cachefile = config.cachefile
const port = config.port
const orgID = config.orgID
const groupID = config.groupID
const apikey = secrets.apikey
/* --- API SUB-APPLICATION --- */

const api_app = express()
const cachefile = config.cachefile
const port = config.port
const orgID = config.orgID
const groupID = config.groupID
const apikey = secrets.apikey

const authenticationMiddleware = (req, res, next) => {
const apiAuthenticationMiddleware = (req, res, next) => {

const expected_header = `Bearer ${apikey}`
if (!req.headers.authorization || req.headers.authorization !== expected_header) {
Expand All @@ -30,7 +38,7 @@ const authenticationMiddleware = (req, res, next) => {
}
}

app.use(authenticationMiddleware)
api_app.use(apiAuthenticationMiddleware)

const writeScrape = async () => {
const members = await scrape_members({
Expand All @@ -56,23 +64,136 @@ const writeScrape = async () => {
}

const readScrape = async () => JSON.parse(await fsAsync.readFile(cachefile))
app.get('/api/members', async (req, res) => {
api_app.get('/members', async (req, res) => {
try {
res.json({ success: true, ...(await readScrape())})
} catch(e) {
res.json({ success: false, status:e.toString()})
}
})

app.get('/api/refresh', async (req, res) => {
api_app.get('/refresh', async (req, res) => {
try{
res.json({ success: true, ...(await writeScrape())})
} catch(e) {
res.json({ success: false, status:e.toString()})
}
})

api_app.get('/sendy_sync', async (req, res) => {

})

/* --- FRONTEND --- */

const app = express()
app.set('view engine', 'ejs')
app.use('/static', express.static('static'))
app.use(session({ secret: 'anything', resave: false, saveUninitialized: false }));


// google auth plumbing
app.use(passport.initialize())
app.use(passport.session());
passport.use(
new GoogleStrategy({
clientID: secrets.google.client_id,
clientSecret: secrets.google.client_secret,
callbackURL: secrets.google.callback
},
(access_token, refresh_token, profile, cb) => {

// simplify profile response
let lensed = profile => ({
id: profile.id,
name: profile.displayName,
email: profile.emails[0].value,
email_verified: profile.emails[0].verified,
photo: profile.photos[0]?.value
})

cb(null, lensed(profile))
})
)

// serialize the profile directly into the session (d i r t y)
passport.serializeUser((user, done) => done(null, JSON.stringify(user)))
passport.deserializeUser((user, done) => done(null, JSON.parse(user)))

app.get('/auth/google',
passport.authenticate('google', { scope: ['profile', 'email'] }));
app.get('/auth/google/callback',
passport.authenticate('google', {failureRedirect: '/auth/failed'}),
(req, res) => {
// successful authentication
res.redirect('/dashboard')
}
)
app.get('/auth/failed', (req, res) => {
res.send('auth failed!')
})
app.get('/auth/logout', (req, res) => {
req.logout()
res.redirect('/')
})

const login_guard = (req, res, next) => {
if (!req.user) {
res.redirect('/')
} else {
next()
}
}


// homepage
app.get('/', (req, res) => {
res.render('index')
})

// dashboard
app.get('/dashboard',
login_guard,
async (req, res) => {
const latest = await readScrape()

const makecsv = members => {
const data = members.map(
m => `${m.name.first},${m.name.last},${m.student},s${m.student}@ed.ac.uk,${m.joined},${m.expires}`
).join('\n')

return `First,Last,StudentNo,Email,Joined,Expires\n` + data
}

res.render('dash', {
latest,
csv: makecsv(latest.members),
render_time: new Date().toISOString(),
apikey: apikey,
user: req.user
})
}
)
app.get('/dashboard/rescrape',
login_guard,
(req, res) => {
res.render('rescrape', {
apikey,
user: req.user
})
}
)



// mount the api application
app.use('/api', api_app)



console.log('getting initial scrape...')
writeScrape()

const dummy = async () => {}
//writeScrape()
dummy()
.then(() => app.listen(port, () => console.log(`EUSA members api listening on port ${port}!`)))
Binary file added static/compsoc-icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
27 changes: 27 additions & 0 deletions static/dashboard.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
window.onload = () => {
document.querySelectorAll('[data-selects]')
.forEach(el => {
el.addEventListener('click', () => {
const target = document.querySelector(`#${el.dataset.selects}`)

const range = document.createRange()
range.selectNode(target)
window.getSelection().removeAllRanges()
window.getSelection().addRange(range)
})
})

document.querySelectorAll('[data-copies]')
.forEach(el => {
el.addEventListener('click', () => {
const target = document.querySelector(`#${el.dataset.copies}`)

const range = document.createRange()
range.selectNode(target)
window.getSelection().removeAllRanges()
window.getSelection().addRange(range)

document.execCommand("copy");
})
})
}
131 changes: 131 additions & 0 deletions static/index.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
@import url('https://fonts.googleapis.com/css2?family=Commissioner:wght@400;700&display=swap');
@import url('https://fonts.googleapis.com/css2?family=PT+Mono&display=swap');

:root {
--accent: #1A659E; /* sapphire blue */
--accent2: #0E3858; /* prussian blue */
}

body {
font-family: "Commissioner", -apple-system, BlinkMacSystemFont, avenir next, avenir, helvetica neue, helvetica, Ubuntu, roboto, noto, segoe ui, arial, sans-serif;
color: #444;
background-color: #FFFFFA;

display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: center;
}

a {
text-decoration: none;
color: var(--accent);
}
a:hover {
color: var(--accent2);
}
code {
padding: 5px;
display: block;
word-break: break-word;
background-color: #ddd;
}

code.inline {
display: inline-block;
}

pre {
margin: 0;
}

.numeric, .mono {
font-family: 'PT Mono', Menlo, Consolas, Monaco, Liberation Mono, Lucida Console, monospace;
}

/* --- NAVIGATION --- */

nav {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;

align-self: stretch;
}

nav img {
height: 30px;
width: 30px;
}

.nav__brand, .nav__login {
display: flex;
flex-direction: row;
align-items: center;
}

.nav__title, .nav__name {
margin: 0 1ch;
}

.nav__login {
position: relative;
cursor: pointer;
}

.nav__login:hover > .nav__logout {
opacity: 0.95;
}
.nav__logout {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: white;

display: flex;
flex-direction: column;
align-items: center;
justify-content: center;

transition: opacity 0.25s cubic-bezier(0,0,0.3,1);
opacity: 0;
}


/* --- layout --- */

@media (min-width: 768px) {
main {
width: 80ch;
}
}

table {
width: 100%;
}

table th {
text-align: left;
}

.spinner {
animation: spin 1s cubic-bezier(1,0,0,1) forwards infinite;
width: 50px;
height: 50px;
border-radius: 50%;
border: 1px solid black;
border-top: 1px solid transparent;
}

@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}

Loading

0 comments on commit 8d312c7

Please sign in to comment.