Skip to content
Permalink
Browse files
Initial commit
  • Loading branch information
circui committed May 16, 2020
0 parents commit 6902de6880cc415240a2960f81aa2bee6ec685aa
Show file tree
Hide file tree
Showing 31 changed files with 859 additions and 0 deletions.
@@ -0,0 +1,5 @@

.DS_Store
package-lock.json
*.db
node_modules/
@@ -0,0 +1,6 @@

# Single Page Application Template

This is a simple template for a [single-page application](https://medium.com/@NeotericEU/single-page-application-vs-multiple-page-application-2591588efe58). It is written in pure JavaScript and uses NodeJS and Koa for its backend, both as the [RESTful API](https://www.smashingmagazine.com/2018/01/understanding-using-rest-api/) and to serve the client code.

It uses SQLite for data persistence.
BIN +6.41 KB chromium_page_load.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
101 index.js
@@ -0,0 +1,101 @@

const Koa = require('koa')
const Router = require('koa-router')
const staticDir = require('koa-static')
const send = require('koa-send')
const koaBody = require('koa-body')({multipart: true, uploadDir: '.'})

const app = new Koa()
const router = new Router()

const defaultPort = 8080
const port = process.env.PORT || defaultPort
const dbName = 'website.db'
const User = require('./modules/user')
const List = require('./modules/list')

app.use(staticDir('public'))

router.get('/', async ctx => {
console.log('---------------------------------------------------------------')
console.log('GET /')
try {
await send(ctx, 'public/index.html')
} catch(err) {
console.log(err.message)
await send(ctx, 'public/index.html')
}
})

router.post('/register', koaBody, async ctx => {
console.log('---------------------------------------------------------------')
console.log('POST /register')
const user = await new User(dbName)
try {
const body = typeof ctx.request.body === 'string' ? JSON.parse(ctx.request.body) : ctx.request.body
await user.register(body.user, body.pass, body.email)
ctx.status = 201
ctx.body = {status: 'success', msg: 'account created'}
} catch(err) {
ctx.status = 422
ctx.body = {status: "error", msg: err.message}
} finally {
user.tearDown()
}
})

router.get('/login', async ctx => {
console.log('---------------------------------------------------------------')
console.log('GET /login')
const user = await new User(dbName)
try {
const header = ctx.request.headers.authorization
const hash = header.split(' ')[1]
const status = await user.login(hash)
ctx.status = 200
ctx.body = {status: 'success', msg: 'valid credentials'}
} catch(err) {
ctx.status = 401
ctx.body = {status: "error", msg: err.message}
} finally {
user.tearDown()
}
})

router.post('/lists', koaBody, async ctx => {
console.log('---------------------------------------------------------------')
console.log('POST /lists')
const list = await new List(dbName)
try {
const body = typeof ctx.request.body === 'string' ? JSON.parse(ctx.request.body) : ctx.request.body
console.log(body)
const header = ctx.request.headers.authorization
console.log(header)
body.id = await list.add(header, body.listname, body.description)
ctx.status = 201
ctx.body = {status: 'success', msg: 'list added', data: ctx.body}
} catch(err) {
ctx.status = 422
ctx.body = {status: "error", msg: err.message}
} finally {
list.tearDown()
}
})

router.get('/lists', async ctx => {
console.log('---------------------------------------------------------------')
console.log('GET /lists')
const list = await new List(dbName)
try {
const lists = await list.getLists(ctx.request.headers.authorization)
console.log(lists)
ctx.status = 200
ctx.body = {status: 'success', msg: 'lists found', data: lists}
} catch(err) {
ctx.status = 422
ctx.body = {status: "error", msg: err.message}
}
})

app.use(router.routes())
module.exports = app.listen(port, () => console.log(`listening on port ${port}`))
@@ -0,0 +1,61 @@

const sqlite = require('sqlite-async')
const atob = require('atob')

module.exports = class List {

constructor(dbName = ':memory:') {
return (async() => {
this.db = await sqlite.open(dbName)
// we need this table to store the user accounts
const sql = 'CREATE TABLE IF NOT EXISTS lists\
(id INTEGER PRIMARY KEY AUTOINCREMENT,\
userid TEXT, listname TEXT, description TEXT,\
date TEXT DEFAULT CURRENT_TIMESTAMP);'
await this.db.run(sql)
return this
})()
}

async add(token, listname, description) {
try {
console.log(token)
const hash = token.substring(6) //removes the Basic_ bit
console.log(hash)
const str = atob(hash)
console.log(str)
const username = str.split(':')[0]
console.log(username)
const sql = `INSERT INTO lists(userid, listname, description)\
VALUES("${username}", "${listname}", "${description}")`
console.log(sql)
const recordID = await this.db.run(sql)
console.log(recordID.lastID)
return recordID.lastID
// return 42
} catch(err) {
console.log(err.message)
throw err
}
}

async getLists(token) {
try {
const hash = token.substring(6) //removes the Basic_ bit
const str = atob(hash)
const username = str.split(':')[0]
const sql = `SELECT * FROM lists WHERE userid="${username}" ORDER BY date DESC;`
console.log(sql)
const lists = await this.db.all(sql)
console.log(lists)
return lists
} catch(err) {
console.log(err.message)
throw err
}
}

async tearDown() {
await this.db.close()
}
}
@@ -0,0 +1,69 @@

const crypto = require('crypto')
const bcrypt = require('bcrypt-promise')
const sqlite = require('sqlite-async')
const atob = require('atob')

const saltRounds = 10

module.exports = class User {

constructor(dbName = ':memory:') {
return (async() => {
this.db = await sqlite.open(dbName)
// we need this table to store the user accounts
const sql = 'CREATE TABLE IF NOT EXISTS users\
(id INTEGER PRIMARY KEY AUTOINCREMENT, user TEXT, pass TEXT, email TEXT);'
await this.db.run(sql)
return this
})()
}

/**
* registers a new user
* @param {String} user the chosen username
* @param {String} pass the chosen password
* @returns {Boolean} returns true if the new user has been added
*/
async register(user, pass, email) {
try {
Array.from(arguments).forEach( val => {
if(val.length === 0) throw new Error('missing field')
})
console.log(`user: ${user}, pass: ${pass}, email: ${email}`)
let sql = `SELECT COUNT(id) as records FROM users WHERE user="${user}";`
const data = await this.db.get(sql)
if(data.records !== 0) throw new Error(`username \'${user}\' already in use`)
sql = `SELECT COUNT(id) as records FROM users WHERE email="${email}";`
const emails = await this.db.get(sql)
if(emails.records !== 0) throw new Error(`email address \'${email}\' is already in use`)
pass = await bcrypt.hash(pass, saltRounds)
sql = `INSERT INTO users(user, pass, email) VALUES("${user}", "${pass}", "${email}")`
await this.db.run(sql)
return true
} catch(err) {
console.log('ERROR IN USER.REGISTER')
console.log(err.message)
throw err
}
}

/**
* checks to see if a set of login credentials are valid
* @param {String} hash the base64-encoded string user:pass
* @returns {Boolean} returns true if credentials are valid
*/
async login(hash) {
const data = atob(hash).split(':')
let sql = `SELECT user, pass FROM users WHERE email="${data[0]}";`
const record = await this.db.get(sql)
if(!record) throw new Error(`email \'${data[0]}\' not found`)
const valid = await bcrypt.compare(data[1], record.pass)
if(!valid) throw new Error(`invalid password for account \'${data[0]}\'`)
return true
}

async tearDown() {
await this.db.close()
}
}
@@ -0,0 +1,26 @@
{
"name": "todo",
"version": "1.0.0",
"engines": {
"node": "12.14.x"
},
"description": "Simple todo app as an SPA",
"main": "index.js",
"scripts": {
"start": "node index.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "Mark J Tyers",
"license": "ISC",
"dependencies": {
"atob": "^2.1.2",
"bcrypt": "^4.0.1",
"bcrypt-promise": "^2.0.0",
"koa": "^2.11.0",
"koa-body": "^4.1.1",
"koa-router": "^8.0.8",
"koa-send": "^5.0.0",
"koa-static": "^5.0.0",
"sqlite-async": "^1.0.12"
}
}
BIN +6 KB public/.DS_Store
Binary file not shown.
@@ -0,0 +1,59 @@

export function generateToken(user, pass) {
const token = `${user}:${pass}`
const hash = btoa(token)
return `Basic ${hash}`
}

export async function login() {
if(!getCookie('authorization')) throw new Error('cookie not found')
const options = { headers: { Authorization: getCookie('authorization') } }
const response = await fetch('/login',options)
const status = response.status
console.log(`HTTP status code: ${status}`)
if(response.status === 401) throw new Error('status 401 NOT AUTHORIZED')
}

// from plainjs.com
export function setCookie(name, value, days) {
const d = new Date
d.setTime(d.getTime() + 24*60*60*1000*days)
document.cookie = `${name}=${value};path=/;expires=${d.toGMTString()}`
}

export function getCookie(name) {
const v = document.cookie.match(`(^|;) ?${name}=([^;]*)(;|$)`)
return v ? v[2] : null
}

export function deleteCookie(name) {
setCookie(name, '', -1)
}

export function onlineStatus() {
if(navigator.onLine) {
return true
} else {
return false
}
}

export function showMessage(message) {
console.log(message)
document.querySelector('aside p').innerText = message
document.querySelector('aside').classList.remove('hidden')
setTimeout( () => document.querySelector('aside').classList.add('hidden'), 2000)
}

export function getLocation() {
if(navigator.geolocation) {
console.log('location supported')
navigator.geolocation.getCurrentPosition( position => {
const pos = position.coords
const locString = `lat: ${pos.latitude}, lon: ${pos.longitude}`
document.getElementById('location').innerHTML = `${locString}<br />&nbsp;`
})
} else {
console.log('geolocation not supported')
}
}
BIN +15 KB public/favicon.ico
Binary file not shown.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
BIN +9.78 KB public/images/red.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
@@ -0,0 +1,41 @@

<!doctype html>
<!-- <html manifest="/website.appcache" lang="en"></html> -->
<html lang="en">
<head>
<meta charset="utf-8">
<title>Simple SPA</title>
<meta name="description" content="Simple SPA">
<meta name="author" content="Mark J Tyers">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="style.css">
<script type="module" src="script.js"></script>
</head>

<body>
<header>
<h1></h1>
<p>You are currently <strong>online</strong></p>
</header>
<nav>
<ul>
<li><a href="#login">Log In</a></li>
<li><a href="#logout">Log out</a></li>
<li><a href="#register">Register</a></li>
<li><a href="#home">View Lists</a></li>
</ul>
</nav>
<aside class="hidden">
<p>XXX</p>
</aside>
<main>

</main>
<footer>
<p id="device"></p>
<p id="resolution"></p>
<p id="connectivity"></p>
<p id="location"></p>
</footer>
</body>
</html>
@@ -0,0 +1,4 @@

export async function setup() {
document.querySelector('h1').innerText = '404 PAGE NOT FOUND!'
}

0 comments on commit 6902de6

Please sign in to comment.