Add start of doorlock server

This commit is contained in:
Dana Woodman
2018-07-10 14:47:07 -07:00
parent 8c83229696
commit 9c11a0506a
24 changed files with 4700 additions and 6697 deletions

60
src/models/cards.js Normal file
View File

@@ -0,0 +1,60 @@
const fs = require('fs')
const path = require('path')
const CARDS_PATH = path.join(process.cwd(), 'cards.json')
module.exports = class Cards {
static all() {
return new Promise((resolve, reject) => {
fs.readFile(CARDS_PATH, (err, data) => {
if (err) return reject(err)
resolve(JSON.parse(data))
})
})
}
static write(cards) {
const json = JSON.stringify(this.sortByName(cards))
return new Promise((resolve, reject) => {
fs.writeFile(CARDS_PATH, json, err => {
if (err) return reject(err)
resolve()
})
})
}
static validate(number) {
return this.all().then(cards => cards.find(c => c.number === number))
// console.log(':', JSON.stringify(number.toString().trim()))
// const scanned = parseInt(
// number
// .toString('hex')
// .trim() // Remove any whiespace or newlines
// .replace('\u0003', '') // Remove "end of text" character
// .replace('\u0002', '') // Remove "start of text" character
// .substring(3) // Strip off con
// .slice(0, -2), // Strip off checksum
// 16
// )
// this.log('Scanned card:', scanned)
// return this.readCardsFromSDCard().then(cards => {
// const card = cards.find(c => parseInt(c.number) === scanned)
// if (card) {
// const name = card.name.split(' ')[0]
// this.log(`Welcome in ${name}!`, scanned)
// this.openDoor()
// } else {
// this.log('Card is invalid:', scanned)
// }
// })
}
static sortByName(cards) {
return cards.sort(
(a, b) => (a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1)
)
}
}

165
src/models/cobot.js Normal file
View File

@@ -0,0 +1,165 @@
const https = require('https')
const {
CARD_UPDATE_INTERVAL,
COBOT_CARDS_API,
COBOT_CLIENT_ID,
COBOT_CLIENT_SECRET,
COBOT_SCOPE,
COBOT_USER_EMAIL,
COBOT_USER_PASSWORD,
} = require('./constants')
module.exports = class Cobot {
constructor(token) {
this.token = token
}
cards() {
if (!COBOT_CARDS_API)
throw new Error('missing "COBOT_CARDS_API" env variable!')
return new Promise((resolve, reject) => {
// TODO move to axios
const req = https.request(
{
headers: {
Authorization: `Bearer ${this.token}`,
},
hostname: 'chimera.cobot.me',
method: 'GET',
path: '/api/check_in_tokens',
},
res => {
const { statusCode, headers } = res
console.log('\n----------------------------------------------------')
console.log('COBOT CARDS RESPONSE:')
console.log(JSON.stringify({ statusCode, headers }, null, 2))
res.setEncoding('utf8')
let cards = ''
res.on('data', chunk => {
cards += chunk
})
res.on('end', () => {
cards = JSON.parse(cards)
console.log(JSON.stringify(cards, null, 2))
if (!cards || !cards.length) {
throw new Error('No cards received from API!')
}
resolve(
cards.map(card => ({
name: card.membership.name,
number: card.token,
}))
)
console.log(
'----------------------------------------------------\n'
)
})
}
)
req.on('data', console.log)
req.on('error', e => {
console.error(e)
reject(e)
})
req.end()
})
// return axios
// .get(COBOT_CARDS_API, {
// headers: {
// Authorization: `Bearer ${this.token}`,
// },
// })
// .then(resp =>
// resp.data.map(card => ({
// name: card.membership.name,
// number: card.token,
// }))
// )
}
static authorize() {
console.log('Authorizing Cobot application...')
if (!COBOT_SCOPE) throw new Error('missing "COBOT_SCOPE" env variable!')
if (!COBOT_USER_EMAIL)
throw new Error('missing "COBOT_USER_EMAIL" env variable!')
if (!COBOT_USER_PASSWORD)
throw new Error('missing "COBOT_USER_PASSWORD" env variable!')
if (!COBOT_CLIENT_ID)
throw new Error('missing "COBOT_CLIENT_ID" env variable!')
if (!COBOT_CLIENT_SECRET)
throw new Error('missing "COBOT_CLIENT_SECRET" env variable!')
const qs = [
`scope=${COBOT_SCOPE}`,
`grant_type=password`,
`username=${COBOT_USER_EMAIL}`,
`password=${encodeURI(COBOT_USER_PASSWORD)}`,
`client_id=${COBOT_CLIENT_ID}`,
`client_secret=${COBOT_CLIENT_SECRET}`,
].join('&')
return new Promise((resolve, reject) => {
const req = https.request(
{
hostname: 'www.cobot.me',
method: 'POST',
path: `/oauth/access_token?${qs}`,
},
res => {
const { statusCode, headers } = res
console.log('\n----------------------------------------------------')
console.log('COBOT AUTHORIZATION RESPONSE:')
console.log(JSON.stringify({ statusCode, headers }, null, 2))
res.setEncoding('utf8')
let cobot
res.on('data', chunk => {
const body = JSON.parse(chunk)
console.log(JSON.stringify({ body }, null, 2))
const token = body.access_token
if (!token) {
console.error(`Expected access token, got: ${body}`)
throw new Error('Error getting access token')
}
cobot = new Cobot(token)
})
res.on('end', () => {
resolve(cobot)
console.log(
'----------------------------------------------------\n'
)
})
}
)
req.on('error', e => reject(e))
req.end()
})
// return axios
// .post(`https://www.cobot.me/oauth/access_token?${qs}`)
// .then(resp => new Cobot(resp.data.access_token))
}
static getCards() {
this.log('Updating cards...')
this.authorize()
.then(cobot => cobot.cards())
.then(cards => {
this.log('UPDATED CARDS:', cards.length, 'cards')
this.writeCardsToSDCard(cards)
this.cards = cards
})
.then(() => {
this.log(
'Updating card list in',
CARD_UPDATE_INTERVAL / 1000,
'seconds...'
)
setTimeout(this.fetchCardListFromCobot.bind(this), CARD_UPDATE_INTERVAL)
})
.catch(this.logErrorMessage)
}
}

