diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c2658d7 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules/ diff --git a/README.md b/README.md index 075185c..5967c05 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,16 @@ # Ponz.io Building Ponz.io, with its endearingly upside-down-triangle-shaped business model. + +Jerry Gao and Will Whitworth + +mongoose + +User + +points +id +email +name +parent_id +depth +children [ids] diff --git a/app.js b/app.js new file mode 100644 index 0000000..dfe51b5 --- /dev/null +++ b/app.js @@ -0,0 +1,134 @@ +const express = require("express"); +const app = express(); +const bodyParser = require("body-parser"); +const expressSession = require("express-session"); +const expressHandlebars = require("express-handlebars"); + +// ---------------------------------------- +// Mongoose +// ---------------------------------------- +const mongoose = require("mongoose"); +app.use((req, res, next) => { + if (mongoose.connection.readyState) { + next(); + } else { + require("./mongo")(req).then(() => next()); + } +}); + +// ---------------------------------------- +// Body Parser +// ---------------------------------------- +app.use( + bodyParser.urlencoded({ + extended: false + }) +); + +// ---------------------------------------- +// Sessions +// ---------------------------------------- +app.use( + expressSession({ + secret: process.env.secret || "puppies", + saveUninitialized: false, + resave: false + }) +); + +// ---------------------------------------- +// Handlebars +// ---------------------------------------- +var hbs = expressHandlebars.create({ + partialsDir: "views/", + defaultLayout: "main", + helpers: { + pyramidContainerWidth: pyramid => { + return `${pyramid.length * 60}px`; + }, + pyramidHeight: pyramid => { + let height = pyramid.length * 52; + return `${height}px`; + }, + pyramidWidth: pyramid => { + let width = pyramid.length * 60 / 2; + return `${width}px`; + }, + indent: depth => { + let indent = depth > 0 ? 50 : 0; + return `${indent}px`; + }, + valueCalc: depth => { + let values = [40, 20, 10, 5, 2]; + let value = depth < 5 ? values[depth] : 1; + return `$${value}`; + } + } +}); + +app.engine("handlebars", hbs.engine); +app.set("view engine", "handlebars"); + +// ---------------------------------------- +// Passport +// ---------------------------------------- +const passport = require("passport"); +app.use(passport.initialize()); +app.use(passport.session()); + +const LocalStrategy = require("passport-local").Strategy; +const User = require("./models/User"); +passport.use( + new LocalStrategy( + { usernameField: "email" }, + function(email, password, done) { + User.findOne({ email }, function(err, user) { + if (err) return done(err); + if (!user || !user.validPassword(password)) + return done(null, false, { message: "Invalid email/password" }); + return done(null, user); + }); + } + ) +); + +passport.serializeUser(function(user, done) { + done(null, user.id); +}); + +passport.deserializeUser(function(id, done) { + User.findById(id).then(user => done(null, user)).catch(done); +}); + +// ---------------------------------------- +// Serve /public +// ---------------------------------------- +app.use(express.static(`${__dirname}/public`)); + +// ---------------------------------------- +// currentUser +// ---------------------------------------- +app.use((req, res, next) => { + if (req.user) res.locals.currentUser = req.user; + next(); +}); + +// ---------------------------------------- +// Routers +// ---------------------------------------- +const indexRouter = require("./routes/index")(passport); +app.use("/", indexRouter); +const ponzvertRouter = require("./routes/ponzvert"); +app.use("/ponzvert", ponzvertRouter); + +// ---------------------------------------- +// Error Handler +// ---------------------------------------- +app.use((err, req, res, next) => { + console.log(err); + res.status(500).send(err.stack); +}); + +app.listen(process.env.PORT || 3000, () => { + console.log("taking calls"); +}); diff --git a/config/mongo.json b/config/mongo.json new file mode 100644 index 0000000..73b0ac9 --- /dev/null +++ b/config/mongo.json @@ -0,0 +1,13 @@ +{ + "development": { + "database": "project_ponz_development", + "host": "localhost" + }, + "test": { + "database": "project_ponz_test", + "host": "localhost" + }, + "production": { + "use_env_variable": "MONGODB_URI" + } +} diff --git a/models/User.js b/models/User.js new file mode 100644 index 0000000..76d65b2 --- /dev/null +++ b/models/User.js @@ -0,0 +1,77 @@ +const mongoose = require("mongoose"); +const Schema = mongoose.Schema; +const uniqueValidator = require("mongoose-unique-validator"); +const bcrypt = require("bcrypt"); + +const bluebird = require("bluebird"); +mongoose.Promise = bluebird; + +const UserSchema = mongoose.Schema({ + email: { + type: String, + unique: true, + required: true + }, + fname: { type: String }, + lname: { type: String }, + points: { type: Number }, + passwordHash: { type: String }, + parent: { + type: Schema.Types.ObjectId, + ref: "User" + }, + children: [ + { + type: Schema.Types.ObjectId, + ref: "User" + } + ] +}); + +UserSchema.plugin(uniqueValidator); + +UserSchema.virtual("password").set(function(value) { + this.passwordHash = bcrypt.hashSync(value, 8); +}); + +UserSchema.methods.validPassword = function(password) { + return bcrypt.compareSync(password, this.passwordHash); +}; + +UserSchema.methods.populateChildren = async function(depth = -1) { + let user = await User.findById(this._id).populate("children"); + user.depth = depth; + user.children = await Promise.all( + user.children.map(child => { + return child.populateChildren(depth + 1); + }) + ); + return user; +}; + +UserSchema.methods.addPointsToParents = async function() { + let distance = 0; + let parent = await User.findById(this.parent); + parent.children.push(this._id); + parent.save(); + let user = this; + while (user.parent) { + let parent = await User.findById(user.parent); + if (parent) { + parent.points += _pointsByDistance(distance); + parent.save(); + distance++; + } + user = parent; + } +}; + +function _pointsByDistance(distance) { + const points = [40, 20, 10, 5, 2]; + if (distance < 5) return points[distance]; + return 1; +} + +const User = mongoose.model("User", UserSchema); + +module.exports = User; diff --git a/models/index.js b/models/index.js new file mode 100644 index 0000000..9900c4b --- /dev/null +++ b/models/index.js @@ -0,0 +1,10 @@ +const mongoose = require("mongoose"); +const bluebird = require("bluebird"); + +mongoose.Promise = bluebird; + +let models = {}; + +models.User = require("./User"); + +module.exports = models; diff --git a/mongo.js b/mongo.js new file mode 100644 index 0000000..18cb15c --- /dev/null +++ b/mongo.js @@ -0,0 +1,10 @@ +const mongoose = require("mongoose"); +const env = process.env.NODE_ENV || "development"; +const config = require("./config/mongo")[env]; + +module.exports = () => { + const envUrl = process.env[config.use_env_variable]; + const localUrl = `mongodb://${config.host}/${config.database}`; + const mongoUrl = envUrl ? envUrl : localUrl; + return mongoose.connect(mongoUrl); +}; diff --git a/package.json b/package.json new file mode 100644 index 0000000..9e076c1 --- /dev/null +++ b/package.json @@ -0,0 +1,32 @@ +{ + "name": "project_ponz", + "version": "1.0.0", + "description": "Building Ponz.io, with its endearingly upside-down-triangle-shaped business model.", + "main": "app.js", + "scripts": { + "start": "nodemon app.js", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/William-Charles/project_ponz.git" + }, + "author": "", + "license": "ISC", + "bugs": { + "url": "https://github.com/William-Charles/project_ponz/issues" + }, + "homepage": "https://github.com/William-Charles/project_ponz#readme", + "dependencies": { + "bcrypt": "^1.0.2", + "bluebird": "^3.5.0", + "body-parser": "^1.17.1", + "express": "^4.15.2", + "express-handlebars": "^3.0.0", + "express-session": "^1.15.2", + "mongoose": "^4.9.6", + "mongoose-unique-validator": "^1.0.5", + "passport": "^0.3.2", + "passport-local": "^1.0.0" + } +} diff --git a/public/style.css b/public/style.css new file mode 100644 index 0000000..4a0d906 --- /dev/null +++ b/public/style.css @@ -0,0 +1,43 @@ +.label { + padding: 15px 20px; +} + +.child-ponz { + margin: 30px 0; +} + +.badge-success { + background-color: #468847; +} + +.pyramid-container { + position: relative; + margin: 0 auto; +} + +.pyramid { + position: absolute; + left: 0; + right: 0; + margin: auto; + width: 0; + height: 0; + border-style: solid; + border-color: transparent transparent #777 transparent; + z-index: 1; +} + +.pyramid-levels { + position: absolute; + left: 0; + right: 0; + margin: auto; + z-index: 2; +} + +.pyramid-level { + color: white; + border-bottom: 2px solid white; + padding: 15px 0; + width: 100%; +} diff --git a/repl.js b/repl.js new file mode 100644 index 0000000..77d5055 --- /dev/null +++ b/repl.js @@ -0,0 +1,9 @@ +const mongoose = require('mongoose'); +const repl = require('repl').start({}); +const User = require('./models/User'); + + +require('./mongo')().then(() => { + repl.context.User = User; + repl.context.lg = (data) => console.log(data); +}); diff --git a/routes/index.js b/routes/index.js new file mode 100644 index 0000000..9f41c11 --- /dev/null +++ b/routes/index.js @@ -0,0 +1,67 @@ +const express = require("express"); +let router = express.Router(); + +const { + clone, + buildPyramid +} = require("../services/ponz"); + +const { + loggedInOnly, + loggedOutOnly +} = require("../services/session"); + +const User = require("../models/User"); + +module.exports = passport => { + router.get("/", loggedInOnly, async (req, res, next) => { + let user = await req.user.populateChildren(); + let userClone = clone(user); + let pyramid = buildPyramid(userClone); + res.render("index", { user, pyramid }); + }); + + router.get("/login", loggedOutOnly, (req, res) => { + res.render("login"); + }); + + router.get("/register", loggedOutOnly, (req, res) => { + res.render("register"); + }); + + router.get("/logout", loggedInOnly, (req, res) => { + req.logout(); + res.redirect("/login"); + }); + + router.post( + "/login", + loggedOutOnly, + passport.authenticate("local", { + successRedirect: "/", + failureRedirect: "/login" + }) + ); + + router.post("/register", loggedOutOnly, (req, res, next) => { + const { fname, lname, email, password } = req.body; + const user = new User({ + fname: fname, + lname: lname, + email: email, + points: 0, + password: password + }); + user + .save() + .then(user => { + req.login(user, err => { + if (err) throw err; + res.redirect("/"); + }); + }) + .catch(next); + }); + + return router; +}; diff --git a/routes/ponzvert.js b/routes/ponzvert.js new file mode 100644 index 0000000..5b961e8 --- /dev/null +++ b/routes/ponzvert.js @@ -0,0 +1,39 @@ +const express = require("express"); +let router = express.Router(); +const { + loggedInOnly, + loggedOutOnly +} = require("../services/session"); + +const User = require("../models/User"); +const { augmentParents } = require("../services/ponz"); + +router.get("/:referralId", loggedOutOnly, (req, res) => { + const referralId = req.params.referralId; + res.render("ponzvert", { referralId }); +}); + +router.post("/", loggedOutOnly, async (req, res, next) => { + const { fname, lname, email, password, referralId } = req.body; + let newUser = new User({ + fname: fname, + lname: lname, + email: email, + points: 0, + password: password, + parent: referralId + }); + let user = await newUser.save(); + + if (user) { + user.addPointsToParents(); + req.login(newUser, err => { + if (err) throw err; + res.redirect("/"); + }); + } else { + res.redirect("/"); + } +}); + +module.exports = router; diff --git a/services/ponz.js b/services/ponz.js new file mode 100644 index 0000000..91d5eca --- /dev/null +++ b/services/ponz.js @@ -0,0 +1,22 @@ +let ponz = {}; + +ponz.clone = object => { + return JSON.parse(JSON.stringify(object)); +}; + +ponz.buildPyramid = user => { + let pyramid = [1]; + let nextLevel = []; + user.children.forEach(child => nextLevel.push(child)); + while (nextLevel.length) { + pyramid.push(nextLevel.length); + let users = nextLevel.slice(0); + nextLevel = []; + users.forEach(user => { + user.children.forEach(child => nextLevel.push(child)); + }); + } + return pyramid; +}; + +module.exports = ponz; diff --git a/services/session.js b/services/session.js new file mode 100644 index 0000000..dffbfe8 --- /dev/null +++ b/services/session.js @@ -0,0 +1,21 @@ +"use strict"; + +let Session = {}; + +Session.loggedInOnly = (req, res, next) => { + if (req.user) { + next(); + } else { + res.redirect("/login"); + } +}; + +Session.loggedOutOnly = (req, res, next) => { + if (!req.user) { + next(); + } else { + res.redirect("/"); + } +}; + +module.exports = Session; diff --git a/views/_child.handlebars b/views/_child.handlebars new file mode 100644 index 0000000..c740419 --- /dev/null +++ b/views/_child.handlebars @@ -0,0 +1,11 @@ + +