From 6685df4eaed4021f0940c3392635ca6010cb019f Mon Sep 17 00:00:00 2001 From: Jared Hanson Date: Wed, 19 Jan 2022 11:56:05 -0800 Subject: [PATCH 001/202] Initial commit. --- .gitignore | 9 + LICENSE | 24 +++ app.js | 41 +++++ bin/www | 90 ++++++++++ db.js | 16 ++ package.json | 44 +++++ public/css/app.css | 55 ++++++ public/css/base.css | 141 ++++++++++++++++ public/css/home.css | 41 +++++ public/css/index.css | 391 +++++++++++++++++++++++++++++++++++++++++++ public/css/login.css | 106 ++++++++++++ routes/index.js | 119 +++++++++++++ views/error.ejs | 3 + views/home.ejs | 27 +++ 14 files changed, 1107 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 app.js create mode 100755 bin/www create mode 100644 db.js create mode 100644 package.json create mode 100644 public/css/app.css create mode 100644 public/css/base.css create mode 100644 public/css/home.css create mode 100644 public/css/index.css create mode 100644 public/css/login.css create mode 100644 routes/index.js create mode 100644 views/error.ejs create mode 100644 views/home.ejs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000..fa85cd8da9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +.env +var + +# Node.js +node_modules/ +npm-debug.log* + +# Mac OS X +.DS_Store diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000000..00d2e135a7 --- /dev/null +++ b/LICENSE @@ -0,0 +1,24 @@ +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +For more information, please refer to \ No newline at end of file diff --git a/app.js b/app.js new file mode 100644 index 0000000000..97b11fc272 --- /dev/null +++ b/app.js @@ -0,0 +1,41 @@ +var createError = require('http-errors'); +var express = require('express'); +var path = require('path'); +var cookieParser = require('cookie-parser'); +var logger = require('morgan'); + +var indexRouter = require('./routes/index'); + +var app = express(); + +app.locals.pluralize = require('pluralize'); + +// view engine setup +app.set('views', path.join(__dirname, 'views')); +app.set('view engine', 'ejs'); + +app.use(logger('dev')); +app.use(express.json()); +app.use(express.urlencoded({ extended: false })); +app.use(cookieParser()); +app.use(express.static(path.join(__dirname, 'public'))); + +app.use('/', indexRouter); + +// catch 404 and forward to error handler +app.use(function(req, res, next) { + next(createError(404)); +}); + +// error handler +app.use(function(err, req, res, next) { + // set locals, only providing error in development + res.locals.message = err.message; + res.locals.error = req.app.get('env') === 'development' ? err : {}; + + // render the error page + res.status(err.status || 500); + res.render('error'); +}); + +module.exports = app; diff --git a/bin/www b/bin/www new file mode 100755 index 0000000000..1c6ebed6f1 --- /dev/null +++ b/bin/www @@ -0,0 +1,90 @@ +#!/usr/bin/env node + +/** + * Module dependencies. + */ + +var app = require('../app'); +var debug = require('debug')('todos:server'); +var http = require('http'); + +/** + * Get port from environment and store in Express. + */ + +var port = normalizePort(process.env.PORT || '3000'); +app.set('port', port); + +/** + * Create HTTP server. + */ + +var server = http.createServer(app); + +/** + * Listen on provided port, on all network interfaces. + */ + +server.listen(port); +server.on('error', onError); +server.on('listening', onListening); + +/** + * Normalize a port into a number, string, or false. + */ + +function normalizePort(val) { + var port = parseInt(val, 10); + + if (isNaN(port)) { + // named pipe + return val; + } + + if (port >= 0) { + // port number + return port; + } + + return false; +} + +/** + * Event listener for HTTP server "error" event. + */ + +function onError(error) { + if (error.syscall !== 'listen') { + throw error; + } + + var bind = typeof port === 'string' + ? 'Pipe ' + port + : 'Port ' + port; + + // handle specific listen errors with friendly messages + switch (error.code) { + case 'EACCES': + console.error(bind + ' requires elevated privileges'); + process.exit(1); + break; + case 'EADDRINUSE': + console.error(bind + ' is already in use'); + process.exit(1); + break; + default: + throw error; + } +} + +/** + * Event listener for HTTP server "listening" event. + */ + +function onListening() { + var addr = server.address(); + var bind = typeof addr === 'string' + ? 'pipe ' + addr + : 'port ' + addr.port; + debug('Listening on ' + bind); +} diff --git a/db.js b/db.js new file mode 100644 index 0000000000..4142a38252 --- /dev/null +++ b/db.js @@ -0,0 +1,16 @@ +var sqlite3 = require('sqlite3'); +var mkdirp = require('mkdirp'); + +mkdirp.sync('var/db'); + +var db = new sqlite3.Database('var/db/todos.db'); + +db.serialize(function() { + db.run("CREATE TABLE IF NOT EXISTS todos ( \ + owner_id INTEGER NOT NULL, \ + title TEXT NOT NULL, \ + completed INTEGER \ + )"); +}); + +module.exports = db; diff --git a/package.json b/package.json new file mode 100644 index 0000000000..40cfef8830 --- /dev/null +++ b/package.json @@ -0,0 +1,44 @@ +{ + "name": "todos-express-starter", + "version": "0.0.0", + "private": true, + "description": "Starter todo app using Express, Passport, and SQLite for sign in.", + "keywords": [ + "example", + "express", + "passport", + "sqlite" + ], + "author": { + "name": "Jared Hanson", + "email": "jaredhanson@gmail.com", + "url": "https://www.jaredhanson.me/" + }, + "homepage": "https://github.com/passport/todos-express-starter", + "repository": { + "type": "git", + "url": "git://github.com/passport/todos-express-starter.git" + }, + "bugs": { + "url": "https://github.com/passport/todos-express-starter/issues" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jaredhanson" + }, + "license": "Unlicense", + "scripts": { + "start": "node ./bin/www" + }, + "dependencies": { + "cookie-parser": "~1.4.4", + "debug": "~2.6.9", + "ejs": "~2.6.1", + "express": "~4.16.1", + "http-errors": "~1.6.3", + "mkdirp": "^1.0.4", + "morgan": "~1.9.1", + "pluralize": "^8.0.0", + "sqlite3": "^5.0.2" + } +} diff --git a/public/css/app.css b/public/css/app.css new file mode 100644 index 0000000000..c84ddeae5b --- /dev/null +++ b/public/css/app.css @@ -0,0 +1,55 @@ +.nav { + position: absolute; + top: -130px; + right: 0; +} + +.nav ul { + margin: 0; + list-style: none; + text-align: center; +} + +.nav li { + display: inline-block; + height: 40px; + margin-left: 12px; + font-size: 14px; + font-weight: 400; + line-height: 40px; +} + +.nav a { + display: block; + color: inherit; + text-decoration: none; +} + +.nav a:hover { + border-bottom: 1px solid #DB7676; +} + +.nav button { + height: 40px; +} + +.nav button:hover { + border-bottom: 1px solid #DB7676; + cursor: pointer; +} + +/* background image by Cole Bemis */ +.nav .user { + padding-left: 20px; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='18' height='18' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' class='feather feather-user'%3E%3Cpath d='M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2'%3E%3C/path%3E%3Ccircle cx='12' cy='7' r='4'%3E%3C/circle%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: center left; +} + +/* background image by Cole Bemis */ +.nav .logout { + padding-left: 20px; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='18' height='18' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' class='feather feather-log-out'%3E%3Cpath d='M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4'%3E%3C/path%3E%3Cpolyline points='16 17 21 12 16 7'%3E%3C/polyline%3E%3Cline x1='21' y1='12' x2='9' y2='12'%3E%3C/line%3E%3C/svg%3E%0A"); + background-repeat: no-repeat; + background-position: center left; +} diff --git a/public/css/base.css b/public/css/base.css new file mode 100644 index 0000000000..da65968a73 --- /dev/null +++ b/public/css/base.css @@ -0,0 +1,141 @@ +hr { + margin: 20px 0; + border: 0; + border-top: 1px dashed #c5c5c5; + border-bottom: 1px dashed #f7f7f7; +} + +.learn a { + font-weight: normal; + text-decoration: none; + color: #b83f45; +} + +.learn a:hover { + text-decoration: underline; + color: #787e7e; +} + +.learn h3, +.learn h4, +.learn h5 { + margin: 10px 0; + font-weight: 500; + line-height: 1.2; + color: #000; +} + +.learn h3 { + font-size: 24px; +} + +.learn h4 { + font-size: 18px; +} + +.learn h5 { + margin-bottom: 0; + font-size: 14px; +} + +.learn ul { + padding: 0; + margin: 0 0 30px 25px; +} + +.learn li { + line-height: 20px; +} + +.learn p { + font-size: 15px; + font-weight: 300; + line-height: 1.3; + margin-top: 0; + margin-bottom: 0; +} + +#issue-count { + display: none; +} + +.quote { + border: none; + margin: 20px 0 60px 0; +} + +.quote p { + font-style: italic; +} + +.quote p:before { + content: '“'; + font-size: 50px; + opacity: .15; + position: absolute; + top: -20px; + left: 3px; +} + +.quote p:after { + content: '”'; + font-size: 50px; + opacity: .15; + position: absolute; + bottom: -42px; + right: 3px; +} + +.quote footer { + position: absolute; + bottom: -40px; + right: 0; +} + +.quote footer img { + border-radius: 3px; +} + +.quote footer a { + margin-left: 5px; + vertical-align: middle; +} + +.speech-bubble { + position: relative; + padding: 10px; + background: rgba(0, 0, 0, .04); + border-radius: 5px; +} + +.speech-bubble:after { + content: ''; + position: absolute; + top: 100%; + right: 30px; + border: 13px solid transparent; + border-top-color: rgba(0, 0, 0, .04); +} + +.learn-bar > .learn { + position: absolute; + width: 272px; + top: 8px; + left: -300px; + padding: 10px; + border-radius: 5px; + background-color: rgba(255, 255, 255, .6); + transition-property: left; + transition-duration: 500ms; +} + +@media (min-width: 899px) { + .learn-bar { + width: auto; + padding-left: 300px; + } + + .learn-bar > .learn { + left: 8px; + } +} diff --git a/public/css/home.css b/public/css/home.css new file mode 100644 index 0000000000..c372cc0c8a --- /dev/null +++ b/public/css/home.css @@ -0,0 +1,41 @@ +.todohome { + margin: 130px 0 40px 0; + position: relative; +} + +.todohome h1 { + position: absolute; + top: -140px; + width: 100%; + font-size: 80px; + font-weight: 200; + text-align: center; + color: #b83f45; + -webkit-text-rendering: optimizeLegibility; + -moz-text-rendering: optimizeLegibility; + text-rendering: optimizeLegibility; +} + +.todohome section { + padding-top: 1px; + text-align: center; +} + +.todohome h2 { + padding-bottom: 48px; + font-size: 28px; + font-weight: 300; +} + +.todohome .button { + padding: 13px 45px; + font-size: 16px; + font-weight: 500; + color: white; + border-radius: 5px; + background: #d83f45; +} + +.todohome a.button { + text-decoration: none; +} diff --git a/public/css/index.css b/public/css/index.css new file mode 100644 index 0000000000..d6dc71e0ff --- /dev/null +++ b/public/css/index.css @@ -0,0 +1,391 @@ +html, +body { + margin: 0; + padding: 0; +} + +button { + margin: 0; + padding: 0; + border: 0; + background: none; + font-size: 100%; + vertical-align: baseline; + font-family: inherit; + font-weight: inherit; + color: inherit; + -webkit-appearance: none; + appearance: none; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +body { + font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif; + line-height: 1.4em; + background: #f5f5f5; + color: #111111; + min-width: 230px; + max-width: 550px; + margin: 0 auto; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + font-weight: 300; +} + +.hidden { + display: none; +} + +.todoapp { + background: #fff; + margin: 130px 0 40px 0; + position: relative; + box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), + 0 25px 50px 0 rgba(0, 0, 0, 0.1); +} + +.todoapp input::-webkit-input-placeholder { + font-style: italic; + font-weight: 400; + color: rgba(0, 0, 0, 0.4); +} + +.todoapp input::-moz-placeholder { + font-style: italic; + font-weight: 400; + color: rgba(0, 0, 0, 0.4); +} + +.todoapp input::input-placeholder { + font-style: italic; + font-weight: 400; + color: rgba(0, 0, 0, 0.4); +} + +.todoapp h1 { + position: absolute; + top: -140px; + width: 100%; + font-size: 80px; + font-weight: 200; + text-align: center; + color: #b83f45; + -webkit-text-rendering: optimizeLegibility; + -moz-text-rendering: optimizeLegibility; + text-rendering: optimizeLegibility; +} + +.new-todo, +.edit { + position: relative; + margin: 0; + width: 100%; + font-size: 24px; + font-family: inherit; + font-weight: inherit; + line-height: 1.4em; + color: inherit; + padding: 6px; + border: 1px solid #999; + box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2); + box-sizing: border-box; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.new-todo { + padding: 16px 16px 16px 60px; + height: 65px; + border: none; + background: rgba(0, 0, 0, 0.003); + box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03); +} + +.main { + position: relative; + z-index: 2; + border-top: 1px solid #e6e6e6; +} + +.toggle-all { + width: 1px; + height: 1px; + border: none; /* Mobile Safari */ + opacity: 0; + position: absolute; + right: 100%; + bottom: 100%; +} + +.toggle-all + label { + display: flex; + align-items: center; + justify-content: center; + width: 45px; + height: 65px; + font-size: 0; + position: absolute; + top: -65px; + left: -0; +} + +.toggle-all + label:before { + content: '❯'; + display: inline-block; + font-size: 22px; + color: #949494; + padding: 10px 27px 10px 27px; + -webkit-transform: rotate(90deg); + transform: rotate(90deg); +} + +.toggle-all:checked + label:before { + color: #484848; +} + +.todo-list { + margin: 0; + padding: 0; + list-style: none; +} + +.todo-list li { + position: relative; + font-size: 24px; + border-bottom: 1px solid #ededed; +} + +.todo-list li:last-child { + border-bottom: none; +} + +.todo-list li.editing { + border-bottom: none; + padding: 0; +} + +.todo-list li.editing .edit { + display: block; + width: calc(100% - 43px); + padding: 12px 16px; + margin: 0 0 0 43px; +} + +.todo-list li.editing .view { + display: none; +} + +.todo-list li .toggle { + text-align: center; + width: 40px; + /* auto, since non-WebKit browsers doesn't support input styling */ + height: auto; + position: absolute; + top: 0; + bottom: 0; + margin: auto 0; + border: none; /* Mobile Safari */ + -webkit-appearance: none; + appearance: none; +} + +.todo-list li .toggle { + opacity: 0; +} + +.todo-list li .toggle + label { + /* + Firefox requires `#` to be escaped - https://bugzilla.mozilla.org/show_bug.cgi?id=922433 + IE and Edge requires *everything* to be escaped to render, so we do that instead of just the `#` - https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/7157459/ + */ + background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23949494%22%20stroke-width%3D%223%22/%3E%3C/svg%3E'); + background-repeat: no-repeat; + background-position: center left; +} + +.todo-list li .toggle:checked + label { + background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%2359A193%22%20stroke-width%3D%223%22%2F%3E%3Cpath%20fill%3D%22%233EA390%22%20d%3D%22M72%2025L42%2071%2027%2056l-4%204%2020%2020%2034-52z%22%2F%3E%3C%2Fsvg%3E'); +} + +.todo-list li label { + word-break: break-all; + padding: 15px 15px 15px 60px; + display: block; + line-height: 1.2; + transition: color 0.4s; + font-weight: 400; + color: #484848; +} + +.todo-list li.completed label { + color: #949494; + text-decoration: line-through; +} + +.todo-list li .destroy { + display: none; + position: absolute; + top: 0; + right: 10px; + bottom: 0; + width: 40px; + height: 40px; + margin: auto 0; + font-size: 30px; + color: #949494; + transition: color 0.2s ease-out; +} + +.todo-list li .destroy:hover, +.todo-list li .destroy:focus { + color: #C18585; +} + +.todo-list li .destroy:after { + content: '×'; + display: block; + height: 100%; + line-height: 1.1; +} + +.todo-list li:hover .destroy { + display: block; +} + +.todo-list li .edit { + display: none; +} + +.todo-list li.editing:last-child { + margin-bottom: -1px; +} + +.footer { + padding: 10px 15px; + height: 20px; + text-align: center; + font-size: 15px; + border-top: 1px solid #e6e6e6; +} + +.footer:before { + content: ''; + position: absolute; + right: 0; + bottom: 0; + left: 0; + height: 50px; + overflow: hidden; + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), + 0 8px 0 -3px #f6f6f6, + 0 9px 1px -3px rgba(0, 0, 0, 0.2), + 0 16px 0 -6px #f6f6f6, + 0 17px 2px -6px rgba(0, 0, 0, 0.2); +} + +.todo-count { + float: left; + text-align: left; +} + +.todo-count strong { + font-weight: 300; +} + +.filters { + margin: 0; + padding: 0; + list-style: none; + position: absolute; + right: 0; + left: 0; +} + +.filters li { + display: inline; +} + +.filters li a { + color: inherit; + margin: 3px; + padding: 3px 7px; + text-decoration: none; + border: 1px solid transparent; + border-radius: 3px; +} + +.filters li a:hover { + border-color: #DB7676; +} + +.filters li a.selected { + border-color: #CE4646; +} + +.clear-completed, +html .clear-completed:active { + float: right; + position: relative; + line-height: 19px; + text-decoration: none; + cursor: pointer; +} + +.clear-completed:hover { + text-decoration: underline; +} + +.info { + margin: 65px auto 0; + color: #4d4d4d; + font-size: 11px; + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); + text-align: center; +} + +.info p { + line-height: 1; +} + +.info a { + color: inherit; + text-decoration: none; + font-weight: 400; +} + +.info a:hover { + text-decoration: underline; +} + +/* + Hack to remove background from Mobile Safari. + Can't use it globally since it destroys checkboxes in Firefox +*/ +@media screen and (-webkit-min-device-pixel-ratio:0) { + .toggle-all, + .todo-list li .toggle { + background: none; + } + + .todo-list li .toggle { + height: 40px; + } +} + +@media (max-width: 430px) { + .footer { + height: 50px; + } + + .filters { + bottom: 10px; + } +} + +:focus, +.toggle:focus + label, +.toggle-all:focus + label { + box-shadow: 0 0 2px 2px #CF7D7D; + outline: 0; +} diff --git a/public/css/login.css b/public/css/login.css new file mode 100644 index 0000000000..164b4c3665 --- /dev/null +++ b/public/css/login.css @@ -0,0 +1,106 @@ +.prompt { + max-width: 400px; + margin: 50px auto; + padding: 25px; + background: #fff; + border: 1px solid #e6e6e6; + border-radius: 8px; +} + +button { + display: block; + padding: 10px; + width: 100%; + border-radius: 3px; + background: #d83f45; + font-size: 14px; + font-weight: 700; + color: white; + cursor: pointer; +} + +a.button { + box-sizing: border-box; + display: block; + padding: 10px; + width: 100%; + border-radius: 3px; + background: #000; + font-size: 14px; + font-weight: 700; + text-align: center; + text-decoration: none; + color: white; +} + +a.google { + background: #4787ed; +} + +button:hover { + background-color: #c83f45; +} + +h1 { + margin: 0 0 20px 0; + padding: 0 0 5px 0; + font-size: 24px; + font-weight: 500; +} + +h3 { + margin-top: 0; + font-size: 24px; + font-weight: 300; + text-align: center; + color: #b83f45; +} + +form section { + margin: 0 0 20px 0; + position: relative; /* for password toggle positioning */ +} + +label { + display: block; + margin: 0 0 3px 0; + font-size: 14px; + font-weight: 500; +} + +input { + box-sizing: border-box; + width: 100%; + padding: 10px; + font-size: 14px; + border: 1px solid #d9d9d9; + border-radius: 5px; +} + +input[type=email]:not(:focus):invalid, +input[type=password]:not(:focus):invalid { + color: red; + outline-color: red; +} + +hr { + border-top: 1px solid #d9d9d9; + border-bottom: none; +} + +p.help { + text-align: center; + font-weight: 400; +} + +/* background image by Cole Bemis */ +.messages p { + font-size: 14px; + font-weight: 400; + line-height: 1.3; + color: #d83f45; + padding-left: 20px; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%23d83f45' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' class='feather feather-alert-circle'%3E%3Ccircle cx='12' cy='12' r='10'%3E%3C/circle%3E%3Cline x1='12' y1='8' x2='12' y2='12'%3E%3C/line%3E%3Cline x1='12' y1='16' x2='12.01' y2='16'%3E%3C/line%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: center left; +} diff --git a/routes/index.js b/routes/index.js new file mode 100644 index 0000000000..dde3f90d42 --- /dev/null +++ b/routes/index.js @@ -0,0 +1,119 @@ +var express = require('express'); +var db = require('../db'); + +function fetchTodos(req, res, next) { + db.all('SELECT rowid AS id, * FROM todos WHERE owner_id = ?', [ + req.user.id + ], function(err, rows) { + if (err) { return next(err); } + + var todos = rows.map(function(row) { + return { + id: row.id, + title: row.title, + completed: row.completed == 1 ? true : false, + url: '/' + row.id + } + }); + res.locals.todos = todos; + res.locals.activeCount = todos.filter(function(todo) { return !todo.completed; }).length; + res.locals.completedCount = todos.length - res.locals.activeCount; + next(); + }); +} + +var router = express.Router(); + +/* GET home page. */ +router.get('/', function(req, res, next) { + if (!req.user) { return res.render('home'); } + next(); +}, fetchTodos, function(req, res, next) { + res.locals.filter = null; + res.render('index', { user: req.user }); +}); + +router.get('/active', fetchTodos, function(req, res, next) { + res.locals.todos = res.locals.todos.filter(function(todo) { return !todo.completed; }); + res.locals.filter = 'active'; + res.render('index', { user: req.user }); +}); + +router.get('/completed', fetchTodos, function(req, res, next) { + res.locals.todos = res.locals.todos.filter(function(todo) { return todo.completed; }); + res.locals.filter = 'completed'; + res.render('index', { user: req.user }); +}); + +router.post('/', function(req, res, next) { + req.body.title = req.body.title.trim(); + next(); +}, function(req, res, next) { + if (req.body.title !== '') { return next(); } + return res.redirect('/' + (req.body.filter || '')); +}, function(req, res, next) { + db.run('INSERT INTO todos (owner_id, title, completed) VALUES (?, ?, ?)', [ + req.user.id, + req.body.title, + req.body.completed == true ? 1 : null + ], function(err) { + if (err) { return next(err); } + return res.redirect('/' + (req.body.filter || '')); + }); +}); + +router.post('/:id(\\d+)', function(req, res, next) { + req.body.title = req.body.title.trim(); + next(); +}, function(req, res, next) { + if (req.body.title !== '') { return next(); } + db.run('DELETE FROM todos WHERE rowid = ? AND owner_id = ?', [ + req.params.id, + req.user.id + ], function(err) { + if (err) { return next(err); } + return res.redirect('/' + (req.body.filter || '')); + }); +}, function(req, res, next) { + db.run('UPDATE todos SET title = ?, completed = ? WHERE rowid = ? AND owner_id = ?', [ + req.body.title, + req.body.completed !== undefined ? 1 : null, + req.params.id, + req.user.id + ], function(err) { + if (err) { return next(err); } + return res.redirect('/' + (req.body.filter || '')); + }); +}); + +router.post('/:id(\\d+)/delete', function(req, res, next) { + db.run('DELETE FROM todos WHERE rowid = ? AND owner_id = ?', [ + req.params.id, + req.user.id + ], function(err) { + if (err) { return next(err); } + return res.redirect('/' + (req.body.filter || '')); + }); +}); + +router.post('/toggle-all', function(req, res, next) { + db.run('UPDATE todos SET completed = ? WHERE owner_id = ?', [ + req.body.completed !== undefined ? 1 : null, + req.user.id + ], function(err) { + if (err) { return next(err); } + return res.redirect('/' + (req.body.filter || '')); + }); +}); + +router.post('/clear-completed', function(req, res, next) { + db.run('DELETE FROM todos WHERE owner_id = ? AND completed = ?', [ + req.user.id, + 1 + ], function(err) { + if (err) { return next(err); } + return res.redirect('/' + (req.body.filter || '')); + }); +}); + +module.exports = router; diff --git a/views/error.ejs b/views/error.ejs new file mode 100644 index 0000000000..7cf94edf1a --- /dev/null +++ b/views/error.ejs @@ -0,0 +1,3 @@ +