9
src/models/door.js Normal file
View File

@@ -0,0 +1,9 @@
module.exports = class Door {
static open() {
console.log('OPEN')
}
static close() {
console.log('CLOSE')
}
}

14
src/models/logs.js Normal file
View File

@@ -0,0 +1,14 @@
module.exports = class Logs {
static all() {
return Promise.all([
{
timestamp: 1531256719431,
card: { name: 'John Smith', number: '1234023423423' },
},
{
timestamp: 1531256756227,
card: { name: 'Jane Doe', number: '2394723984752983' },
},
])
}
}

5
src/routes/cards.js Normal file
View File

@@ -0,0 +1,5 @@
const Cards = require('../models/cards')
module.exports = (req, res) => {
Cards.all().then(cards => res.render('cards', { cards }))
}

16
src/routes/checkin.js Normal file
View File

@@ -0,0 +1,16 @@
const Cards = require('../models/cards')
module.exports = (req, res) => {
const rfid = req.body.rfid.trim().toLowerCase()
console.log('SCANNED CARD:', rfid)
Cards.validate(rfid).then(card => {
console.log('CARD:', card)
// TODO: add to log if success
if (card) {
res.redirect('/success?name=' + card.name)
} else {
res.redirect('/failure')
}
})
}

3
src/routes/failure.js Normal file
View File

@@ -0,0 +1,3 @@
module.exports = (req, res) => {
res.render('failure', {})
}

5
src/routes/logs.js Normal file
View File

@@ -0,0 +1,5 @@
const Logs = require('../models/logs')
module.exports = (req, res) => {
Logs.all().then(logs => res.render('logs', { logs }))
}

4
src/routes/success.js Normal file
View File

@@ -0,0 +1,4 @@
module.exports = (req, res) => {
const name = req.query.name
res.render('success', { name })
}

42
src/server.js Normal file
View File

@@ -0,0 +1,42 @@
const bodyParser = require('body-parser')
const express = require('express')
const helmet = require('helmet')
const moment = require('moment')
const path = require('path')
const app = express()
const PORT = process.env.PORT || 3000
//---------------------------------------------------------
// Middleware
//---------------------------------------------------------
// View engine setup
app.set('views', path.join(process.cwd(), 'src', 'views'))
app.set('view engine', 'pug')
// Handle form body content
app.use(bodyParser.urlencoded({ extended: true }))
// Handle static assets in /public
app.use(express.static(path.join(process.cwd(), 'public')))
// Security middleware
app.use(helmet())
// Allow moment to be used in templates
app.locals.moment = moment
//---------------------------------------------------------
// Routes
//---------------------------------------------------------
app.post('/checkin', require('./routes/checkin'))
app.get('/success', require('./routes/success'))
app.get('/failure', require('./routes/failure'))
app.get('/cards', require('./routes/cards'))
app.get('/logs', require('./routes/logs'))
app.get('/', (req, res) => res.render('home', {}))
app.listen(PORT, () => console.log('Example app listening on port 3000!'))

