mirror of
https://github.com/zyphlar/doorlock.git
synced 2024-04-03 21:36:03 +00:00
Add start of doorlock server
This commit is contained in:
parent
8c83229696
commit
9c11a0506a
|
@ -23,9 +23,4 @@ rules:
|
||||||
semi:
|
semi:
|
||||||
- error
|
- error
|
||||||
- never
|
- never
|
||||||
sort-vars: error
|
|
||||||
sort-imports:
|
|
||||||
- error
|
|
||||||
- ignoreCase: true
|
|
||||||
sort-keys: error
|
|
||||||
react/jsx-sort-prop: true
|
react/jsx-sort-prop: true
|
||||||
|
|
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -1,3 +1,5 @@
|
||||||
|
cards.json
|
||||||
|
logs.json
|
||||||
|
|
||||||
.env
|
.env
|
||||||
node_modules
|
node_modules
|
||||||
|
|
|
@ -1,7 +0,0 @@
|
||||||
# This file should list any files or directories you want included with the
|
|
||||||
# T2 bundling and deployment process. This is handy for including
|
|
||||||
# non-JavaScript assets, like HTML, CSS, and images, for use within your project.
|
|
||||||
# .tesselinclude works the same as .npminclude
|
|
||||||
# You DO NOT need to list node_modules or package.json
|
|
||||||
# For more information, visit: https://tessel.io/docs/cli#usage
|
|
||||||
.env
|
|
|
@ -1,17 +0,0 @@
|
||||||
function LED() {
|
|
||||||
return {
|
|
||||||
off: jest.fn(),
|
|
||||||
on: jest.fn(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const tessel = {
|
|
||||||
led: {
|
|
||||||
0: LED(), // red
|
|
||||||
1: LED(), // amber
|
|
||||||
2: LED(), // green
|
|
||||||
3: LED(), // blue
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = tessel
|
|
10811
package-lock.json
generated
10811
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
15
package.json
15
package.json
|
@ -4,24 +4,29 @@
|
||||||
"main": "doorlock.js",
|
"main": "doorlock.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"deploy": "t2 run doorlock.js",
|
"deploy": "t2 run doorlock.js",
|
||||||
"start": "node doorlock.js",
|
"start": "forever start src/server.js",
|
||||||
"test": "jest",
|
"test": "jest",
|
||||||
|
"watch": "nodemon src/server.js",
|
||||||
"watch-test": "npm test -- --watch"
|
"watch-test": "npm test -- --watch"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "0.18.0",
|
"axios": "0.18.0",
|
||||||
|
"body-parser": "1.18.3",
|
||||||
"chalk": "2.3.1",
|
"chalk": "2.3.1",
|
||||||
"dotenv": "5.0.1",
|
"dotenv": "5.0.1",
|
||||||
"serialport": "6.2.0",
|
"express": "4.16.3",
|
||||||
"tessel": "0.3.25"
|
"helmet": "3.12.1",
|
||||||
|
"moment": "2.22.2",
|
||||||
|
"pug": "2.0.3",
|
||||||
|
"serialport": "6.2.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"babel-eslint": "8.2.2",
|
"babel-eslint": "8.2.2",
|
||||||
"eslint": "4.19.1",
|
"eslint": "4.19.1",
|
||||||
"eslint-plugin-react": "7.7.0",
|
"eslint-plugin-react": "7.7.0",
|
||||||
|
"forever": "0.15.3",
|
||||||
"jest": "22.4.2",
|
"jest": "22.4.2",
|
||||||
"nodemon": "1.17.5",
|
"nodemon": "1.18.0"
|
||||||
"t2-cli": "0.1.18"
|
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"fsevents": "*"
|
"fsevents": "*"
|
||||||
|
|
107
public/styles.css
Normal file
107
public/styles.css
Normal file
|
@ -0,0 +1,107 @@
|
||||||
|
/*------------------------------------------------------
|
||||||
|
- Variables
|
||||||
|
------------------------------------------------------*/
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--font-family-sans-serif: Roboto, sans-serif;
|
||||||
|
|
||||||
|
--color-primary: #2aa5c1;
|
||||||
|
--color-primary-dark: #1e8096;
|
||||||
|
--color-secondary: #2ac17a;
|
||||||
|
--color-secondary-dark: #1d915a;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*------------------------------------------------------
|
||||||
|
- Base styles
|
||||||
|
------------------------------------------------------*/
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
color: var(--color-primary);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
a:hover {
|
||||||
|
color: var(--color-primary-darker);
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
line-height: var(--line-height-md);
|
||||||
|
margin: var(--spacing-md) 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*------------------------------------------------------
|
||||||
|
- Buttons
|
||||||
|
------------------------------------------------------*/
|
||||||
|
|
||||||
|
.button {
|
||||||
|
background: var(--color-primary);
|
||||||
|
border: 1px solid var(--color-primary);
|
||||||
|
color: white;
|
||||||
|
display: inline-block;
|
||||||
|
padding: var(--spacing-sm) var(--spacing-md);
|
||||||
|
}
|
||||||
|
.button:hover {
|
||||||
|
background: var(--color-primary-dark);
|
||||||
|
border-color: var(--color-primary-dark);
|
||||||
|
}
|
||||||
|
.button-danger {
|
||||||
|
background: var(--color-danger);
|
||||||
|
border-color: var(--color-danger);
|
||||||
|
}
|
||||||
|
.button-danger:hover {
|
||||||
|
background: var(--color-danger-dark);
|
||||||
|
border-color: var(--color-danger-dark);
|
||||||
|
}
|
||||||
|
.button-info {
|
||||||
|
background: var(--color-info);
|
||||||
|
border-color: var(--color-info);
|
||||||
|
}
|
||||||
|
.button-info:hover {
|
||||||
|
background: var(--color-info-dark);
|
||||||
|
border-color: var(--color-info-dark);
|
||||||
|
}
|
||||||
|
.button-outline {
|
||||||
|
color: var(--color-primary);
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
.button-outline:hover {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.button-sm {
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
padding: var(--spacing-xs) var(--spacing-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*------------------------------------------------------
|
||||||
|
- Forms
|
||||||
|
------------------------------------------------------*/
|
||||||
|
|
||||||
|
.label {
|
||||||
|
display: block;
|
||||||
|
font-weight: bold;
|
||||||
|
padding: var(--spacing-md) 0 var(--spacing-sm) 0;
|
||||||
|
}
|
||||||
|
.input {
|
||||||
|
background: white;
|
||||||
|
border: 1px solid var(--color-gray-light);
|
||||||
|
padding: var(--spacing-sm);
|
||||||
|
}
|
||||||
|
.input-block {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*------------------------------------------------------
|
||||||
|
- Page heading
|
||||||
|
------------------------------------------------------*/
|
||||||
|
|
||||||
|
.page-heading {
|
||||||
|
color: var(--color-gray-light);
|
||||||
|
margin-bottom: var(--spacing-lg);
|
||||||
|
font-size: var(--font-size-xl);
|
||||||
|
font-weight: 100;
|
||||||
|
}
|
60
src/models/cards.js
Normal file
60
src/models/cards.js
Normal 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
165
src/models/cobot.js
Normal 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
9
src/models/door.js
Normal 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
14
src/models/logs.js
Normal 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
5
src/routes/cards.js
Normal 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
16
src/routes/checkin.js
Normal 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
3
src/routes/failure.js
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
module.exports = (req, res) => {
|
||||||
|
res.render('failure', {})
|
||||||
|
}
|
5
src/routes/logs.js
Normal file
5
src/routes/logs.js
Normal 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
4
src/routes/success.js
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
module.exports = (req, res) => {
|
||||||
|
const name = req.query.name
|
||||||
|
res.render('success', { name })
|
||||||
|
}
|
42
src/server.js
Normal file
42
src/server.js
Normal 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
18
src/views/cards.pug
Normal 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
19
src/views/failure.pug
Normal 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
9
src/views/home.pug
Normal 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
23
src/views/layout.pug
Normal 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
21
src/views/logs.pug
Normal 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
18
src/views/success.pug
Normal 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)
|
||||||
|
|
Loading…
Reference in New Issue
Block a user