<%= message %>

+

<%= error.status %>

+
<%= error.stack %>
diff --git a/views/home.ejs b/views/home.ejs new file mode 100644 index 0000000000..6eb7d50ad7 --- /dev/null +++ b/views/home.ejs @@ -0,0 +1,27 @@ + + + + + + Express • TodoMVC + + + + + +
+
+

todos

+
+
+

todos helps you get things done

+ Sign in +
+
+ + + From c130a7e4968bab11faf1227072e7c86c06c4cdc5 Mon Sep 17 00:00:00 2001 From: Jared Hanson Date: Wed, 19 Jan 2022 15:56:01 -0800 Subject: [PATCH 002/202] Add starter login view. --- views/login.ejs | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 views/login.ejs diff --git a/views/login.ejs b/views/login.ejs new file mode 100644 index 0000000000..5831f262f2 --- /dev/null +++ b/views/login.ejs @@ -0,0 +1,24 @@ + + + + + + Express • TodoMVC + + + + + +
+

todos

+

Sign in

+
+

Don't have an account? Sign up

+
+ + + From 5146173211b79872967a20d2088bd2d6afc2d847 Mon Sep 17 00:00:00 2001 From: Jared Hanson Date: Wed, 19 Jan 2022 16:28:27 -0800 Subject: [PATCH 003/202] Close h1 tag correctly. --- views/login.ejs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/views/login.ejs b/views/login.ejs index 5831f262f2..90f8caab1b 100644 --- a/views/login.ejs +++ b/views/login.ejs @@ -11,7 +11,7 @@

