diff --git a/.gitignore b/.gitignore index 46ca415..a878871 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ .DS_Store node_modules/ *.db +screenshots/* diff --git a/__mocks__/sqlite-async-old.js b/__mocks__/sqlite-async-old.js new file mode 100644 index 0000000..46d7b6f --- /dev/null +++ b/__mocks__/sqlite-async-old.js @@ -0,0 +1,51 @@ + +'use strict' + +const records = [ + { + id: 0, + user: 'jdoe', + pass: '$2b$10$vPqO/uGlKchrQCqyBIKdb.8hLEJgaC4aAg4fpre5rausycX1XmkWy' + } +] + +module.exports.open = function() { + return { + all: async sql => { + console.log(`MOCK ${sql}`) + let field = sql.match(/(?<=SELECT\s+).*?(?=\s+FROM)/g)[0] + const condition = sql.match(/\bWHERE\s+(.*)$/g)[0].replace('WHERE ', '').replace(';', '') + const key = condition.split('=')[0].trim() + const val = condition.split('=')[1].replace(/"/g, '').trim() + console.log(`field: "${field}", condition: "${condition}", key: "${key}", val: "${val}"`) + if(field === 'count(id) AS count') field = 'id' + console.log(`field: "${field}"`) + let data = [] + console.log(`${field} : ${key} : ${val}`) + for(const record of records) { + //console.log(record) + console.log(record[field]) + console.log(`"${record[field]}" : "${val}"`) + if(record[field] == val) { + console.log('matching record') + data.push(record) + } + } + console.log(data) + let result = {} + console.log(`field ${field} length: ${field.length}`) + if(field === 'id') { + console.log('need count...') + result.count = data.length + } else { + console.log('need data...') + result[field] = records[0][field] + } + console.log(result) + return result + }, + run: async() => true, // we can just ignore these. + close: async() => true // pretend to close the database. + } +} + diff --git a/__mocks__/sqlite-async-x.js b/__mocks__/sqlite-async-x.js new file mode 100644 index 0000000..98b3969 --- /dev/null +++ b/__mocks__/sqlite-async-x.js @@ -0,0 +1,22 @@ + +'use strict' + +const sqlite = require('sqlite-async') + +module.exports.open = function() { + return { + all: async sql => { + console.log(`MOCK ${sql}`) + const db = await sqlite.open(':memory:') + await db.run('CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY AUTOINCREMENT, user TEXT, pass TEXT);') + const user = 'jdoe' + const pass = '$2b$10$vPqO/uGlKchrQCqyBIKdb.8hLEJgaC4aAg4fpre5rausycX1XmkWy' + await db.run(`INSERT INTO URLSearchParams(user, pass) VALUES("${user}", "${pass}");`) + const data = await db.all(sql) + await db.close() + return data + }, + run: async() => true, // we can just ignore these. + close: async() => true // pretend to close the database. + } +} diff --git a/countries.json b/countries.json new file mode 100644 index 0000000..fb6f449 --- /dev/null +++ b/countries.json @@ -0,0 +1,242 @@ +[ + {"name":"Israel","dial_code":"+972","code":"IL"}, + {"name":"Afghanistan","dial_code":"+93","code":"AF"}, + {"name":"Albania","dial_code":"+355","code":"AL"}, + {"name":"Algeria","dial_code":"+213","code":"DZ"}, + {"name":"AmericanSamoa","dial_code":"+1 684","code":"AS"}, + {"name":"Andorra","dial_code":"+376","code":"AD"}, + {"name":"Angola","dial_code":"+244","code":"AO"}, + {"name":"Anguilla","dial_code":"+1 264","code":"AI"}, + {"name":"Antigua and Barbuda","dial_code":"+1268","code":"AG"}, + {"name":"Argentina","dial_code":"+54","code":"AR"}, + {"name":"Armenia","dial_code":"+374","code":"AM"}, + {"name":"Aruba","dial_code":"+297","code":"AW"}, + {"name":"Australia","dial_code":"+61","code":"AU"}, + {"name":"Austria","dial_code":"+43","code":"AT"}, + {"name":"Azerbaijan","dial_code":"+994","code":"AZ"}, + {"name":"Bahamas","dial_code":"+1 242","code":"BS"}, + {"name":"Bahrain","dial_code":"+973","code":"BH"}, + {"name":"Bangladesh","dial_code":"+880","code":"BD"}, + {"name":"Barbados","dial_code":"+1 246","code":"BB"}, + {"name":"Belarus","dial_code":"+375","code":"BY"}, + {"name":"Belgium","dial_code":"+32","code":"BE"}, + {"name":"Belize","dial_code":"+501","code":"BZ"}, + {"name":"Benin","dial_code":"+229","code":"BJ"}, + {"name":"Bermuda","dial_code":"+1 441","code":"BM"}, + {"name":"Bhutan","dial_code":"+975","code":"BT"}, + {"name":"Bosnia and Herzegovina","dial_code":"+387","code":"BA"}, + {"name":"Botswana","dial_code":"+267","code":"BW"}, + {"name":"Brazil","dial_code":"+55","code":"BR"}, + {"name":"British Indian Ocean Territory","dial_code":"+246","code":"IO"},{"name":"Bulgaria","dial_code":"+359","code":"BG"}, + {"name":"Burkina Faso","dial_code":"+226","code":"BF"}, + {"name":"Burundi","dial_code":"+257","code":"BI"}, + {"name":"Cambodia","dial_code":"+855","code":"KH"}, + {"name":"Cameroon","dial_code":"+237","code":"CM"}, + {"name":"Canada","dial_code":"+1","code":"CA"}, + {"name":"Cape Verde","dial_code":"+238","code":"CV"}, + {"name":"Cayman Islands","dial_code":"+ 345","code":"KY"}, + {"name":"Central African Republic","dial_code":"+236","code":"CF"}, + {"name":"Chad","dial_code":"+235","code":"TD"}, + {"name":"Chile","dial_code":"+56","code":"CL"}, + {"name":"China","dial_code":"+86","code":"CN"}, + {"name":"Christmas Island","dial_code":"+61","code":"CX"}, + {"name":"Colombia","dial_code":"+57","code":"CO"}, + {"name":"Comoros","dial_code":"+269","code":"KM"}, + {"name":"Congo","dial_code":"+242","code":"CG"}, + {"name":"Cook Islands","dial_code":"+682","code":"CK"}, + {"name":"Costa Rica","dial_code":"+506","code":"CR"}, + {"name":"Croatia","dial_code":"+385","code":"HR"}, + {"name":"Cuba","dial_code":"+53","code":"CU"}, + {"name":"Cyprus","dial_code":"+537","code":"CY"}, + {"name":"Czech Republic","dial_code":"+420","code":"CZ"}, + {"name":"Denmark","dial_code":"+45","code":"DK"}, + {"name":"Djibouti","dial_code":"+253","code":"DJ"}, + {"name":"Dominica","dial_code":"+1 767","code":"DM"}, + {"name":"Dominican Republic","dial_code":"+1 849","code":"DO"}, + {"name":"Ecuador","dial_code":"+593","code":"EC"}, + {"name":"Egypt","dial_code":"+20","code":"EG"}, + {"name":"El Salvador","dial_code":"+503","code":"SV"}, + {"name":"Equatorial Guinea","dial_code":"+240","code":"GQ"}, + {"name":"Eritrea","dial_code":"+291","code":"ER"}, + {"name":"Estonia","dial_code":"+372","code":"EE"}, + {"name":"Ethiopia","dial_code":"+251","code":"ET"}, + {"name":"Faroe Islands","dial_code":"+298","code":"FO"}, + {"name":"Fiji","dial_code":"+679","code":"FJ"}, + {"name":"Finland","dial_code":"+358","code":"FI"}, + {"name":"France","dial_code":"+33","code":"FR"}, + {"name":"French Guiana","dial_code":"+594","code":"GF"}, + {"name":"French Polynesia","dial_code":"+689","code":"PF"}, + {"name":"Gabon","dial_code":"+241","code":"GA"}, + {"name":"Gambia","dial_code":"+220","code":"GM"}, + {"name":"Georgia","dial_code":"+995","code":"GE"}, + {"name":"Germany","dial_code":"+49","code":"DE"}, + {"name":"Ghana","dial_code":"+233","code":"GH"}, + {"name":"Gibraltar","dial_code":"+350","code":"GI"}, + {"name":"Greece","dial_code":"+30","code":"GR"}, + {"name":"Greenland","dial_code":"+299","code":"GL"}, + {"name":"Grenada","dial_code":"+1 473","code":"GD"}, + {"name":"Guadeloupe","dial_code":"+590","code":"GP"}, + {"name":"Guam","dial_code":"+1 671","code":"GU"}, + {"name":"Guatemala","dial_code":"+502","code":"GT"}, + {"name":"Guinea","dial_code":"+224","code":"GN"}, + {"name":"Guinea-Bissau","dial_code":"+245","code":"GW"}, + {"name":"Guyana","dial_code":"+595","code":"GY"}, + {"name":"Haiti","dial_code":"+509","code":"HT"}, + {"name":"Honduras","dial_code":"+504","code":"HN"}, + {"name":"Hungary","dial_code":"+36","code":"HU"}, + {"name":"Iceland","dial_code":"+354","code":"IS"}, + {"name":"India","dial_code":"+91","code":"IN"}, + {"name":"Indonesia","dial_code":"+62","code":"ID"}, + {"name":"Iraq","dial_code":"+964","code":"IQ"}, + {"name":"Ireland","dial_code":"+353","code":"IE"}, + {"name":"Israel","dial_code":"+972","code":"IL"}, + {"name":"Italy","dial_code":"+39","code":"IT"}, + {"name":"Jamaica","dial_code":"+1 876","code":"JM"}, + {"name":"Japan","dial_code":"+81","code":"JP"}, + {"name":"Jordan","dial_code":"+962","code":"JO"}, + {"name":"Kazakhstan","dial_code":"+7 7","code":"KZ"}, + {"name":"Kenya","dial_code":"+254","code":"KE"}, + {"name":"Kiribati","dial_code":"+686","code":"KI"}, + {"name":"Kuwait","dial_code":"+965","code":"KW"}, + {"name":"Kyrgyzstan","dial_code":"+996","code":"KG"}, + {"name":"Latvia","dial_code":"+371","code":"LV"}, + {"name":"Lebanon","dial_code":"+961","code":"LB"}, + {"name":"Lesotho","dial_code":"+266","code":"LS"}, + {"name":"Liberia","dial_code":"+231","code":"LR"}, + {"name":"Liechtenstein","dial_code":"+423","code":"LI"}, + {"name":"Lithuania","dial_code":"+370","code":"LT"}, + {"name":"Luxembourg","dial_code":"+352","code":"LU"}, + {"name":"Madagascar","dial_code":"+261","code":"MG"}, + {"name":"Malawi","dial_code":"+265","code":"MW"}, + {"name":"Malaysia","dial_code":"+60","code":"MY"}, + {"name":"Maldives","dial_code":"+960","code":"MV"}, + {"name":"Mali","dial_code":"+223","code":"ML"}, + {"name":"Malta","dial_code":"+356","code":"MT"}, + {"name":"Marshall Islands","dial_code":"+692","code":"MH"}, + {"name":"Martinique","dial_code":"+596","code":"MQ"}, + {"name":"Mauritania","dial_code":"+222","code":"MR"}, + {"name":"Mauritius","dial_code":"+230","code":"MU"}, + {"name":"Mayotte","dial_code":"+262","code":"YT"}, + {"name":"Mexico","dial_code":"+52","code":"MX"}, + {"name":"Monaco","dial_code":"+377","code":"MC"}, + {"name":"Mongolia","dial_code":"+976","code":"MN"}, + {"name":"Montenegro","dial_code":"+382","code":"ME"}, + {"name":"Montserrat","dial_code":"+1664","code":"MS"}, + {"name":"Morocco","dial_code":"+212","code":"MA"}, + {"name":"Myanmar","dial_code":"+95","code":"MM"}, + {"name":"Namibia","dial_code":"+264","code":"NA"}, + {"name":"Nauru","dial_code":"+674","code":"NR"}, + {"name":"Nepal","dial_code":"+977","code":"NP"}, + {"name":"Netherlands","dial_code":"+31","code":"NL"}, + {"name":"Netherlands Antilles","dial_code":"+599","code":"AN"}, + {"name":"New Caledonia","dial_code":"+687","code":"NC"}, + {"name":"New Zealand","dial_code":"+64","code":"NZ"}, + {"name":"Nicaragua","dial_code":"+505","code":"NI"}, + {"name":"Niger","dial_code":"+227","code":"NE"}, + {"name":"Nigeria","dial_code":"+234","code":"NG"}, + {"name":"Niue","dial_code":"+683","code":"NU"}, + {"name":"Norfolk Island","dial_code":"+672","code":"NF"}, + {"name":"Northern Mariana Islands","dial_code":"+1 670","code":"MP"}, + {"name":"Norway","dial_code":"+47","code":"NO"}, + {"name":"Oman","dial_code":"+968","code":"OM"}, + {"name":"Pakistan","dial_code":"+92","code":"PK"}, + {"name":"Palau","dial_code":"+680","code":"PW"}, + {"name":"Panama","dial_code":"+507","code":"PA"}, + {"name":"Papua New Guinea","dial_code":"+675","code":"PG"}, + {"name":"Paraguay","dial_code":"+595","code":"PY"}, + {"name":"Peru","dial_code":"+51","code":"PE"}, + {"name":"Philippines","dial_code":"+63","code":"PH"}, + {"name":"Poland","dial_code":"+48","code":"PL"}, + {"name":"Portugal","dial_code":"+351","code":"PT"}, + {"name":"Puerto Rico","dial_code":"+1 939","code":"PR"}, + {"name":"Qatar","dial_code":"+974","code":"QA"}, + {"name":"Romania","dial_code":"+40","code":"RO"}, + {"name":"Rwanda","dial_code":"+250","code":"RW"}, + {"name":"Samoa","dial_code":"+685","code":"WS"}, + {"name":"San Marino","dial_code":"+378","code":"SM"}, + {"name":"Saudi Arabia","dial_code":"+966","code":"SA"}, + {"name":"Senegal","dial_code":"+221","code":"SN"}, + {"name":"Serbia","dial_code":"+381","code":"RS"}, + {"name":"Seychelles","dial_code":"+248","code":"SC"}, + {"name":"Sierra Leone","dial_code":"+232","code":"SL"}, + {"name":"Singapore","dial_code":"+65","code":"SG"}, + {"name":"Slovakia","dial_code":"+421","code":"SK"}, + {"name":"Slovenia","dial_code":"+386","code":"SI"}, + {"name":"Solomon Islands","dial_code":"+677","code":"SB"}, + {"name":"South Africa","dial_code":"+27","code":"ZA"}, + {"name":"South Georgia and the South Sandwich Islands","dial_code":"+500","code":"GS"}, + {"name":"Spain","dial_code":"+34","code":"ES"}, + {"name":"Sri Lanka","dial_code":"+94","code":"LK"}, + {"name":"Sudan","dial_code":"+249","code":"SD"}, + {"name":"Suriname","dial_code":"+597","code":"SR"}, + {"name":"Swaziland","dial_code":"+268","code":"SZ"}, + {"name":"Sweden","dial_code":"+46","code":"SE"}, + {"name":"Switzerland","dial_code":"+41","code":"CH"}, + {"name":"Tajikistan","dial_code":"+992","code":"TJ"}, + {"name":"Thailand","dial_code":"+66","code":"TH"}, + {"name":"Togo","dial_code":"+228","code":"TG"}, + {"name":"Tokelau","dial_code":"+690","code":"TK"}, + {"name":"Tonga","dial_code":"+676","code":"TO"}, + {"name":"Trinidad and Tobago","dial_code":"+1 868","code":"TT"}, + {"name":"Tunisia","dial_code":"+216","code":"TN"}, + {"name":"Turkey","dial_code":"+90","code":"TR"}, + {"name":"Turkmenistan","dial_code":"+993","code":"TM"}, + {"name":"Turks and Caicos Islands","dial_code":"+1 649","code":"TC"}, + {"name":"Tuvalu","dial_code":"+688","code":"TV"}, + {"name":"Uganda","dial_code":"+256","code":"UG"}, + {"name":"Ukraine","dial_code":"+380","code":"UA"}, + {"name":"United Arab Emirates","dial_code":"+971","code":"AE"}, + {"name":"United Kingdom","dial_code":"+44","code":"GB"}, + {"name":"United States","dial_code":"+1","code":"US"}, + {"name":"Uruguay","dial_code":"+598","code":"UY"}, + {"name":"Uzbekistan","dial_code":"+998","code":"UZ"}, + {"name":"Vanuatu","dial_code":"+678","code":"VU"}, + {"name":"Wallis and Futuna","dial_code":"+681","code":"WF"}, + {"name":"Yemen","dial_code":"+967","code":"YE"}, + {"name":"Zambia","dial_code":"+260","code":"ZM"}, + {"name":"Zimbabwe","dial_code":"+263","code":"ZW"}, + {"name":"Åland Islands","dial_code":"","code":"AX"}, + {"name":"Antarctica","dial_code":null,"code":"AQ"}, + {"name":"Bolivia, Plurinational State of","dial_code":"+591","code":"BO"}, + {"name":"Brunei Darussalam","dial_code":"+673","code":"BN"}, + {"name":"Cocos (Keeling) Islands","dial_code":"+61","code":"CC"}, + {"name":"Congo, The Democratic Republic of the","dial_code":"+243","code":"CD"}, + {"name":"Cote d'Ivoire","dial_code":"+225","code":"CI"}, + {"name":"Falkland Islands (Malvinas)","dial_code":"+500","code":"FK"}, + {"name":"Guernsey","dial_code":"+44","code":"GG"}, + {"name":"Holy See (Vatican City State)","dial_code":"+379","code":"VA"}, + {"name":"Hong Kong","dial_code":"+852","code":"HK"}, + {"name":"Iran, Islamic Republic of","dial_code":"+98","code":"IR"}, + {"name":"Isle of Man","dial_code":"+44","code":"IM"}, + {"name":"Jersey","dial_code":"+44","code":"JE"}, + {"name":"Korea, Democratic People's Republic of","dial_code":"+850","code":"KP"}, + {"name":"Korea, Republic of","dial_code":"+82","code":"KR"}, + {"name":"Lao People's Democratic Republic","dial_code":"+856","code":"LA"}, + {"name":"Libyan Arab Jamahiriya","dial_code":"+218","code":"LY"}, + {"name":"Macao","dial_code":"+853","code":"MO"}, + {"name":"Macedonia, The Former Yugoslav Republic of","dial_code":"+389","code":"MK"}, + {"name":"Micronesia, Federated States of","dial_code":"+691","code":"FM"}, + {"name":"Moldova, Republic of","dial_code":"+373","code":"MD"}, + {"name":"Mozambique","dial_code":"+258","code":"MZ"}, + {"name":"Palestinian Territory, Occupied","dial_code":"+970","code":"PS"},{"name":"Pitcairn","dial_code":"+872","code":"PN"}, + {"name":"Réunion","dial_code":"+262","code":"RE"}, + {"name":"Russia","dial_code":"+7","code":"RU"}, + {"name":"Saint Barthélemy","dial_code":"+590","code":"BL"}, + {"name":"Saint Helena, Ascension and Tristan Da Cunha","dial_code":"+290","code":"SH"}, + {"name":"Saint Kitts and Nevis","dial_code":"+1 869","code":"KN"}, + {"name":"Saint Lucia","dial_code":"+1 758","code":"LC"}, + {"name":"Saint Martin","dial_code":"+590","code":"MF"}, + {"name":"Saint Pierre and Miquelon","dial_code":"+508","code":"PM"}, + {"name":"Saint Vincent and the Grenadines","dial_code":"+1 784","code":"VC"}, + {"name":"Sao Tome and Principe","dial_code":"+239","code":"ST"}, + {"name":"Somalia","dial_code":"+252","code":"SO"}, + {"name":"Svalbard and Jan Mayen","dial_code":"+47","code":"SJ"}, + {"name":"Syrian Arab Republic","dial_code":"+963","code":"SY"}, + {"name":"Taiwan, Province of China","dial_code":"+886","code":"TW"}, + {"name":"Tanzania, United Republic of","dial_code":"+255","code":"TZ"}, + {"name":"Timor-Leste","dial_code":"+670","code":"TL"}, + {"name":"Venezuela, Bolivarian Republic of","dial_code":"+58","code":"VE"}, + {"name":"Viet Nam","dial_code":"+84","code":"VN"}, + {"name":"Virgin Islands, British","dial_code":"+1 284","code":"VG"}, + {"name":"Virgin Islands, U.S.","dial_code":"+1 340","code":"VI"} +] \ No newline at end of file diff --git a/index.js b/index.js new file mode 100644 index 0000000..fb92e31 --- /dev/null +++ b/index.js @@ -0,0 +1,147 @@ +#!/usr/bin/env node + +/** + * Routes File + */ + +'use strict' + +/* MODULE IMPORTS */ +const Koa = require('koa') +const Router = require('koa-router') +const views = require('koa-views') +const staticDir = require('koa-static') +const bodyParser = require('koa-bodyparser') +const koaBody = require('koa-body')({multipart: true, uploadDir: '.'}) +const session = require('koa-session') +const sqlite = require('sqlite-async') +const bcrypt = require('bcrypt-promise') +const fs = require('fs-extra') +const mime = require('mime-types') +//const jimp = require('jimp') + +/* IMPORT CUSTOM MODULES */ +const accounts = require('./modules/accounts') + +const app = new Koa() +const router = new Router() + +/* CONFIGURING THE MIDDLEWARE */ +app.keys = ['darkSecret'] +app.use(staticDir('public')) +app.use(bodyParser()) +app.use(session(app)) +app.use(views(`${__dirname}/views`, { extension: 'handlebars' }, {map: { handlebars: 'handlebars' }})) + +const port = 8080 +const saltRounds = 10 + +/** + * The secure home page. + * + * @name Home Page + * @route {GET} / + * @authentication This route requires cookie-based authentication. + */ +router.get('/', async ctx => { + try { + if(ctx.session.authorised !== true) return ctx.redirect('/login?msg=you need to log in') + const data = {} + if(ctx.query.msg) data.msg = ctx.query.msg + await ctx.render('index') + } catch(err) { + await ctx.render('error', {message: err.message}) + } +}) + +/** + * The user registration page. + * + * @name Register Page + * @route {GET} /register + */ +router.get('/register', async ctx => await ctx.render('register')) + +/** + * The script to process new user registrations. + * + * @name Register Script + * @route {POST} /register + */ +router.post('/register', koaBody, async ctx => { + try { + const body = ctx.request.body + console.log(body) + // PROCESSING FILE + const {path, type} = ctx.request.files.avatar + const fileExtension = mime.extension(type) + console.log(`path: ${path}`) + console.log(`type: ${type}`) + console.log(`fileExtension: ${fileExtension}`) + await fs.copy(path, 'public/avatars/avatar.png') + // ENCRYPTING PASSWORD AND BUILDING SQL + body.pass = await bcrypt.hash(body.pass, saltRounds) + const sql = `INSERT INTO users(user, pass) VALUES("${body.user}", "${body.pass}")` + console.log(sql) + // DATABASE COMMANDS + const db = await sqlite.open('./website.db') + await db.run(sql) + await db.close() + // REDIRECTING USER TO HOME PAGE + ctx.redirect(`/?msg=new user "${body.name}" added`) + } catch(err) { + await ctx.render('error', {message: err.message}) + } +}) + +router.get('/login', async ctx => { + const data = {} + if(ctx.query.msg) data.msg = ctx.query.msg + if(ctx.query.user) data.user = ctx.query.user + await ctx.render('login', data) +}) + +router.post('/login', async ctx => { + try { + const body = ctx.request.body + const db = await sqlite.open('./website.db') + // DOES THE USERNAME EXIST? + const records = await db.get(`SELECT count(id) AS count FROM users WHERE user="${body.user}";`) + if(!records.count) return ctx.redirect('/login?msg=invalid%20username') + const record = await db.get(`SELECT pass FROM users WHERE user = "${body.user}";`) + await db.close() + // DOES THE PASSWORD MATCH? + const valid = await bcrypt.compare(body.pass, record.pass) + if(valid == false) return ctx.redirect(`/login?user=${body.user}&msg=invalid%20password`) + // WE HAVE A VALID USERNAME AND PASSWORD + ctx.session.authorised = true + return ctx.redirect('/?msg=you are now logged in...') + } catch(err) { + await ctx.render('error', {message: err.message}) + } +}) + +// router.post('/login', async ctx => { // 19 lines reduced to 10! +// const body = ctx.request.body +// try { +// await accounts.checkCredentials(body.user, body.pass) +// ctx.session.authorised = true +// return ctx.redirect('/?msg=you are now logged in...') +// } catch(err) { +// return ctx.redirect(`/login?user=${body.user}&msg=${err.message}`) +// } +// }) + +router.get('/logout', async ctx => { + ctx.session.authorised = null; + ctx.redirect('/') +}) + +app.use(router.routes()) +module.exports = app.listen(port, async() => { + // MAKE SURE WE HAVE A DATABASE WITH THE CORRECT SCHEMA + const db = await sqlite.open('./website.db') + await db.run('CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY AUTOINCREMENT, user TEXT, pass TEXT);') + await db.close() + console.log(`listening on port ${port}`) +}) \ No newline at end of file diff --git a/modules/accounts.js b/modules/accounts.js new file mode 100644 index 0000000..5bcd759 --- /dev/null +++ b/modules/accounts.js @@ -0,0 +1,81 @@ +#!/usr/bin/env node + +/** + * Accounts module + * @module modules/accounts + */ + +'use strict' + +var sqlite = require('sqlite-async'); +let bcrypt = require('bcrypt-promise'); + +/** + * This is a generic function that opens the database, executes a query, + * closes the database connection and returns the data. + * @param {String} query - The SQL statement to execute. + * @returns {Object} - the date returned by the query. + */ +async function runSQL(query) { + try { + console.log(query) + let DBName = "./website.db"; + const db = await sqlite.open(DBName); + const data = await db.all(query); + await db.close(); + if(data.length === 1) return data[0] + return data; + } catch(err) { + throw err + } +} + +module.exports.checkCredentials = async(username, password)=> { + try { + var records = await runSQL(`SELECT count(id) AS count FROM users WHERE user="${username}";`); + if(!records.count) throw new Error("invalid username") + const record = await runSQL(`SELECT pass FROM users WHERE user = "${username}";`) + const valid = await bcrypt.compare(password, record.pass) + if(valid == false) throw new Error(`invalid password`) + return true + } catch(err) { + throw err + } +} + + + +/* ----------------------------- STUB FUNCTIONS ----------------------------- */ + +/** + * This function checks the database to see if a username already exists in + * the database. If it detects a duplicate it throws an exception. + * @param {String} username - The username to check. + * @returns {boolean} - returns true if the username does not exist. + * @throws {Error} - throws an error if the username already exists. + */ +async function checkNoDuplicateUsername (username) { + return true +} + +/** + * This function takes data from an uploaded image and saves it to the `avatars` directory. The file name will be the username. + * @param {String} path - the location of the uploaded image + * @param {String} mimeType - the mime type of the uploaded file. + * @returns {boolean} - returns true if the image is valid and is saved. + * @throws {TypeError} - throws an error if the file is not a png or jpg image. + */ +async function saveImage(path, mimetype) { + return true +} + +/** + * This function adds new users to the database. + * @param {String} username - The username to to add. + * @param {String} password - The password to add. + * @returns {boolean} - returns true if the username does not exist. + * @throws {Error} - throws an error if the new user account has been created. + */ +module.exports.addUser = async(username, password) => { + return true; +} \ No newline at end of file diff --git a/public/avatars/avatar.png b/public/avatars/avatar.png new file mode 100644 index 0000000..125a389 Binary files /dev/null and b/public/avatars/avatar.png differ diff --git a/public/style.css b/public/style.css new file mode 100644 index 0000000..91b5714 --- /dev/null +++ b/public/style.css @@ -0,0 +1,12 @@ + +body { + font-family: Arial, Helvetica, sans-serif; + } + + .msg { + border: 1px solid red; + font-weight: bold; + color: red; + padding: 1em; + } + \ No newline at end of file diff --git a/temp.js b/temp.js new file mode 100644 index 0000000..ea10ade --- /dev/null +++ b/temp.js @@ -0,0 +1,29 @@ + +'use strict' + +const records = [ + { + user: 'jdoe', + pass: '$2b$10$vPqO/uGlKchrQCqyBIKdb.8hLEJgaC4aAg4fpre5rausycX1XmkWy' + } +] + +//const sql = 'SELECT count(id) AS count FROM users WHERE user="jdoe";' +const sql = 'SELECT user FROM users WHERE user="jdoe"' +const field = sql.match(/(?<=SELECT\s+).*?(?=\s+FROM)/g)[0] +const condition = sql.match(/\bWHERE\s+(.*)$/g)[0].replace('WHERE ', '').replace(';', '') +const key = condition.split('=')[0] +const val = condition.split('=')[1].replace(/"/g, '') +console.log(field) +console.log(key) +console.log(val) + +let data = [] +for(const record of records) if(record[key] == val) data.push(record[key]) +let result = {} +if(field === 'count(id) AS count') { + result.count = data.length +} else { + result[field] = val +} +console.log(result) \ No newline at end of file diff --git a/test.sh b/test.sh new file mode 100755 index 0000000..f6c4ea0 --- /dev/null +++ b/test.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +node index.js& +node_modules/.bin/jest --detectOpenHandles tests/acceptance/ +kill %1 diff --git a/tests/acceptance/auth.spec.js b/tests/acceptance/auth.spec.js new file mode 100644 index 0000000..6dfca2f --- /dev/null +++ b/tests/acceptance/auth.spec.js @@ -0,0 +1,60 @@ + +'use strict' + +const puppeteer = require('puppeteer') + +let page +let browser + +const baseURL = 'http://localhost:8080' + +beforeAll( async() => { + const width = 800 + const height = 600 + browser = await puppeteer.launch({ + headless: true, + slowMo: 40, + args: [`--window-size=${width},${height}`, '--disable-http2'] + }) + page = await browser.newPage() + await page.setViewport({ width, height }) +}) + +afterAll( async() => await browser.close()) + +describe('checking home screen is protected', () => { + test('home screen redirects to login if not logged in', async done => { + await page.waitFor(1000) + await page.goto('http://localhost:8080/logout') + await page.waitFor(1000) + //await page.goto('http://localhost:8080/', { waitUntil: 'domcontentloaded' }) + const title = await page.title() + expect(title).toBe('Log In') + done() + }) +}) + +describe('registering an account', () => { + test('home screen redirects to login if not logged in', async done => { + try { + await page.waitFor(1000) + await page.goto(`${baseURL}/register`, { waitUntil: 'domcontentloaded' }) + await page.type('input[name=user]', 'jbloggs') + await page.type('input[name=pass]', 'p455w0rd') + const input = await page.$('input[name=avatar]') + await input.uploadFile('screenshots/person.png') + await page.screenshot({ path: 'screenshots/completedRegForm.png' }) + await page.click('input[type=submit]') + await page.waitFor(1000) + await page.screenshot({ path: 'screenshots/accountCreated.png' }) + const title = await page.title() + expect(title).toBe('Log In') + const text = await page.evaluate(() => document.body.textContent) + expect(text).toContain('you need to log in') + } catch(err) { + //done.fail(new Error(err.message)) + } finally { + done() + } + }) +}) diff --git a/tests/unit/accounts.spec.js b/tests/unit/accounts.spec.js new file mode 100644 index 0000000..ac9ff3a --- /dev/null +++ b/tests/unit/accounts.spec.js @@ -0,0 +1,62 @@ + +'use strict' + +const accounts = require('../../modules/accounts.js') +//jest.mock('sqlite-async') + +beforeAll( async() => { + console.log('Jest starting!') + // we insert a single user into the database + const sqlite = require('sqlite-async') + const bcrypt = require('bcrypt-promise') + const pass = await bcrypt.hash('goodPassword', 10) + const db = await sqlite.open('./website.db') + await db.run('DROP TABLE IF EXISTS users;') + await db.run('CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY AUTOINCREMENT, user TEXT, pass TEXT);') + console.log(`INSERT INTO users(user, pass) VALUES("jdoe", "${pass}"`) + await db.run(`INSERT INTO users(user, pass) VALUES("jdoe", "${pass}")`) + await db.close() + console.log('database built') +}) + +describe('checkCredentials()', () => { + test('returns true if valid username and password', async done => { + expect.assertions(1) + try { + const result = await accounts.checkCredentials('jdoe', 'goodPassword') + expect(result).toBe(true) + } catch(err) { + console.log(`ERROR: ${err.message}`) + } finally { + done() + } + }) + + test('throws error if invalid username', async done => { + expect.assertions(1) + try { + await accounts.checkCredentials('johndoe', 'goodPassword') + //expect(result).toBe(true) + } catch(err) { + expect(err.message).toBe('invalid username') + } finally { + done() + } + }) + + test('throws error if invalid password', async done => { + expect.assertions(1) + try { + await accounts.checkCredentials('jdoe', 'badPassword') + //expect(result).toBe(true) + } catch(err){ + expect(err.message).toBe('invalid password') + } finally { + done() + } + }) +}) + +describe('addUser()', () => { + // TODO +}) diff --git a/tests/unit/index.specx.js b/tests/unit/index.specx.js new file mode 100644 index 0000000..93f7ad4 --- /dev/null +++ b/tests/unit/index.specx.js @@ -0,0 +1,33 @@ + +'use strict' + +const request = require('supertest') +const status = require('http-status-codes') + +const server = require('../../index.js') + +beforeAll( async() => console.log('Jest starting!')) + +describe('GET /', () => { + // TODO +}) + +describe('GET /register', () => { + // TODO +}) + +describe('POST /register', () => { + // TODO +}) + +describe('GET /login', () => { + // TODO +}) + +describe('POST /login', () => { + // TODO +}) + +describe('GET /logout', () => { + // TODO +}) diff --git a/views/error.handlebars b/views/error.handlebars new file mode 100644 index 0000000..11fe16d --- /dev/null +++ b/views/error.handlebars @@ -0,0 +1,16 @@ + + + + + + + ERROR + + + + + +

An Error Has Occurred

+

{{message}}

+ + diff --git a/views/index.handlebars b/views/index.handlebars new file mode 100644 index 0000000..f0b7767 --- /dev/null +++ b/views/index.handlebars @@ -0,0 +1,15 @@ + + + + + + + Home Page + + + + + +

Home

+

This is a secure page. Users need to have a valid account and be logged in to see it.

+ diff --git a/views/login.handlebars b/views/login.handlebars new file mode 100644 index 0000000..d83f8b4 --- /dev/null +++ b/views/login.handlebars @@ -0,0 +1,23 @@ + + + + + + + Log In + + + + + +

Log In

+ {{#if msg}} +

{{msg}}

+ {{/if}} +
+

Username:

+

Password:

+

+
+ + diff --git a/views/register.handlebars b/views/register.handlebars new file mode 100644 index 0000000..2e543b8 --- /dev/null +++ b/views/register.handlebars @@ -0,0 +1,21 @@ + + + + + + + Create an Account + + + + + +

Create an Account

+
+

Username:

+

Password:

+

Profile Picture:

+

+
+ +