Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
8e802c5
create pool connected to local db, export query method
Apr 4, 2020
ee3be63
added start script, installed dependecies and eslint
Apr 4, 2020
5b97298
initial commit
Apr 4, 2020
8e2bfa2
add signup form
Apr 4, 2020
a9401aa
airbnb rules
Apr 4, 2020
dbb2326
install ejs
Apr 4, 2020
5557766
switch name att to use camel casing
Apr 4, 2020
9df9b3b
install bcrypt
Apr 4, 2020
8c6e06f
add controllers for /signup GET and POST
Apr 4, 2020
b32a1e0
create user model, add find by email and create methods
Apr 4, 2020
d6632d6
add GET and POST to /signup
Apr 4, 2020
558324c
use body and cookie parsing, use /signup routes, set ejs, open public
Apr 4, 2020
0b1d48c
add normalizer
Apr 4, 2020
5dc5719
require all routes, use auth middleware
Apr 8, 2020
591d893
change connection string to prepare for heroku
Apr 8, 2020
956d8f9
add new line to deconstruction assignment
Apr 8, 2020
0683023
add handlers for tasks functions
Apr 8, 2020
8faf8ab
add sql functions for tasks
Apr 8, 2020
c715ca8
disable frontend forms, add fetch
Apr 8, 2020
f3e7221
add login form
Apr 8, 2020
490c65e
load logout script
Apr 8, 2020
3538445
add table for todos
Apr 8, 2020
20fdc5b
add id to p tag, load script for form
Apr 8, 2020
bacd55c
add nav with links to all viewable routes
Apr 8, 2020
3cc2ce7
write form for new task
Apr 8, 2020
6297c10
add routes, call controllers
Apr 8, 2020
20361bb
add sakura.css
Apr 8, 2020
e639a64
add auth middleware, verifies jwt, compares password with encrypted pass
Apr 8, 2020
20cfe89
add login/out handling functions
Apr 8, 2020
623b840
add tables, reformat sections
Apr 8, 2020
ee1a0ce
add deploy link
Apr 8, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"env": {
"commonjs": true,
"es6": true,
"node": true
},
"extends": [
"airbnb-base"
],
"globals": {
"Atomics": "readonly",
"SharedArrayBuffer": "readonly"
},
"parserOptions": {
"ecmaVersion": 2018
},
"rules": {
}
}
68 changes: 48 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,20 +1,48 @@
# Unit 7 Problem Set #4
## Full Stack To-Do w/ Authentication

## Project Overview
The purpose of this Problem Set is to merge your work from Problem Set 7.1 and Problem Set 7.3. You will rebuild your MVC To-Do list from 7.1 using the RESTful API that you created in 7.3. Instead of sending rendered templates in response to an incoming request, you will make AJAX requests to your backend, handle JSON responses, and render to-do's to the DOM.

### Project Requirements
* Your To-Do list app must handle multiple users.
* Users must register with a username and a password.
* Users should be able to login with a username and password.
* Each user has their own To-Do List, to which they can create, update, view, and delete To-Do's.
* User must be able to logout and be redirected back to the login screen.

