generated from web/template-single-page-app
Permalink
Show file tree
Hide file tree
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Showing
31 changed files
with
859 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
|
||
.DS_Store | ||
package-lock.json | ||
*.db | ||
node_modules/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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}`)) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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() | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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() | ||
} | ||
} |
26
package.json
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 /> ` | ||
}) | ||
} else { | ||
console.log('geolocation not supported') | ||
} | ||
} |
BIN
+15 KB
public/favicon.ico
Binary file not shown.
BIN
+11.6 KB
public/images/amber.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
BIN
+5.46 KB
public/images/desktop.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
BIN
+11.4 KB
public/images/green.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
BIN
+15.7 KB
public/images/offline.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
BIN
+15 KB
public/images/online.png
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.
BIN
+4.84 KB
public/images/tablet.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
|
||
export async function setup() { | ||
document.querySelector('h1').innerText = '404 PAGE NOT FOUND!' | ||
} |
Oops, something went wrong.