18
src/views/cards.pug Normal file
View File

@@ -0,0 +1,18 @@
extends ./layout.pug
block title
| Cards
block content
h1.page-heading Cards
table.collapse.w-100
thead
tr
th.px-md.py-sm.bb.bc-gray-lighter.gray-light.fw-1 Member Name
th.px-md.py-sm.bb.bc-gray-lighter.gray-light.fw-1 RFID Card Number
tbody
for card in cards
tr.hov-bg-gray-lightest
td.px-md.py-sm.bb.bc-gray-lighter= card.name
td.px-md.py-sm.bb.bc-gray-lighter= card.number

19
src/views/failure.pug Normal file
View File

@@ -0,0 +1,19 @@
extends ./layout.pug
block title
| Scanned Failed!
block content
.df.ai-center.jc-center.bg-danger.white.px-lg.py-md
i.fas.fa-times.mr-lg.txt-xxl
.f-1
.txt-xl Card not valid
.mt-sm.danger-lighter Sorry your card isn't valid. Maybe your membership has expired?
p
a(href='/') ← Back
script.
//- setTimeout(function () { window.location.href = '/' }, 6000)

9
src/views/home.pug Normal file
View File

@@ -0,0 +1,9 @@
extends ./layout.pug
block title
| Scan Card
block content
h1.page-heading Scan Your Card
form(action='/checkin' method='post')
input#rfid.input.input-block(name="rfid" autofocus)

23
src/views/layout.pug Normal file
View File

@@ -0,0 +1,23 @@
doctype html
html(lang='en')
head
title
block title
| - Chimera Doorlock
link(href='https://fonts.googleapis.com/css?family=Roboto:100,400,700' rel='stylesheet')
link(href='//unpkg.com/ress/dist/ress.min.css' rel='stylesheet' type='text/css')
link(href='//unpkg.com/euphoria/dist/euphoria.min.css' rel='stylesheet' type='text/css')
link(href='https://use.fontawesome.com/releases/v5.0.13/css/all.css' rel='stylesheet' integrity='sha384-DNOHZ68U8hZfKXOrtjWvjxusGo9WQnrNx2sqG0tfsghAvtVlRW3tvkXWZh58N9jp' crossorigin='anonymous')
link(href='/styles.css' rel='stylesheet')
body.p-none.m-none.ff-sans-serif
header.df.p-md.gray.bb.bc-gray-lighter
h1.txt-lg.fw-1.ls-lg.upper
a(href='/').gray-light Chimera Doorlock
.ml-auto
a.ml-md(href='/') Scan
a.ml-md(href='/cards') Cards
a.ml-md(href='/logs') Logs
section.mx-auto.my-md.p-md(style='max-width: 900px;')
block content

21
src/views/logs.pug Normal file
View File

@@ -0,0 +1,21 @@
extends ./layout.pug
block title
| Logs
block content
h1.page-heading Logs
table.collapse.w-100
thead
tr
th.px-md.py-sm.bb.bc-gray-lighter.gray-light.fw-1 Date
th.px-md.py-sm.bb.bc-gray-lighter.gray-light.fw-1 Member Name
th.px-md.py-sm.bb.bc-gray-lighter.gray-light.fw-1 RFID Card Number
tbody
for log in logs
- date = new Date(log.timestamp)
tr.hov-bg-gray-lightest
td.px-md.py-sm.bb.bc-gray-lighter.c-help(title=date)= moment(date).fromNow()
td.px-md.py-sm.bb.bc-gray-lighter= log.card.name
td.px-md.py-sm.bb.bc-gray-lighter= log.card.number

18
src/views/success.pug Normal file
View File

@@ -0,0 +1,18 @@
extends ./layout.pug
block title
| Scanned Successfully!
block content
.df.ai-center.jc-center.bg-success.white.px-lg.py-md
i.fas.fa-check.mr-lg.txt-xxl
.f-1
.txt-xl Welcome in #{name}!
.mt-sm.success-lighter Have an awesome day of making!
p
a(href='/') ← Back
script.
//- setTimeout(function () { window.location.href = '/' }, 6000)