Initial Commit
This commit is contained in:
commit
f54d97e4a9
30 changed files with 2532 additions and 0 deletions
39
.gitignore
vendored
Normal file
39
.gitignore
vendored
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
pids
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
*.pid.lock
|
||||||
|
|
||||||
|
# Dependency directories
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# TypeScript v1 declaration files
|
||||||
|
typings/
|
||||||
|
|
||||||
|
# Optional npm cache directory
|
||||||
|
.npm
|
||||||
|
|
||||||
|
# Optional eslint cache
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
# Optional REPL history
|
||||||
|
.node_repl_history
|
||||||
|
|
||||||
|
# Yarn Integrity file
|
||||||
|
.yarn-integrity
|
||||||
|
|
||||||
|
# dotenv environment variables file
|
||||||
|
.env
|
||||||
|
|
||||||
|
# Mac files
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Database
|
||||||
|
db/
|
27
README.md
Normal file
27
README.md
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
# Christmas Community
|
||||||
|
Web app for your family's Christmas shopping
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
To create a simple place for your entire family to use to find gifts that people want, and to avoid double-gifting.
|
||||||
|
|
||||||
|
## Install
|
||||||
|
```sh
|
||||||
|
git clone https://gitlab.com/wingysam/christmas-community
|
||||||
|
cd christmas-community
|
||||||
|
yarn
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
Add environment variables with a .env. Example:
|
||||||
|
```env
|
||||||
|
SITE_TITLE="Christmas Zone"
|
||||||
|
PORT=80
|
||||||
|
```
|
||||||
|
|
||||||
|
## Startup
|
||||||
|
```sh
|
||||||
|
yarn start
|
||||||
|
```
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
Visit `/` on the HTTP server to add an admin account.
|
8
config/index.js
Normal file
8
config/index.js
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
require('dotenv').config();
|
||||||
|
module.exports = {
|
||||||
|
dbUrl: process.env.DB_URL || 'db',
|
||||||
|
defaultFailureRedirect: process.env.DEFAULT_FAILURE_REDIRECT || '/login',
|
||||||
|
port: process.env.PORT || 3000,
|
||||||
|
secret: process.env.SECRET || require('uuid/v4')(),
|
||||||
|
siteTitle: process.env.SITE_TITLE || 'Christmas Community'
|
||||||
|
};
|
57
index.js
Normal file
57
index.js
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
const LocalStrategy = require('passport-local').Strategy;
|
||||||
|
const bcrypt = require('bcrypt-nodejs');
|
||||||
|
const flash = require('connect-flash');
|
||||||
|
const passport = require('passport');
|
||||||
|
const express = require('express');
|
||||||
|
const PouchDB = require('pouchdb');
|
||||||
|
|
||||||
|
const config = require('./config');
|
||||||
|
|
||||||
|
const logger = require('./logger');
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
|
||||||
|
const db = new PouchDB(config.dbUrl);
|
||||||
|
|
||||||
|
passport.use('local', new LocalStrategy(
|
||||||
|
(username, password, done) => {
|
||||||
|
db.get(username)
|
||||||
|
.then(doc => {
|
||||||
|
bcrypt.compare(password, doc.password, (err, correct) => {
|
||||||
|
if (err) return done(err);
|
||||||
|
if (!correct) return done(null, false, { message: 'Incorrect password' });
|
||||||
|
if (correct) return done(null, doc);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
if (err.message === 'missing') return done(null, false, { message: 'Incorrect username.' });
|
||||||
|
return done(err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
));
|
||||||
|
|
||||||
|
passport.serializeUser((user, callback) => callback(null, user._id));
|
||||||
|
|
||||||
|
passport.deserializeUser((user, callback) => {
|
||||||
|
db.get(user)
|
||||||
|
.then(dbUser => callback(null, dbUser))
|
||||||
|
.catch(err => callback(err));
|
||||||
|
});
|
||||||
|
|
||||||
|
app.use(require('body-parser').urlencoded({ extended: true }));
|
||||||
|
app.use(require('express-session')({ secret: config.secret, resave: false, saveUninitialized: true }));
|
||||||
|
app.use(flash());
|
||||||
|
app.use(passport.initialize());
|
||||||
|
app.use(passport.session());
|
||||||
|
|
||||||
|
app.use(require('./middlewares/locals'));
|
||||||
|
|
||||||
|
app.use((req, res, next) => {
|
||||||
|
logger.log('express', `${req.ip} - ${req.method} ${req.originalUrl}`);
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
|
app.set('view engine', 'pug');
|
||||||
|
app.use('/', require('./routes')(db));
|
||||||
|
|
||||||
|
app.listen(config.port, () => logger.success('express', `Express server started on port ${config.port}!`))
|
12
logger.js
Normal file
12
logger.js
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
const chalk = require('chalk');
|
||||||
|
const config = require('./config');
|
||||||
|
const colors = {log: 'blue', success: 'green', error: 'red', warn: 'yellow'};
|
||||||
|
|
||||||
|
// rewrite to use Object.keys()
|
||||||
|
for (let property in colors) {
|
||||||
|
if (colors.hasOwnProperty(property)) {
|
||||||
|
module.exports[property] = (type, msg) => {
|
||||||
|
console.log(chalk.keyword(colors[property])(`[ ${type.toUpperCase()} ] ${msg}`));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
8
middlewares/locals.js
Normal file
8
middlewares/locals.js
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
const config = require('../config');
|
||||||
|
module.exports = (req, res, next) => {
|
||||||
|
res.locals.successes = req.flash('success');
|
||||||
|
res.locals.errors = req.flash('error');
|
||||||
|
res.locals.config = config;
|
||||||
|
res.locals.req = req;
|
||||||
|
next();
|
||||||
|
};
|
11
middlewares/verifyAuth.js
Normal file
11
middlewares/verifyAuth.js
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
const config = require('../config');
|
||||||
|
module.exports = options => {
|
||||||
|
return (req, res, next) => {
|
||||||
|
options = options ? options : {};
|
||||||
|
if (req.isAuthenticated()) {
|
||||||
|
next();
|
||||||
|
} else {
|
||||||
|
res.redirect(options.failureRedirect || config.defaultFailureRedirect);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
27
package.json
Normal file
27
package.json
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
{
|
||||||
|
"name": "christmas-community",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Christmas lists for communities",
|
||||||
|
"main": "main.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
|
"start": "node index.js"
|
||||||
|
},
|
||||||
|
"author": "Wingysam <wingysam+git@gmail.com>",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"bcrypt-nodejs": "^0.0.3",
|
||||||
|
"body-parser": "^1.18.3",
|
||||||
|
"chalk": "^2.4.1",
|
||||||
|
"connect-flash": "^0.1.1",
|
||||||
|
"dotenv": "^6.1.0",
|
||||||
|
"express": "^4.16.4",
|
||||||
|
"express-session": "^1.15.6",
|
||||||
|
"get-product-name": "^0.0.2",
|
||||||
|
"passport": "^0.4.0",
|
||||||
|
"passport-local": "^1.0.0",
|
||||||
|
"pouchdb": "^7.0.0",
|
||||||
|
"pug": "^2.0.3",
|
||||||
|
"uuid": "^3.3.2"
|
||||||
|
}
|
||||||
|
}
|
60
routes/adminSettings/index.js
Normal file
60
routes/adminSettings/index.js
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
const verifyAuth = require('../../middlewares/verifyAuth');
|
||||||
|
const bcrypt = require('bcrypt-nodejs');
|
||||||
|
const express = require('express');
|
||||||
|
|
||||||
|
module.exports = (db) => {
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
router.get('/', verifyAuth(), (req, res) => {
|
||||||
|
if (!req.user.admin) return res.redirect('/');
|
||||||
|
db.allDocs({ include_docs: true })
|
||||||
|
.then(docs => {
|
||||||
|
res.render('adminSettings', { title: 'Admin Settings', users: docs.rows })
|
||||||
|
})
|
||||||
|
.catch(err => { throw err; });
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post('/add', verifyAuth(), async (req, res) => {
|
||||||
|
if (!req.user.admin) return res.redirect('/');
|
||||||
|
bcrypt.hash(req.body.newUserPassword, null, null, async (err, newUserPasswordHash) => {
|
||||||
|
if (err) throw err;
|
||||||
|
await db.put({
|
||||||
|
_id: req.body.newUserUsername,
|
||||||
|
password: newUserPasswordHash,
|
||||||
|
admin: false,
|
||||||
|
wishlist: []
|
||||||
|
});
|
||||||
|
req.flash('success', `Successfully added user ${req.body.newUserUsername}!`);
|
||||||
|
res.redirect('/admin-settings');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get('/remove/:userToRemove', verifyAuth(), (req, res) => {
|
||||||
|
if (!req.user.admin) return res.redirect('/');
|
||||||
|
res.render('remove', { userToRemove: req.params.userToRemove });
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post('/remove/:userToRemove', verifyAuth(), async (req, res) => {
|
||||||
|
if (!req.user.admin) return res.redirect('/');
|
||||||
|
const doc = await db.get(req.params.userToRemove);
|
||||||
|
if (doc.admin) {
|
||||||
|
req.flash('error', 'Failed to remove: user is admin.');
|
||||||
|
return res.redirect('/admin-settings');
|
||||||
|
}
|
||||||
|
await db.remove(doc);
|
||||||
|
const docs = await db.allDocs({ include_docs: true });
|
||||||
|
for (let i = 0; i < docs.length; i++) {
|
||||||
|
for (let j = 0; j < docs[i].doc.wishlist.length; j++) {
|
||||||
|
if (docs[i].doc.wishlist[j].pledgedBy === req.params.userToRemove) {
|
||||||
|
docs[i].doc.wishlist[j].pledgedBy === undefined;
|
||||||
|
if (docs[i].doc.wishlist[j].addedBy === req.params.userToRemove) await db.remove(doc);
|
||||||
|
else await db.put(docs[i].doc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
req.flash('success', `Successfully removed user ${req.params.userToRemove}`);
|
||||||
|
res.redirect('/admin-settings')
|
||||||
|
});
|
||||||
|
|
||||||
|
return router;
|
||||||
|
};
|
37
routes/index.js
Normal file
37
routes/index.js
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
const verifyAuth = require('../middlewares/verifyAuth');
|
||||||
|
const express = require('express');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
module.exports = (db) => {
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
router.use('/', express.static(path.join(__dirname, '../static')));
|
||||||
|
|
||||||
|
router.get('/',
|
||||||
|
async (req, res, next) => {
|
||||||
|
dbInfo = await db.info();
|
||||||
|
if (dbInfo.doc_count === 0) {
|
||||||
|
res.redirect('/setup');
|
||||||
|
} else {
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
verifyAuth(),
|
||||||
|
(req, res) => {
|
||||||
|
res.redirect('/wishlist');
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
router.use('/setup', require('./setup')(db));
|
||||||
|
|
||||||
|
router.use('/login', require('./login')());
|
||||||
|
router.use('/logout', require('./logout')());
|
||||||
|
|
||||||
|
router.use('/wishlist', require('./wishlist')(db));
|
||||||
|
|
||||||
|
router.use('/profile', require('./profile')(db));
|
||||||
|
|
||||||
|
router.use('/admin-settings', require('./adminSettings')(db));
|
||||||
|
|
||||||
|
return router;
|
||||||
|
}
|
29
routes/login/index.js
Normal file
29
routes/login/index.js
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
const passport = require('passport');
|
||||||
|
const express = require('express');
|
||||||
|
|
||||||
|
module.exports = () => {
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
router.get('/',
|
||||||
|
(req, res) => {
|
||||||
|
if (req.isAuthenticated()) {
|
||||||
|
res.redirect('/');
|
||||||
|
} else {
|
||||||
|
res.render('login');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
'/',
|
||||||
|
(req, res, next) => {
|
||||||
|
next();
|
||||||
|
},
|
||||||
|
passport.authenticate('local', {
|
||||||
|
successRedirect: '/',
|
||||||
|
failureRedirect: '/login',
|
||||||
|
failureFlash: 'Invalid username or password'
|
||||||
|
})
|
||||||
|
);
|
||||||
|
return router;
|
||||||
|
};
|
14
routes/logout/index.js
Normal file
14
routes/logout/index.js
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
const verifyAuth = require('../../middlewares/verifyAuth');
|
||||||
|
const express = require('express');
|
||||||
|
|
||||||
|
module.exports = () => {
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
router.get('/', verifyAuth(), (req, res) => res.render('logout'));
|
||||||
|
router.post('/', (req, res) => {
|
||||||
|
req.logout();
|
||||||
|
res.redirect('/');
|
||||||
|
});
|
||||||
|
|
||||||
|
return router;
|
||||||
|
};
|
39
routes/profile/index.js
Normal file
39
routes/profile/index.js
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
const verifyAuth = require('../../middlewares/verifyAuth');
|
||||||
|
const bcrypt = require('bcrypt-nodejs');
|
||||||
|
const express = require('express');
|
||||||
|
|
||||||
|
module.exports = (db) => {
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
router.get('/', verifyAuth(), (req, res) => res.render('profile', { title: `Profile Settings - ${req.user._id}`}));
|
||||||
|
router.post('/', verifyAuth(), (req, res) => {
|
||||||
|
if (req.body.oldPassword && req.body.newPassword) {
|
||||||
|
bcrypt.compare(req.body.oldPassword, req.user.password, (err, correct) => {
|
||||||
|
if (err) throw err;
|
||||||
|
if (correct) {
|
||||||
|
bcrypt.hash(req.body.newPassword, null, null, (err, hash) => {
|
||||||
|
if (err) throw err;
|
||||||
|
db.get(req.user._id)
|
||||||
|
.then(doc => {
|
||||||
|
doc.password = hash;
|
||||||
|
db.put(doc)
|
||||||
|
.then(() => {
|
||||||
|
req.flash('success', 'Changes saved successfully!');
|
||||||
|
res.redirect('/profile');
|
||||||
|
})
|
||||||
|
.catch(err => { throw err; });
|
||||||
|
})
|
||||||
|
.catch(err => { throw err; });
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
req.flash('error', 'Incorrect old password');
|
||||||
|
res.redirect('/profile');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
res.redirect('/profile');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return router;
|
||||||
|
};
|
39
routes/setup/index.js
Normal file
39
routes/setup/index.js
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
const bcrypt = require('bcrypt-nodejs')
|
||||||
|
const express = require('express');
|
||||||
|
|
||||||
|
module.exports = (db) => {
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
router.get('/',
|
||||||
|
async (req, res) => {
|
||||||
|
const dbInfo = await db.info();
|
||||||
|
if (dbInfo.doc_count === 0) {
|
||||||
|
res.render('setup', { title: 'Setup' });
|
||||||
|
} else {
|
||||||
|
res.redirect('/');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
router.post('/',
|
||||||
|
async (req, res) => {
|
||||||
|
const dbInfo = await db.info();
|
||||||
|
if (dbInfo.doc_count === 0) {
|
||||||
|
bcrypt.hash(req.body.adminPassword, null, null, (err, adminPasswordHash) => {
|
||||||
|
if (err) throw err;
|
||||||
|
db.put({
|
||||||
|
_id: req.body.adminUsername,
|
||||||
|
password: adminPasswordHash,
|
||||||
|
admin: true,
|
||||||
|
wishlist: []
|
||||||
|
})
|
||||||
|
res.redirect('/');
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
res.redirect('/');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return router;
|
||||||
|
}
|
119
routes/wishlist/index.js
Normal file
119
routes/wishlist/index.js
Normal file
|
@ -0,0 +1,119 @@
|
||||||
|
const verifyAuth = require('../../middlewares/verifyAuth');
|
||||||
|
const getProductName = require('get-product-name');
|
||||||
|
const bcrypt = require('bcrypt-nodejs');
|
||||||
|
const express = require('express');
|
||||||
|
const uuid = require('uuid/v4');
|
||||||
|
|
||||||
|
const totals = wishlist => {
|
||||||
|
let unpledged = 0;
|
||||||
|
let pledged = 0;
|
||||||
|
wishlist.forEach(wishItem => {
|
||||||
|
if (wishItem.pledgedBy) pledged += 1;
|
||||||
|
else unpledged += 1;
|
||||||
|
});
|
||||||
|
return { unpledged, pledged };
|
||||||
|
};
|
||||||
|
|
||||||
|
const ValidURL = (string) => { // Ty SO
|
||||||
|
try {
|
||||||
|
new URL(string);
|
||||||
|
return true;
|
||||||
|
} catch (_) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = (db) => {
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
router.get('/', verifyAuth(), async (req, res) => {
|
||||||
|
const docs = await db.allDocs({ include_docs: true })
|
||||||
|
res.render('wishlists', { title: 'Wishlists', users: docs.rows, totals})
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get('/:user', verifyAuth(), async (req, res) => {
|
||||||
|
try {
|
||||||
|
const dbUser = await db.get(req.params.user);
|
||||||
|
res.render('wishlist', { title: `Wishlist - ${dbUser._id}`, wishlist: dbUser.wishlist });
|
||||||
|
} catch (error) {
|
||||||
|
res.redirect('/wishlist');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post('/:user', verifyAuth(), async (req, res) => {
|
||||||
|
const isUrl = ValidURL(req.body.itemUrlOrName);
|
||||||
|
const item = {};
|
||||||
|
let productData;
|
||||||
|
try {
|
||||||
|
if (isUrl) productData = await getProductName(req.body.itemUrlOrName);
|
||||||
|
} catch (err) {}
|
||||||
|
item.name = (productData ? productData.name : req.body.itemUrlOrName);
|
||||||
|
item.addedBy = req.user._id;
|
||||||
|
item.pledgedBy = (req.user._id === req.params.user ? undefined : req.user._id);
|
||||||
|
if (isUrl) item.url = req.body.itemUrlOrName;
|
||||||
|
item.id = uuid();
|
||||||
|
const doc = await db.get(req.params.user);
|
||||||
|
doc.wishlist.push(item);
|
||||||
|
await db.put(doc);
|
||||||
|
req.flash('success', (req.user._id === req.params.user ? 'Added item to wishlist' : `Pleged item for ${req.params.user}`));
|
||||||
|
res.redirect(`/wishlist/${req.params.user}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post('/:user/pledge/:itemId', verifyAuth(), async (req, res) => {
|
||||||
|
const docs = await db.allDocs({ include_docs: true });
|
||||||
|
for (let i = 0; i < docs.rows.length; i++) {
|
||||||
|
for (let j = 0; j < docs.rows[i].doc.wishlist.length; j++) {
|
||||||
|
if (docs.rows[i].doc.wishlist[j].id === req.params.itemId) {
|
||||||
|
if (docs.rows[i].doc.wishlist[j].pledgedBy !== undefined) {
|
||||||
|
req.flash('error', 'Item already pledged for');
|
||||||
|
return res.redirect(`/wishlist/${req.params.user}`);
|
||||||
|
}
|
||||||
|
docs.rows[i].doc.wishlist[j].pledgedBy = req.user._id;
|
||||||
|
await db.put(docs.rows[i].doc);
|
||||||
|
req.flash('success', 'Successfully pledged for item!');
|
||||||
|
return res.redirect(`/wishlist/${req.params.user}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
router.post('/:user/unpledge/:itemId', verifyAuth(), async (req, res) => {
|
||||||
|
const docs = await db.allDocs({ include_docs: true });
|
||||||
|
for (let i = 0; i < docs.rows.length; i++) {
|
||||||
|
for (let j = 0; j < docs.rows[i].doc.wishlist.length; j++) {
|
||||||
|
if (docs.rows[i].doc.wishlist[j].id === req.params.itemId) {
|
||||||
|
if (docs.rows[i].doc.wishlist[j].pledgedBy !== req.user._id) {
|
||||||
|
req.flash('error', 'You did not pledge for this');
|
||||||
|
return res.redirect(`/wishlist/${req.params.user}`);
|
||||||
|
}
|
||||||
|
docs.rows[i].doc.wishlist[j].pledgedBy = undefined;
|
||||||
|
if (docs.rows[i].doc.wishlist[j].addedBy === req.user._id) docs.rows[i].doc.wishlist.pop(j);
|
||||||
|
await db.put(docs.rows[i].doc);
|
||||||
|
req.flash('success', 'Successfully unpledged for item');
|
||||||
|
return res.redirect(`/wishlist/${req.params.user}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
req.flash('error', 'Failed to find item');
|
||||||
|
return res.redirect(`/wishlist/${req.params.user}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post('/:user/remove/:itemId', verifyAuth(), async (req, res) => {
|
||||||
|
if (req.user._id !== req.params.user) {
|
||||||
|
req.flash('error', 'Not correct user');
|
||||||
|
return res.redirect(`/wishlists/${req.params.user}`);
|
||||||
|
}
|
||||||
|
const doc = await db.get(req.user._id);
|
||||||
|
for (let i = 0; i < doc.wishlist.length; i++) {
|
||||||
|
if (doc.wishlist[i].id === req.params.itemId) {
|
||||||
|
doc.wishlist.pop(i);
|
||||||
|
await db.put(doc);
|
||||||
|
req.flash('success', 'Successfully removed from wishlist');
|
||||||
|
return res.redirect(`/wishlist/${req.params.user}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
req.flash('error', 'Failed to find item');
|
||||||
|
return res.redirect(`/wishlist/${req.params.user}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
return router;
|
||||||
|
};
|
14
static/css/main.css
Normal file
14
static/css/main.css
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
@import "https://cdnjs.cloudflare.com/ajax/libs/bulma/0.7.0/css/bulma.min.css";
|
||||||
|
|
||||||
|
.inline {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.linkButton {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul.noStyle li {
|
||||||
|
list-style-type: none;
|
||||||
|
}
|
BIN
static/img/logo.png
Normal file
BIN
static/img/logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 98 KiB |
8
static/js/nav.js
Normal file
8
static/js/nav.js
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
window.onload = () => {
|
||||||
|
const burger = document.getElementById('navBarBurger');
|
||||||
|
const navBarMenu = document.getElementById('navBarMenu');
|
||||||
|
burger.addEventListener('click', () => {
|
||||||
|
burger.classList.toggle('is-active');
|
||||||
|
navBarMenu.classList.toggle('is-active');
|
||||||
|
});
|
||||||
|
};
|
30
views/adminSettings.pug
Normal file
30
views/adminSettings.pug
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
extends layout.pug
|
||||||
|
|
||||||
|
block content
|
||||||
|
h2 Users
|
||||||
|
each user in users
|
||||||
|
span.is-size-6.inline= user.id
|
||||||
|
if !user.doc.admin
|
||||||
|
a(href=`/admin-settings/remove/${user.id}`)
|
||||||
|
span.is-size-7.icon.has-text-danger
|
||||||
|
i.fas.fa-times
|
||||||
|
span.is-sr-only
|
||||||
|
Remove
|
||||||
|
br
|
||||||
|
h3 Add user
|
||||||
|
form(action='/admin-settings/add', method='POST')
|
||||||
|
.field
|
||||||
|
label.label Username
|
||||||
|
.control.has-icons-left
|
||||||
|
input.input(type='text', name='newUserUsername', placeholder='john')
|
||||||
|
span.icon.is-small.is-left
|
||||||
|
i.fas.fa-user
|
||||||
|
.field
|
||||||
|
label.label Password
|
||||||
|
.control.has-icons-left
|
||||||
|
input.input(type='password', name='newUserPassword', placeholder='pa$$word!')
|
||||||
|
span.icon.is-small.is-left
|
||||||
|
i.fas.fa-lock
|
||||||
|
.field
|
||||||
|
.control
|
||||||
|
input.button.is-primary(type='submit' value='Add User')
|
6
views/includes/messages.pug
Normal file
6
views/includes/messages.pug
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
if successes
|
||||||
|
each success in successes
|
||||||
|
p.has-text-success= success
|
||||||
|
if errors
|
||||||
|
each error in errors
|
||||||
|
p.has-text-danger= error
|
36
views/includes/navbar.pug
Normal file
36
views/includes/navbar.pug
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
mixin navBarLink(href, title)
|
||||||
|
if href === req.path
|
||||||
|
a.is-active.navbar-item(href=href)= title
|
||||||
|
else
|
||||||
|
a.navbar-item(href=href)= title
|
||||||
|
nav.navbar(role='navigation', aria-label='main navigation')
|
||||||
|
.navbar-brand
|
||||||
|
if '/' === req.path
|
||||||
|
a.is-active.navbar-item(href='/')
|
||||||
|
img(src='/img/logo.png', alt='')
|
||||||
|
span #{config.siteTitle}
|
||||||
|
else
|
||||||
|
a.navbar-item(href='/')
|
||||||
|
img(src='/img/logo.png', alt='')
|
||||||
|
span #{config.siteTitle}
|
||||||
|
a.navbar-burger#navBarBurger(role='button', aria-label='menu', aria-expanded='false')
|
||||||
|
span(aria-hidden='true')
|
||||||
|
span(aria-hidden='true')
|
||||||
|
span(aria-hidden='true')
|
||||||
|
.navbar-menu#navBarMenu
|
||||||
|
.navbar-start
|
||||||
|
.navbar-end
|
||||||
|
if req.isAuthenticated()
|
||||||
|
.navbar-item.has-dropdown.is-hoverable
|
||||||
|
a.navbar-link= req.user._id
|
||||||
|
.navbar-dropdown
|
||||||
|
+navBarLink(`/wishlist/${req.user._id}`, 'My Wishlist')
|
||||||
|
+navBarLink('/profile', 'Profile')
|
||||||
|
if req.user.admin
|
||||||
|
+navBarLink('/admin-settings', 'Admin settings')
|
||||||
|
hr.navbar-divider
|
||||||
|
.navbar-item
|
||||||
|
form#logoutForm(action='/logout', method='POST')
|
||||||
|
button.linkButton(type='submit') Log Out
|
||||||
|
//-+navBarLink('javascript:document.getElementById("logoutForm").submit()', 'Log Out')
|
||||||
|
script(src="/js/nav.js")
|
28
views/layout.pug
Normal file
28
views/layout.pug
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
doctype html
|
||||||
|
html(lang='en')
|
||||||
|
head
|
||||||
|
meta(name='viewport', content='width=device-width, initial-scale=1')
|
||||||
|
if title
|
||||||
|
title #{config.siteTitle} - #{title}
|
||||||
|
else
|
||||||
|
title #{config.siteTitle}
|
||||||
|
link(rel='stylesheet', href='/css/main.css')
|
||||||
|
noscript
|
||||||
|
link(
|
||||||
|
rel='stylesheet',
|
||||||
|
href='https://use.fontawesome.com/releases/v5.0.10/css/all.css',
|
||||||
|
integrity='sha384-+d0P83n9kaQMCwj8F4RJB66tzIwOKmrdb46+porD/OvrJ+37WqIM7UoBtwHO6Nlg',
|
||||||
|
crossorigin='anonymous'
|
||||||
|
)
|
||||||
|
script(defer, src='https://use.fontawesome.com/releases/v5.0.7/js/all.js')
|
||||||
|
body
|
||||||
|
include includes/navbar.pug
|
||||||
|
div.content#pageContent
|
||||||
|
section.section
|
||||||
|
div.container
|
||||||
|
if title
|
||||||
|
h1= config.siteTitle + ' - ' + title
|
||||||
|
else if title !== false
|
||||||
|
h1 #{config.siteTitle}
|
||||||
|
include includes/messages.pug
|
||||||
|
block content
|
20
views/login.pug
Normal file
20
views/login.pug
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
extends layout.pug
|
||||||
|
|
||||||
|
block content
|
||||||
|
form(method='POST')
|
||||||
|
.field
|
||||||
|
label.label Username
|
||||||
|
.control.has-icons-left
|
||||||
|
input.input(type='text', name='username', placeholder='john')
|
||||||
|
span.icon.is-small.is-left
|
||||||
|
i.fas.fa-user
|
||||||
|
.field
|
||||||
|
label.label Password
|
||||||
|
.control.has-icons-left
|
||||||
|
input.input(type='password', name='password', placeholder='pa$$word!')
|
||||||
|
span.icon.is-small.is-left
|
||||||
|
i.fas.fa-lock
|
||||||
|
.field
|
||||||
|
.control
|
||||||
|
input.button.is-primary(type='submit' value='Log In')
|
||||||
|
|
7
views/logout.pug
Normal file
7
views/logout.pug
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
extends layout.pug
|
||||||
|
|
||||||
|
block content
|
||||||
|
form(method='POST')
|
||||||
|
.field
|
||||||
|
.control
|
||||||
|
input.button.is-primary(type='submit' value='Log Out')
|
20
views/profile.pug
Normal file
20
views/profile.pug
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
extends layout.pug
|
||||||
|
|
||||||
|
block content
|
||||||
|
form(method='POST')
|
||||||
|
.field
|
||||||
|
label.label Old Password (Required if changing password)
|
||||||
|
.control.has-icons-left
|
||||||
|
input.input(type='password', name='oldPassword', placeholder='pa$$word!')
|
||||||
|
span.icon.is-small.is-left
|
||||||
|
i.fas.fa-lock
|
||||||
|
.field
|
||||||
|
label.label New Password (Leave blank if not changing password)
|
||||||
|
.control.has-icons-left
|
||||||
|
input.input(type='password', name='newPassword', placeholder='pa$$word!')
|
||||||
|
span.icon.is-small.is-left
|
||||||
|
i.fas.fa-lock
|
||||||
|
.field
|
||||||
|
.control
|
||||||
|
input.button.is-primary(type='submit' value='Save')
|
||||||
|
|
7
views/remove.pug
Normal file
7
views/remove.pug
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
extends layout.pug
|
||||||
|
|
||||||
|
block content
|
||||||
|
form(method='POST')
|
||||||
|
.field
|
||||||
|
.control
|
||||||
|
input.button.is-danger(type='submit' value=`Remove user ${userToRemove}`)
|
20
views/setup.pug
Normal file
20
views/setup.pug
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
extends layout.pug
|
||||||
|
|
||||||
|
block content
|
||||||
|
h2 Admin User
|
||||||
|
form(action='/setup', method='POST')
|
||||||
|
.field
|
||||||
|
label.label Username
|
||||||
|
.control.has-icons-left
|
||||||
|
input.input(type='text', name='adminUsername', placeholder='john')
|
||||||
|
span.icon.is-small.is-left
|
||||||
|
i.fas.fa-user
|
||||||
|
.field
|
||||||
|
label.label Password
|
||||||
|
.control.has-icons-left
|
||||||
|
input.input(type='password', name='adminPassword', placeholder='pa$$word!')
|
||||||
|
span.icon.is-small.is-left
|
||||||
|
i.fas.fa-lock
|
||||||
|
.field
|
||||||
|
.control
|
||||||
|
input.button.is-primary(type='submit' value='Set up!')
|
36
views/wishlist.pug
Normal file
36
views/wishlist.pug
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
extends layout.pug
|
||||||
|
|
||||||
|
block content
|
||||||
|
for item in wishlist
|
||||||
|
if req.user._id === item.addedBy || req.params.user !== req.user._id
|
||||||
|
.box
|
||||||
|
if item.url
|
||||||
|
span
|
||||||
|
a(href=item.url, rel='noopener noreferrer', target='_blank')= (item.name ? item.name : item.url)
|
||||||
|
if req.params.user !== req.user._id && !item.pledgedBy
|
||||||
|
form(method='POST', action=`/wishlist/${req.params.user}/pledge/${item.id}`)
|
||||||
|
.field
|
||||||
|
.control
|
||||||
|
input.button.is-primary(type='submit' value='Pledge')
|
||||||
|
if item.pledgedBy === req.user._id
|
||||||
|
form(method='POST', action=`/wishlist/${req.params.user}/unpledge/${item.id}`)
|
||||||
|
.field
|
||||||
|
.control
|
||||||
|
input.button(type='submit' value='Unpledge')
|
||||||
|
if req.user._id === req.params.user
|
||||||
|
form(method='POST', action=`/wishlist/${req.params.user}/remove/${item.id}`)
|
||||||
|
.field
|
||||||
|
.control
|
||||||
|
input.button.is-warning(type='submit' value='Remove')
|
||||||
|
else
|
||||||
|
span= item.name
|
||||||
|
form(method='POST')
|
||||||
|
.field
|
||||||
|
label.label Item URL or Name
|
||||||
|
.control.has-icons-left
|
||||||
|
input.input(type='text', name='itemUrlOrName', placeholder='https://www.amazon.com/dp/B00ZV9RDKK')
|
||||||
|
span.icon.is-small.is-left
|
||||||
|
i.fas.fa-gift
|
||||||
|
.field
|
||||||
|
.control
|
||||||
|
input.button(type='submit' value=(req.user._id === req.params.user ? 'Add item to wishlist' : 'Pledge item'))
|
12
views/wishlists.pug
Normal file
12
views/wishlists.pug
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
extends layout.pug
|
||||||
|
|
||||||
|
block content
|
||||||
|
ul.noStyle
|
||||||
|
each user in users
|
||||||
|
if req.user._id !== user.id
|
||||||
|
li
|
||||||
|
a(href=`/wishlist/${user.id}`)
|
||||||
|
.box
|
||||||
|
span= user.id
|
||||||
|
span= `: ${totals(user.doc.wishlist).pledged}/${user.doc.wishlist.length}`
|
||||||
|
progress.progress.is-info(value=totals(user.doc.wishlist).pledged, max=user.doc.wishlist.length)
|
Loading…
Reference in a new issue