todos

-

Sign in

+

Sign in


Don't have an account? Sign up

From d32752e9e33452f02a2c760d2335d7f5af457a62 Mon Sep 17 00:00:00 2001 From: Jared Hanson Date: Wed, 19 Jan 2022 16:31:39 -0800 Subject: [PATCH 004/202] Add starter signup view. --- views/signup.ejs | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 views/signup.ejs diff --git a/views/signup.ejs b/views/signup.ejs new file mode 100644 index 0000000000..bba2f7d20c --- /dev/null +++ b/views/signup.ejs @@ -0,0 +1,24 @@ + + + + + + Express • TodoMVC + + + + + +
+

todos

+

Sign up

+
+

Already have an account? Sign in

+
+ + + From 71065b2b4e0d8aab9fcd99c5f2c6a4a67e3b4e6c Mon Sep 17 00:00:00 2001 From: Jared Hanson Date: Wed, 19 Jan 2022 18:33:50 -0800 Subject: [PATCH 005/202] Create users table in database. --- db.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/db.js b/db.js index 4142a38252..639c4d0085 100644 --- a/db.js +++ b/db.js @@ -1,16 +1,32 @@ var sqlite3 = require('sqlite3'); var mkdirp = require('mkdirp'); +var crypto = require('crypto'); mkdirp.sync('var/db'); var db = new sqlite3.Database('var/db/todos.db'); db.serialize(function() { + // create the database schema for the todos app + db.run("CREATE TABLE IF NOT EXISTS users ( \ + username TEXT UNIQUE, \ + hashed_password BLOB, \ + salt BLOB \ + )"); + db.run("CREATE TABLE IF NOT EXISTS todos ( \ owner_id INTEGER NOT NULL, \ title TEXT NOT NULL, \ completed INTEGER \ )"); + + // create an initial user (username: alice, password: letmein) + var salt = crypto.randomBytes(16); + db.run('INSERT OR IGNORE INTO users (username, hashed_password, salt) VALUES (?, ?, ?)', [ + 'alice', + crypto.pbkdf2Sync('letmein', salt, 310000, 32, 'sha256'), + salt + ]); }); module.exports = db; From 5ca1fca763c74bb3beaac42377310968ed5641cd Mon Sep 17 00:00:00 2001 From: Jared Hanson Date: Wed, 19 Jan 2022 21:09:23 -0800 Subject: [PATCH 006/202] Add starter app view. --- views/index.ejs | 94 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 views/index.ejs diff --git a/views/index.ejs b/views/index.ejs new file mode 100644 index 0000000000..863d66d1e8 --- /dev/null +++ b/views/index.ejs @@ -0,0 +1,94 @@ + + + + + + Express • TodoMVC + + + + + +
+ +
+

todos

+
+ + <% if (filter) { %> + + <% } %> +
+
+ <% if (activeCount + completedCount > 0) { %> +
+
+ onchange="this.form.submit();"> + +
+
    + <% todos.forEach(function(todo) { %> +
  • > +
    +
    + onchange="this.form.submit();"> + + +
    + + <% if (filter) { %> + + <% } %> +
    +
    + <% if (filter) { %> + + <% } %> +
    +
  • + <% }); %> +
+
+ <% } %> + <% if (activeCount + completedCount > 0) { %> + + <% } %> +
+ + + From 92c1ea36ed94c35df1463dcf19086f1ae2d6f6f5 Mon Sep 17 00:00:00 2001 From: Jared Hanson Date: Mon, 24 Jan 2022 14:16:20 -0800 Subject: [PATCH 007/202] Add dotenv. --- app.js | 2 ++ package.json | 1 + 2 files changed, 3 insertions(+) diff --git a/app.js b/app.js index 97b11fc272..80938763e5 100644 --- a/app.js +++ b/app.js @@ -1,3 +1,5 @@ +require('dotenv').config(); + var createError = require('http-errors'); var express = require('express'); var path = require('path'); diff --git a/package.json b/package.json index 40cfef8830..17ba129bf8 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "dependencies": { "cookie-parser": "~1.4.4", "debug": "~2.6.9", + "dotenv": "^8.6.0", "ejs": "~2.6.1", "express": "~4.16.1", "http-errors": "~1.6.3", From b4529b697008642b82d216f8246554f5e61f73b8 Mon Sep 17 00:00:00 2001 From: Jared Hanson Date: Tue, 25 Jan 2022 09:13:50 -0800 Subject: [PATCH 008/202] Add federated credentials table to database. --- db.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/db.js b/db.js index 639c4d0085..37355412ad 100644 --- a/db.js +++ b/db.js @@ -14,6 +14,13 @@ db.serialize(function() { salt BLOB \ )"); + db.run("CREATE TABLE IF NOT EXISTS federated_credentials ( \ + user_id INTEGER NOT NULL, \ + provider TEXT NOT NULL, \ + subject TEXT NOT NULL, \ + PRIMARY KEY (provider, subject) \ + )"); + db.run("CREATE TABLE IF NOT EXISTS todos ( \ owner_id INTEGER NOT NULL, \ title TEXT NOT NULL, \ From cef35e3f5fd85691ca702333b2a6e42486eea249 Mon Sep 17 00:00:00 2001 From: Jared Hanson Date: Tue, 25 Jan 2022 09:15:34 -0800 Subject: [PATCH 009/202] Add name column to users table. --- db.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/db.js b/db.js index 37355412ad..fcd6ccf33f 100644 --- a/db.js +++ b/db.js @@ -11,7 +11,8 @@ db.serialize(function() { db.run("CREATE TABLE IF NOT EXISTS users ( \ username TEXT UNIQUE, \ hashed_password BLOB, \ - salt BLOB \ + salt BLOB, \ + name TEXT \ )"); db.run("CREATE TABLE IF NOT EXISTS federated_credentials ( \ From cb7f0f71d14827fda221f815b6a248de75be50e2 Mon Sep 17 00:00:00 2001 From: Jared Hanson Date: Tue, 25 Jan 2022 09:26:27 -0800 Subject: [PATCH 010/202] Show user by name or username. --- views/index.ejs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/views/index.ejs b/views/index.ejs index 863d66d1e8..9c088b2c3d 100644 --- a/views/index.ejs +++ b/views/index.ejs @@ -12,7 +12,7 @@