Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
The diff you're trying to view is too large. We only load the first 3000 changed files.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
node_modules
.env
1 change: 1 addition & 0 deletions Procfile
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
web: node server.js
85 changes: 68 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,22 +1,73 @@
#100Devs Simple Express App Submission

### Goal: Make Your Own CRUD APP and Push To Heroku
# Pocket

### How to submit your code for review:
A personal budget tracker app with passport local authorization. Add bills and multiple sources of income. See how your expenses break down daily, weekly, monthly, and yearly.

- Fork and clone this repo
- Create a new branch called answer
- Checkout answer branch
- Push to your fork
- Issue a pull request
- Your pull request description should contain the following:
- (1 to 5 no 3) I completed the challenge
- (1 to 5 no 3) I feel good about my code
- Anything specific on which you want feedback!
**Link to project:** https://pocket.cyclic.app

Example:
```
I completed the challenge: 5
I feel good about my code: 4
I'm not sure if my constructors are setup cleanly...

## Screenshots

![App Screenshot](https://via.placeholder.com/468x300?text=App+Screenshot+Here)


## Tech Stack

**Client:** JavaScript, EJS, TailwindCSS

**Server:** Node, Express, MongoDB

**Dependencies:** bcrypt, connect-mongo, dotenv, ejs, express, express-flash, express-session, mongodb, mongoose, morgan, nodemon, passport, passport-local, validator

## Features

- Secure login with Passport Auth
- Add expenses and multiple sources of income
- Sort expenses by name, category, or cost
- See how expenses break down on a daily, weekly, monthly, and yearly basis
- Responsive for desktop & mobile


## Installation

Install with npm

```bash
npm install bcrypt connect-mongo dotenv ejs express express-flash express-session mongodb mongoose morgan nodemon passport passport-local validator
```
## Environment Variables

To run this project, you will need to add the following environment variables to your `.env` file

`PORT = <port>` (can be any port, ex: 3000)

### Database

`DB_STRING = <your MongoDB uri>`
## Optimizations

- User can select custom frequency for expenses and income streams (every x number of weeks/months)
- Made table data sortable
- Added Mongoose virtuals to calculate daily, weekly, monthly, and yearly cost of each expense without having to store all that data in the database
- Added soft delete so expenses aren't automatically purged from database in case user needs to restore data

**Planned Features & Improvements:**

- Calcuate user's remaining funds
- Add budgeting and saving tips
- Add confirmation popups for deleting expenses, income, and account

## Other Examples of My Work

**Mailroom:** https://mailroom.cyclic.app

![App Screenshot](https://via.placeholder.com/468x300?text=App+Screenshot+Here)


**Pictogram:** https://pictogram.cyclic.app

![App Screenshot](https://via.placeholder.com/468x300?text=App+Screenshot+Here)

**myPetPal: ** https://mypetpal.onrender.com

![App Screenshot](https://via.placeholder.com/468x300?text=App+Screenshot+Here)
19 changes: 19 additions & 0 deletions config/database.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
const mongoose = require("mongoose");

const connectDB = async () => {
try {
const conn = await mongoose.connect(process.env.DB_STRING, {
useNewUrlParser: true,
useUnifiedTopology: true,
useFindAndModify: false,
useCreateIndex: true,
});

console.log(`MongoDB Connected: ${conn.connection.host}`);
} catch (err) {
console.error(err);
process.exit(1);
}
};

module.exports = connectDB;
41 changes: 41 additions & 0 deletions config/passport.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
const LocalStrategy = require("passport-local").Strategy;
const mongoose = require("mongoose");
const User = require("../models/User");

module.exports = function (passport) {
passport.use(
new LocalStrategy({ usernameField: "email" }, (email, password, done) => {
User.findOne({ email: email.toLowerCase() }, (err, user) => {
if (err) {
return done(err);
}
if (!user) {
return done(null, false, { msg: `Email ${email} not found.` });
}
if (!user.password) {
return done(null, false, {
msg:
"Your account was registered using a sign-in provider. To enable password login, sign in using a provider, and then set a password under your user profile.",
});
}
user.comparePassword(password, (err, isMatch) => {
if (err) {
return done(err);
}
if (isMatch) {
return done(null, user);
}
return done(null, false, { msg: "Invalid email or password." });
});
});
})
);

passport.serializeUser((user, done) => {
done(null, user.id);
});

passport.deserializeUser((id, done) => {
User.findById(id, (err, user) => done(err, user));
});
};
136 changes: 136 additions & 0 deletions controllers/auth.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
const passport = require("passport");
const validator = require("validator");
const User = require("../models/User");

exports.getLogin = (req, res) => {
if (req.user) {
return res.redirect("/budget");
}
res.render("login", {
title: "Login",
});
};

exports.postLogin = (req, res, next) => {
const validationErrors = [];
if (!validator.isEmail(req.body.email))
validationErrors.push({ msg: "Please enter a valid email address." });
if (validator.isEmpty(req.body.password))
validationErrors.push({ msg: "Password cannot be blank." });

if (validationErrors.length) {
req.flash("errors", validationErrors);
return res.redirect("/login");
}
req.body.email = validator.normalizeEmail(req.body.email, {
gmail_remove_dots: false,
});

passport.authenticate("local", (err, user, info) => {
if (err) {
return next(err);
}
if (!user) {
req.flash("errors", info);
return res.redirect("/login");
}
req.logIn(user, (err) => {
if (err) {
return next(err);
}
req.flash("success", { msg: "Success! You are logged in." });
res.redirect(req.session.returnTo || "/budget");
});
})(req, res, next);
};

exports.logout = (req, res) => {
req.logout(() => {
console.log('User has logged out.')
})
req.session.destroy((err) => {
if (err)
console.log("Error : Failed to destroy the session during logout.", err);
req.user = null;
res.redirect("/");
});
};

exports.getSignup = (req, res) => {
if (req.user) {
return res.redirect("/budget");
}
res.render("signup", {
title: "Create Account",
});
};

exports.postSignup = (req, res, next) => {
const validationErrors = [];
if (!validator.isEmail(req.body.email))
validationErrors.push({ msg: "Please enter a valid email address." });
if (!validator.isLength(req.body.password, { min: 8 }))
validationErrors.push({
msg: "Password must be at least 8 characters long",
});
if (req.body.password !== req.body.confirmPassword)
validationErrors.push({ msg: "Passwords do not match" });

if (validationErrors.length) {
req.flash("errors", validationErrors);
return res.redirect("../signup");
}
req.body.email = validator.normalizeEmail(req.body.email, {
gmail_remove_dots: false,
});

const user = new User({
userName: req.body.userName,
email: req.body.email,
password: req.body.password,
});

User.findOne(
{ $or: [{ email: req.body.email }, { userName: req.body.userName }] },
(err, existingUser) => {
if (err) {
return next(err);
}
if (existingUser) {
req.flash("errors", {
msg: "Account with that email address or username already exists.",
});
return res.redirect("../signup");
}
user.save((err) => {
if (err) {
return next(err);
}
req.logIn(user, (err) => {
if (err) {
return next(err);
}
res.redirect("/budget");
});
});
}
);
};

exports.getAccount = async (req, res) => {
if (!req.user) {
return res.redirect("/");
}
const user = await User.findById(req.user.id)

res.render("account", {user: user});
};

exports.deleteAccount = async (req, res) => {
if (!req.user) {
return res.redirect("/");
}
const user = await User.findOneAndDelete(req.user.id)

res.render("account", {user: user});
};
Loading