## Submission Directions
1. Fork, clone, and create your project in this repo.
2. Update this README so that instead of project directions, it houses the documentation for your project. It can be very simple. [Here's an example](https://github.com/emtes/assignment-todo-list) from Enmanuel!
3. Deploy to Heroku and be sure to include the project URL in your README.

### Due Date
Tuesday, April 7 at 9AM
# Full Stack To-Do w/ Authentication

Unit 7 Problem Set #4

## Project Requirements

- Your To-Do list app must handle multiple users.
- Users must register with a username and a password.
- Users should be able to login with a username and password.
- Each user has their own To-Do List, to which they can create, update, view, and delete To-Do's.
- User must be able to logout and be redirected back to the login screen.

## Getting Started

**This app is deployed [here](https://calm-everglades-14048.herokuapp.com/)**

- Clone
- Run 'npm start'
- Navigate 'localhost:3000'

### Tables

**Tasks**

```sql
CREATE TABLE tasks (
task_id SERIAL PRIMARY KEY,
task_name character varying(64)
NOT NULL CONSTRAINT no_empty_task_name CHECK(task_name != ''),
task_description text,
is_complete boolean DEFAULT FALSE,
due_date DATE,
user_id integer REFERENCES users,
date_created timestamptz DEFAULT NOW()
);
```

**Users**

```sql
CREATE TABLE users (
user_id SERIAL PRIMARY KEY,
first_name text NOT NULL,
last_name text,
email character varying(48) UNIQUE NOT NULL,
password text NOT NULL
);
```
30 changes: 30 additions & 0 deletions app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
const express = require('express');
const bodyParser = require('body-parser');
const cookieParser = require('cookie-parser');
const signUp = require('./routes/signUp');
const logIn = require('./routes/logIn');
const tasks = require('./routes/tasks');
const authenticate = require('./middleware/authenticate');

const app = express();
const port = process.env.PORT || 3000;

app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
app.use(cookieParser());
app.use(express.static('public'));

app.set('view engine', 'ejs');

app.get('/', (req, res) => {
res.render('index');
});

app.use(signUp);
app.use(logIn);

app.use(authenticate);

app.use(tasks);

app.listen(port, () => console.log(`Listening on port ${port}...`));
59 changes: 59 additions & 0 deletions controllers/logIn.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
const bcrypt = require('bcrypt');
const jwt = require('jsonwebtoken');
const User = require('../models/User');

const viewPage = (req, res) => {
res.render('logInForm');
};

const logout = (req, res) => res
.cookie('todo_api_app_token', '', { httpOnly: true })
.status(200)
.render('logout');

// Making a difference between email and password errors because of scope of application
// With more time (and if it was a priority) I would give less hints about what's wrong
// With less hints, another way to verify an unsuccessful login would be great!
const attemptLogIn = async (req, res) => {
const { email, password } = req.body;

let user;
try {
user = await User.find(email);
} catch (err) {
return res.status(500).json({ error: '500 User does not exist' });
}
[user] = user.rows;

// abstract verifying email and password
try {
const isPasswordAMatch = await bcrypt.compare(password, user.password);
if (!isPasswordAMatch) {
return res
.status(403)
.cookie('todo_api_app_token', '', { httpOnly: true })
.json({ error: '403 Forbidden' });
}
} catch (err) {
return res.status(503).json({ error: '503 Failed to authenticate' });
}

const privateKey = 'secret'; // hardcoded because educational purposes
const expirationTime = '2d'; // readable, can be math expression for seconds
const signOpts = { expiresIn: expirationTime };
return jwt.sign({ email, password }, privateKey, signOpts, (err, token) => {
if (err) {
throw new Error();
}
res
.cookie('todo_api_app_token', token, { httpOnly: true })
.status(200)
.json({ success: 'Authorized' });
});
};

module.exports = {
viewPage,
attemptLogIn,
logout,
};
36 changes: 36 additions & 0 deletions controllers/signUp.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
const bcrypt = require('bcrypt');
const User = require('../models/User');

const viewPage = (req, res) => {
res.render('signUpForm');
};

const encryptPassword = async (plainTextPassword) => {
const saltRounds = 8;
const hash = await bcrypt.hash(plainTextPassword, saltRounds);
return hash;
};

// While creating user may fail for a number of reasons, for simplicity instead
// of checking if email is already used I will just attempt the creation and
// consider an error as originitating from the UNIQUE constraint of the email
// colum. Hint user of possible sources of error!
const attemptSignUp = async (req, res) => {
const {
firstName, lastName, email, password,
} = req.body;
const hash = await encryptPassword(password);

try {
await User.create(firstName, lastName, email, hash);
} catch (err) {
return res.status(500).json({ error: '500 Resource not created.' });
}

return res.status(201).json({ success: '201 Resource created.' });
};

module.exports = {
viewPage,
attemptSignUp,
};
83 changes: 83 additions & 0 deletions controllers/tasks.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
const jwt = require('jsonwebtoken');
const Task = require('../models/Task');
const User = require('../models/User');

const getUserIdFromToken = async (token) => {
const privateKey = 'secret';
const payload = jwt.verify(token, privateKey);
const { email } = payload;
try {
const queryRes = await User.find(email);
const [user] = queryRes.rows;
return user.user_id;
} catch (err) {
throw new Error();
}
};

const getAllTasks = async (req, res) => {
const { todo_api_app_token } = req.cookies;
const userId = await getUserIdFromToken(todo_api_app_token);
let queryRes;
let tasks;
try {
queryRes = await Task.getAll(userId);
tasks = queryRes.rows;
} catch (err) {
res.status(500).json({ error: '500 Internal Server Error' });
}
res.status(200).json(tasks);
};

const addTask = async (req, res) => {
const { todo_api_app_token } = req.cookies;
const userId = await getUserIdFromToken(todo_api_app_token);
const {
task, description, isComplete, dueDate,
} = req.body;
await Task.create(userId, task, description, isComplete, dueDate);
const lastCreatedTask = await Task.getLast(userId);
res.status(200).json(lastCreatedTask);
};

const getTaskById = async (req, res) => {
const { todo_api_app_token } = req.cookies;
const userId = await getUserIdFromToken(todo_api_app_token);
const { taskId } = req.params;
const task = await Task.find(userId, taskId);
res.status(200).json(task);
};

const toggleTaskComplete = async (req, res) => {
// const { todo_api_app_token } = req.cookies;
// const userId = await getUserIdFromToken(todo_api_app_token);
const { taskId } = req.params;
Task.toggleComplete(taskId)
.then(() => {
res.status(200).json({ success: '200 Resource updated' });
})
.catch((err) => {
res.status(500).json({ error: '500 Could not update resource' });
});
};

const deleteTask = async (req, res) => {
// const { todo_api_app_token } = req.cookies;
// const userId = await getUserIdFromToken(todo_api_app_token);
const { taskId } = req.params;
Task.deleteTask(taskId)
.then(() => {
res.status(200).json({ success: '200 Resource deleted' });
})
.catch((err) => {
res.status(500).json({ error: '500 Could not delete resource' });
});
};

module.exports = {
getAllTasks,
addTask,
getTaskById,
toggleTaskComplete,
deleteTask,
};
9 changes: 9 additions & 0 deletions db.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
const { Pool } = require('pg');

const pool = new Pool({
connectionString:
process.env.DATABASE_URL
|| 'postgresql://enmanuel@/var/run/postgresql:5432/todo_api',
});

module.exports = { query: (text, params) => pool.query(text, params) };
26 changes: 26 additions & 0 deletions middleware/authenticate.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
const jwt = require('jsonwebtoken');
const bcrypt = require('bcrypt');
const User = require('../models/User');

const authenticate = async (req, res, next) => {
const token = req.cookies.todo_api_app_token;

if (token) {
const privateKey = 'secret';
const payload = jwt.verify(token, privateKey);
const { email, password } = payload;
try {
const queryRes = await User.find(email);
const [user] = queryRes.rows;
const isVerified = await bcrypt.compare(password, user.password);
if (isVerified) {
return next();
}
} catch (err) {
return res.status(500).json({ error: 'Failed to authenticate' });
}
}
return res.status(403).json({ error: 'Unauthorized!' });
};

module.exports = authenticate;
Loading