- Initial commit... A DB, some routes, and basic authentication routines...

This commit is contained in:
2019-07-04 16:19:30 -04:00
commit d9a2d33913
32 changed files with 3465 additions and 0 deletions

30
config.js Normal file
View File

@@ -0,0 +1,30 @@
module.exports = {
name: 'wEvent API',
env: process.env.NODE_ENV || 'development',
port: process.env.PORT || 3001,
base_url: process.env.BASE_URL || 'http://localhost:3000',
db: {
uri: process.env.MONGODB_URI || 'mongodb://127.0.0.1:27017/wEvent-dev',
},
version: '0.0.1',
assetStoreUrl: 'https://www.google.com/',
services: {
apple: {},
facebook: {
appId: '2359355590971136',
appSecret: 'a5703f7d0af8e694aec5bd4175a85d6b',
},
google: {
appId: '442412638360-p0idffou0qlpgor7agideudb1dh10mpf.apps.googleusercontent.com',
appSecret: 'a7fmS7Wc9Ssycr21WXdQ4TYl',
},
},
security: {
jwt: {
audience: 'wEvents.io',
daysValid: 365,
issuer: 'patrons.wEvents.io',
secret: 'Th!sIs a d3v3lopm3nt server $#cr¢T.',
},
},
};

66
index.js Normal file
View File

@@ -0,0 +1,66 @@
const jwt = require('jsonwebtoken');
const mongoose = require('mongoose');
const passport = require('passport');
const restify = require('restify');
const config = require('./config');
const auth = require('./strategies/auth')(passport);
const validateJsonData = require('./lib/validateType.js');
/**
* Initialize Server
*/
const server = restify.createServer({
name: config.name,
version: config.version,
});
/**
* Middleware
*/
server.use(restify.plugins.acceptParser(server.acceptable));
server.use(restify.plugins.bodyParser({
hash: 'sha1',
mapParams: true,
multiples: true,
}));
server.use(restify.plugins.fullResponse());
server.use(restify.plugins.gzipResponse());
server.use(restify.plugins.queryParser({ mapParams: true }));
server.use(auth.passport.initialize());
/**
* Error checking
*/
server.post('*', validateJsonData);
server.put('*', validateJsonData);
/**
* Start Server, Connect to DB & Require Routes
*/
server.listen(config.port, () => {
// establish connection to mongodb
mongoose.Promise = global.Promise;
mongoose.connect(
config.db.uri,
{
useCreateIndex: true,
useFindAndModify: false,
useNewUrlParser: true,
},
);
const db = mongoose.connection;
db.on('error', (err) => {
console.error(err);
process.exit(1);
});
db.once('open', () => {
require('./routes')(server, auth);
console.log(`Server is listening on port ${config.port}`);
});
});

11
lib/validateType.js Normal file
View File

@@ -0,0 +1,11 @@
const validateJsonData = (req, res, next) => {
if (!req.is('application/json')) {
return next(
new errors.InvalidContentError("Expects 'application/json'"),
);
}
next();
};
module.exports = validateJsonData;

36
models/bid.js Normal file
View File

@@ -0,0 +1,36 @@
const mongoose = require('mongoose');
const timestamps = require('mongoose-timestamp');
const BidSchema = new mongoose.Schema(
{
itemId: {
type: String,
required: true,
trim: true,
},
bidderId: {
type: String,
required: true,
trim: true,
},
bidAmount: {
type: Number,
required: true,
},
maxAmount: {
type: Number,
required: true,
},
time: {
type: Number,
required: true,
},
},
{ minimize: false },
);
BidSchema.plugin(timestamps);
const Bid = mongoose.model('Bid', BidSchema);
module.exports = Bid;

44
models/common/address.js Normal file
View File

@@ -0,0 +1,44 @@
const mongoose = require('mongoose');
const mongooseStringQuery = require('mongoose-string-query');
const mongooseTimestamps = require('mongoose-timestamp');
const AddressSchema = new mongoose.Schema(
{
address1: {
type: String,
required: true,
trim: true,
},
address2: {
type: String,
trim: true,
},
locality: {
type: String,
required: true,
trim: true,
},
state: {
type: String,
required: true,
trim: true,
},
postalCode: {
type: String,
required: true,
trim: true,
},
label: {
type: String,
required: true,
enum: [ 'billing', 'shipping' ],
},
},
{ minimize: false },
);
AddressSchema.plugin(mongooseStringQuery);
AddressSchema.plugin(mongooseTimestamps);
module.exports = AddressSchema;

30
models/common/email.js Normal file
View File

@@ -0,0 +1,30 @@
const mongoose = require('mongoose');
const mongooseStringQuery = require('mongoose-string-query');
const mongooseTimestamps = require('mongoose-timestamp');
const EmailSchema = new mongoose.Schema(
{
user: {
type: String,
required: true,
},
domain: {
type: String,
required: true,
},
label: {
type: String,
},
},
{ minimize: false },
);
EmailSchema.virtual('address').get(function() {
return this.user + '@' + this.domain;
});
EmailSchema.plugin(mongooseStringQuery);
EmailSchema.plugin(mongooseTimestamps);
module.exports = EmailSchema;

29
models/common/phone.js Normal file
View File

@@ -0,0 +1,29 @@
const mongoose = require('mongoose');
const mongooseStringQuery = require('mongoose-string-query');
const mongooseTimestamps = require('mongoose-timestamp');
const PhoneSchema = new mongoose.Schema(
{
countryCode: {
type: String,
required: true,
default: '1',
},
number: {
type: String,
required: true,
},
label: {
type: String,
required: true,
enum: [ 'home', 'mobile' ],
},
},
{ minimize: false },
);
PhoneSchema.plugin(mongooseStringQuery);
PhoneSchema.plugin(mongooseTimestamps);
module.exports = PhoneSchema;

16
models/constants.js Normal file
View File

@@ -0,0 +1,16 @@
module.exports = {
ITEM_TYPES: {
type: String,
required: true,
enum: [
'auction',
'donation',
'event',
'raffle',
'membership',
'popup',
'ticket',
],
default: 'auction',
},
};

147
models/event.js Normal file
View File

@@ -0,0 +1,147 @@
const config = require('../config.js');
const mongoose = require('mongoose');
const timestamps = require('mongoose-timestamp');
const PostSchema = new mongoose.Schema(
{
author: String,
title: {
type: String,
required: true,
},
content: {
type: String,
required: true,
},
isPublic: {
type: Boolean,
default: true,
},
scheduledPost: String,
sendNotification: {
type: Boolean,
default: false,
},
notificationContent: String,
notificationLinksTo: String,
}
);
const TicketSchema = new mongoose.Schema(
{
name: {
type: String,
required: true,
trim: true,
},
price: {
type: Number,
default: 0,
},
capacity: {
type: Number,
required: true,
},
available: {
type: Number,
required: true,
},
itemId: {
type: String,
required: true,
trim: true,
},
startSale: {
type: String,
required: true,
trim: true,
},
endSale: {
type: String,
required: true,
trim: true,
},
},
{ minimize: false },
);
const EventSchema = new mongoose.Schema(
{
eventDate: {
type: String,
required: true,
trim: true,
},
startTime: {
type: String,
required: true,
trim: true,
},
endTime: {
type: Number,
required: true,
},
title: {
type: String,
required: true,
trim: true,
},
tagline: {
type: String,
required: true,
trim: true,
},
description: {
type: String,
required: true,
trim: true,
},
isTicketed: {
type: Boolean,
default: false,
},
ticketClasses: [ TicketSchema ],
showFrom: {
type: String,
required: true,
trim: true,
},
showUntil: {
type: String,
required: true,
trim: true,
},
requireLoginToSeeAuction: {
type: Boolean,
default: false,
},
images: [{
url: String,
}],
url: String,
posts: [ PostSchema ],
},
{ minimize: false },
);
EventSchema.plugin(timestamps);
EventSchema.path('images').get(v => `${config.assetStoreUrl}${v.url}`)
const Event = mongoose.model('Event', EventSchema);
module.exports = Event;

49
models/install.js Normal file
View File

@@ -0,0 +1,49 @@
const mongoose = require('mongoose');
const timestamps = require('mongoose-timestamp');
const InstallSchema = new mongoose.Schema(
{
timeZone: {
type: String,
required: true,
trim: true,
},
deviceType: {
type: String,
required: true,
trim: true,
enum: [ 'android', 'ios', 'web' ],
},
badge: {
type: Number,
default: 0,
},
installationId: {
type: String,
required: true,
unique: true,
},
email: {
type: String,
required: true,
trim: true,
},
appIdentifier: {
type: String,
required: true,
trim: true,
},
localeIdentifier: {
type: String,
required: true,
trim: true,
},
},
{ minimize: false },
);
InstallSchema.plugin(timestamps);
const Install = mongoose.model('Install', InstallSchema);
module.exports = Install;

136
models/item.js Normal file
View File

@@ -0,0 +1,136 @@
const { ITEM_TYPES } = require('./constants.js');
const config = require('../config.js');
const mongoose = require('mongoose');
const mongooseStringQuery = require('mongoose-string-query');
const timestamps = require('mongoose-timestamp');
const ItemSchema = new mongoose.Schema(
{
eventId: {
type: String,
required: true,
trim: true,
},
title: {
type: String,
required: true,
trim: true,
},
subtitle: {
type: String,
trim: true,
},
donor: {
type: String,
trim: true,
},
description: {
type: String,
required: true,
trim: true,
},
images: [{
url: String
}],
type: ITEM_TYPES,
quantityAvailable: {
type: Number,
required: true,
default: 1,
},
soldCount: {
type: Number,
default: 0,
},
currentPrice: {
required: true,
type: Number,
},
startingPrice: {
type: Number,
},
reservePrice: {
type: Number,
},
estimatedValue: {
type: Number,
},
currentWinner: {
type: String,
trim: true,
},
bidders: [{
name: String,
}],
bidCount: {
type: Number,
default: 0,
},
bidIncrement: {
type: Number,
default: 10,
},
catalogNumber: {
type: Number,
},
start: {
type: Number,
required: true,
},
end: {
type: Number,
required: true,
},
hideBeforeStart: {
type: Boolean,
required: true,
default: false,
},
hideAfterEnd: {
type: Boolean,
required: true,
default: false,
},
notifyOnAvailable: {
type: Boolean,
required: true,
default: false,
},
isShippable: {
type: Boolean,
default: false,
},
shippingCost: Number,
organizationTake: {
type: Number,
required: true,
default: 1,
},
},
{ minimize: false },
);
ItemSchema.plugin(timestamps);
ItemSchema.path('images').get(v => `${config.assetStoreUrl}${v.url}`);
/**
* STATICS
*/
ItemSchema.statics.addBatch = function(data = [], callback = () => {}) {
};
const Item = mongoose.model('Item', ItemSchema);
module.exports = Item;

63
models/organization.js Normal file
View File

@@ -0,0 +1,63 @@
const mongoose = require('mongoose');
const timestamps = require('mongoose-timestamp');
const PeopleSchema = new mongoose.Schema(
{
name: {
type: String,
required: true,
trim: true,
},
title: {
type: String,
required: true,
trim: true,
},
bio: {
type: String,
trim: true,
},
address: AddressSchema,
email: EmailSchema,
phone: PhoneSchema,
},
{ minimize: false },
);
const OrganizationSchema = new mongoose.Schema(
{
name: {
type: String,
required: true,
trim: true,
},
url: String,
address: [ AddressSchema ],
telephone: [ TelephoneSchema ],
about: {
type: String,
required: true,
trim: true,
},
team: [ PeopleSchema ],
board: [ PeopleSchema ],
privacyUrl: String,
tosUrl: String,
copyright: String,
},
{ minimize: false },
);
OrganizationSchema.plugin(timestamps);
const Organization = mongoose.model('Organization', OrganizationSchema);
module.exports = Organization;

44
models/sale.js Normal file
View File

@@ -0,0 +1,44 @@
const { ITEM_TYPES } = require('./constants.js');
const mongoose = require('mongoose');
const timestamps = require('mongoose-timestamp');
const SaleSchema = new mongoose.Schema(
{
itemId: {
type: String,
required: true,
trim: true,
},
userId: {
type: String,
required: true,
trim: true,
},
amount: {
type: Number,
required: true,
},
itemType: ITEM_TYPES,
paymentToken: String,
isPaid: {
type: Boolean,
required: true,
default: false,
},
isPickedUp: {
type: Boolean,
required: true,
default: false,
},
},
{ minimize: false },
);
SaleSchema.plugin(timestamps);
const Sale = mongoose.model('Sale', SaleSchema);
module.exports = Sale;

284
models/user.js Normal file
View File

@@ -0,0 +1,284 @@
const crypto = require('crypto');
const jwt = require('jsonwebtoken');
const mongoose = require('mongoose');
const timestamps = require('mongoose-timestamp');
const config = require('../config.js');
const AddressSchema = require('./common/address.js');
const PhoneSchema = require('./common/phone.js');
const LoginSchema = new mongoose.Schema(
{
method: {
type: String,
required: true,
enum: [ 'apple', 'facebook', 'google', 'local' ],
},
userId: {
type: String,
trim: true,
},
accessToken: {
type: String,
required: true,
trim: true,
},
secret: {
type: String,
trim: true,
},
profile: {},
},
{ minimize: false },
);
const UserSchema = new mongoose.Schema(
{
nomDeBid: {
type: String,
trim: true,
unique: true,
},
firstName: {
type: String,
required: true,
trim: true,
},
lastName: {
type: String,
required: true,
trim: true,
},
email: {
type: String,
required: true,
unique: true,
},
avatar: {
type: String,
trim: true,
},
address: [ AddressSchema ],
phone: [ PhoneSchema ],
credentials: [ LoginSchema ],
organizationIdentifier: {
type: String,
trim: true,
},
paymentToken: {
type: String,
trim: true,
},
isVerified: {
type: Boolean,
default: false,
},
isAllowedToBid: {
type: Boolean,
default: false,
},
isOrganizationEmployee: {
type: Boolean,
default: false,
},
},
{ minimize: false },
);
/**
* PLUGINS
*/
UserSchema.plugin(timestamps);
/**
* METHODS
*/
UserSchema.methods.authenticate = function (username, password) {
const user = this.model('User').findOne({ email: username });
const strategy = user ? user.getAuthStrategy('local') : null;
if (strategy) {
let hash = crypto.pbkdf2Sync(password, strategy.get('secret'), 10000, 512, 'sha512').toString('hex')
return strategy.get('accessToken') === hash;
}
return false;
};
UserSchema.methods.generateJWT = function (props = {}) {
const { exp, iss } = props;
const today = new Date();
let expirationDate = exp;
if (!expirationDate) {
expirationDate = new Date(today);
expirationDate.setDate(today.getDate() + config.security.jwt.daysValid);
}
return jwt.sign({
sub: this._id,
iss: iss || config.security.jwt.issuer,
aud: config.security.jwt.audience,
iat: parseInt(today.getTime()),
exp: parseInt(expirationDate.getTime() / 1000, 10),
}, config.security.jwt.secret);
}
UserSchema.methods.getAuthStrategy = function (method = 'local') {
return this.credentials.filter((strategy) => {
return strategy.method === method;
}).pop() || false;
};
UserSchema.methods.getNomDeBid = function () {
return this.nomDeBid || `${this.firstName} ${this.lastName.charAt(0)}`;
};
UserSchema.methods.isEventManager = function () {
return this.isOrganizationEmployee || false;
};
UserSchema.methods.isRegistrationVerified = function () {
return this.isVerified || false;
};
UserSchema.methods.setPassword = function (password, callback = () => {}) {
const hasLocalStrategy = !!this.credentials.length &&
!!this.credentials.filter(strategy => strategy.method === 'local').length;
const salt = crypto.randomBytes(16).toString('hex');
const accessToken = crypto.pbkdf2Sync(password, salt, 10000, 512, 'sha512').toString('hex');
const strategy = {
accessToken,
method: 'local',
secret: salt,
};
if (hasLocalStrategy) {
this.model('User').findOneAndUpdate(
{ _id: this._id, 'credentials.method': 'local' },
{ $set: { 'credentials.$': strategy } },
{ upsert: true },
callback,
);
}
if (!hasLocalStrategy) {
this.credentials.push(strategy);
this.save(callback);
}
};
UserSchema.methods.toAuthJSON = function () {
const hasNomDeBid = !!this.nomDeBid;
const nomDeBid = this.getNomDeBid();
return {
email: this.email,
token: this.generateJWT(),
user: {
nomDeBid: nomDeBid,
email: this.email,
firstName: this.firstName,
lastName: this.lastName,
avatar: this.avatar,
isAllowedToBid: this.isAllowedToBid,
isOrganizationEmployee: this.isOrganizationEmployee,
generatedNomDeBid: !hasNomDeBid,
},
};
};
UserSchema.methods.validatePassword = function (password) {
const strategy = this.getAuthStrategy('local');
if (strategy) {
let hash = crypto.pbkdf2Sync(password, strategy.secret, 10000, 512, 'sha512').toString('hex');
return strategy.accessToken === hash;
}
return false;
};
/**
* STATICS
*/
UserSchema.statics.findOrCreate = function (filter = {}, profile = {}, callback = () => {}) {
const self = this;
this.findOne(filter, function(err,result) {
if (err) {
callback(err, null);
}
if (!result) {
self.create(profile, (err, result) => callback(err, result));
}else{
callback(err, result);
}
});
};
UserSchema.statics.findOneAndUpdateOrCreate = function (
filter = {},
strategy = {},
profile = {},
callback = () => {},
) {
const self = this;
this.findOne(filter, function(err, result) {
if (err) {
callback(err, null);
}
if (!result) {
self.create(
{
strategy: [ strategy ],
...profile
},
(err, result) => callback(err, result),
);
} else {
const hasStrategy = !!result.credentials.length &&
!!result.credentials.filter(auth => auth.method === strategy.method).length;
if (hasStrategy) {
self.model('User').findOneAndUpdate(
{ _id: result._id, 'credentials.method': strategy.method },
{ $set: { 'credentials.$': strategy } },
{ upsert: true },
callback,
);
} else {
result.credentials.push(strategy);
result.save(callback);
}
}
});
};
/**
* PATH OPERATIONS
*/
UserSchema.path('avatar').get(v => `${config.assetStoreUrl}${v}`);
/**
* Export
*/
const User = mongoose.model('User', UserSchema);
module.exports = User;

28
package.json Normal file
View File

@@ -0,0 +1,28 @@
{
"name": "api",
"version": "0.0.1",
"description": "The wEvent API and DB server",
"main": "index.js",
"author": "mike@fitz.guru",
"license": "MIT",
"notes": "b'VJ!4a{L(8T(CvG8BKGj).]",
"dependencies": {
"api-query-params": "^4.13.0",
"crypto": "^1.0.1",
"jsonwebtoken": "^8.5.1",
"mongoose": "^5.6.0",
"mongoose-find-or-create": "^1.3.1",
"mongoose-string-query": "^0.2.7",
"mongoose-timestamp": "^0.6.0",
"passport": "^0.4.0",
"passport-facebook": "^3.0.0",
"passport-google-oauth": "^2.0.0",
"passport-http-bearer": "^1.0.1",
"passport-jwt": "^4.0.0",
"passport-local": "^1.0.0",
"restify": "^8.3.3",
"restify-errors": "^8.0.0",
"restify-jwt-community": "^1.0.13",
"restify-plugins": "^1.6.0"
}
}

92
routes/auth.js Normal file
View File

@@ -0,0 +1,92 @@
const errors = require('restify-errors');
const config = require('../config');
const handlePassportResponse = (req, res, next) => (err, passportUser, info) => {
if (err) {
return next(err);
}
const isVerifiedUser = passportUser.isRegistrationVerified();
if (passportUser && isVerifiedUser) {
const user = passportUser;
user.token = passportUser.generateJWT();
return res.send({ ...user.toAuthJSON() });
} else if (passportUser && !isVerifiedUser){
return res.send({
registrationSuccess: true,
nextSteps: 'Check your email for our confirmation email, you will not be able to login without confirming.'
});
}
return res.send(400, info);
};
module.exports = function (server, auth) {
const { passport } = auth;
/* Local Auth */
server.post('/auth', (req, res, next) => {
const { body: { username = null, password = null } = {} } = req;
if (!username || !password) {
let errors = {};
if (!username) {
errors.username = 'is required';
}
if (!password) {
errors.password = 'is required';
}
return res.send(422, { errors });
}
const callback = handlePassportResponse(req, res, next);
return passport.authenticate('local', { session: false }, callback)(req, res, next);
});
/**
* SERVICES
*/
/* Google */
server.get(
'/auth/google',
passport.authenticate('google', { scope: 'profile email', session: false }),
);
server.get(
'/auth/google/callback',
(req, res, next) => {
const callback = handlePassportResponse(req, res, next);
return passport.authenticate(
'google',
{ failureRedirect: '/login' },
callback,
)(req, res, next);
},
);
/* Facebook */
server.get(
'/auth/facebook',
passport.authenticate('facebook', {
scope: ['email', 'public_profile'],
session: false,
}),
);
server.get(
'/auth/facebook/callback',
(req, res, next) => {
const callback = handlePassportResponse(req, res, next);
return passport.authenticate(
'facebook',
{ failureRedirect: '/login' },
callback,
)(req, res, next);
}
);
};

110
routes/bids.js Normal file
View File

@@ -0,0 +1,110 @@
const errors = require('restify-errors');
const Bid = require('../models/bid');
module.exports = function(server) {
server.post('/bids', (req, res, next) => {
if (!req.is('application/json')) {
return next(
new errors.InvalidContentError("Expects 'application/json'"),
);
}
let data = req.body || {};
let bid = new Bid(data);
bid.save(function(err) {
if (err) {
console.error(err);
return next(new errors.InternalError(err.message));
next();
}
res.send(201);
next();
});
});
server.get('/bids', (req, res, next) => {
Bid.apiQuery(req.params, function(err, docs) {
if (err) {
console.error(err);
return next(
new errors.InvalidContentError(err.errors.name.message),
);
}
res.send(docs);
next();
});
});
server.get('/bids/:bid_id', (req, res, next) => {
Bid.findOne({ _id: req.params.bid_id }, function(err, doc) {
if (err) {
console.error(err);
return next(
new errors.InvalidContentError(err.errors.name.message),
);
}
res.send(doc);
next();
});
});
server.put('/bids/:bid_id', (req, res, next) => {
if (!req.is('application/json')) {
return next(
new errors.InvalidContentError("Expects 'application/json'"),
);
}
let data = req.body || {};
if (!data._id) {
data = Object.assign({}, data, { _id: req.params.bid_id });
}
Bid.findOne({ _id: req.params.bid_id }, function(err, doc) {
if (err) {
console.error(err);
return next(
new errors.InvalidContentError(err.errors.name.message),
);
} else if (!doc) {
return next(
new errors.ResourceNotFoundError(
'The resource you requested could not be found.',
),
);
}
Bid.update({ _id: data._id }, data, function(err) {
if (err) {
console.error(err);
return next(
new errors.InvalidContentError(err.errors.name.message),
);
}
res.send(200, data);
next();
});
});
});
server.del('/bids/:bid_id', (req, res, next) => {
Bid.deleteOne({ _id: req.params.bid_id }, function(err) {
if (err) {
console.error(err);
return next(
new errors.InvalidContentError(err.errors.name.message),
);
}
res.send(204);
next();
});
});
};

110
routes/events.js Normal file
View File

@@ -0,0 +1,110 @@
const errors = require('restify-errors');
const Event = require('../models/event');
module.exports = function(server) {
server.post('/events', (req, res, next) => {
if (!req.is('application/json')) {
return next(
new errors.InvalidContentError("Expects 'application/json'"),
);
}
let data = req.body || {};
let event = new Event(data);
event.save(function(err) {
if (err) {
console.error(err);
return next(new errors.InternalError(err.message));
next();
}
res.send(201);
next();
});
});
server.get('/events', (req, res, next) => {
Event.apiQuery(req.params, function(err, docs) {
if (err) {
console.error(err);
return next(
new errors.InvalidContentError(err.errors.name.message),
);
}
res.send(docs);
next();
});
});
server.get('/events/:event_id', (req, res, next) => {
Event.findOne({ _id: req.params.event_id }, function(err, doc) {
if (err) {
console.error(err);
return next(
new errors.InvalidContentError(err.errors.name.message),
);
}
res.send(doc);
next();
});
});
server.put('/events/:event_id', (req, res, next) => {
if (!req.is('application/json')) {
return next(
new errors.InvalidContentError("Expects 'application/json'"),
);
}
let data = req.body || {};
if (!data._id) {
data = Object.assign({}, data, { _id: req.params.event_id });
}
Event.findOne({ _id: req.params.event_id }, function(err, doc) {
if (err) {
console.error(err);
return next(
new errors.InvalidContentError(err.errors.name.message),
);
} else if (!doc) {
return next(
new errors.ResourceNotFoundError(
'The resource you requested could not be found.',
),
);
}
Event.update({ _id: data._id }, data, function(err) {
if (err) {
console.error(err);
return next(
new errors.InvalidContentError(err.errors.name.message),
);
}
res.send(200, data);
next();
});
});
});
server.del('/events/:event_id', (req, res, next) => {
Event.deleteOne({ _id: req.params.event_id }, function(err) {
if (err) {
console.error(err);
return next(
new errors.InvalidContentError(err.errors.name.message),
);
}
res.send(204);
next();
});
});
};

9
routes/index.js Normal file
View File

@@ -0,0 +1,9 @@
module.exports = function(server, auth) {
require('./auth.js')(server, auth);
require('./bids.js')(server, auth);
require('./events.js')(server, auth);
require('./installs.js')(server, auth);
require('./items.js')(server, auth);
require('./sales.js')(server, auth);
require('./users.js')(server, auth);
};

110
routes/installs.js Normal file
View File

@@ -0,0 +1,110 @@
const errors = require('restify-errors');
const Install = require('../models/install');
module.exports = function(server) {
server.post('/installs', (req, res, next) => {
if (!req.is('application/json')) {
return next(
new errors.InvalidContentError("Expects 'application/json'"),
);
}
let data = req.body || {};
let install = new Install(data);
install.save(function(err) {
if (err) {
console.error(err);
return next(new errors.InternalError(err.message));
next();
}
res.send(201);
next();
});
});
server.get('/installs', (req, res, next) => {
Install.apiQuery(req.params, function(err, docs) {
if (err) {
console.error(err);
return next(
new errors.InvalidContentError(err.errors.name.message),
);
}
res.send(docs);
next();
});
});
server.get('/installs/:install_id', (req, res, next) => {
Install.findOne({ _id: req.params.install_id }, function(err, doc) {
if (err) {
console.error(err);
return next(
new errors.InvalidContentError(err.errors.name.message),
);
}
res.send(doc);
next();
});
});
server.put('/installs/:install_id', (req, res, next) => {
if (!req.is('application/json')) {
return next(
new errors.InvalidContentError("Expects 'application/json'"),
);
}
let data = req.body || {};
if (!data._id) {
data = Object.assign({}, data, { _id: req.params.install_id });
}
Install.findOne({ _id: req.params.install_id }, function(err, doc) {
if (err) {
console.error(err);
return next(
new errors.InvalidContentError(err.errors.name.message),
);
} else if (!doc) {
return next(
new errors.ResourceNotFoundError(
'The resource you requested could not be found.',
),
);
}
Install.update({ _id: data._id }, data, function(err) {
if (err) {
console.error(err);
return next(
new errors.InvalidContentError(err.errors.name.message),
);
}
res.send(200, data);
next();
});
});
});
server.del('/installs/:install_id', (req, res, next) => {
Install.deleteOne({ _id: req.params.install_id }, function(err) {
if (err) {
console.error(err);
return next(
new errors.InvalidContentError(err.errors.name.message),
);
}
res.send(204);
next();
});
});
};

110
routes/items.js Normal file
View File

@@ -0,0 +1,110 @@
const errors = require('restify-errors');
const Item = require('../models/item');
module.exports = function(server) {
server.post('/items', (req, res, next) => {
if (!req.is('application/json')) {
return next(
new errors.InvalidContentError("Expects 'application/json'"),
);
}
let data = req.body || {};
let item = new Item(data);
item.save(function(err) {
if (err) {
console.error(err);
return next(new errors.InternalError(err.message));
next();
}
res.send(201);
next();
});
});
server.get('/items', (req, res, next) => {
Item.apiQuery(req.params, function(err, docs) {
if (err) {
console.error(err);
return next(
new errors.InvalidContentError(err.errors.name.message),
);
}
res.send(docs);
next();
});
});
server.get('/items/:item_id', (req, res, next) => {
Item.findOne({ _id: req.params.item_id }, function(err, doc) {
if (err) {
console.error(err);
return next(
new errors.InvalidContentError(err.errors.name.message),
);
}
res.send(doc);
next();
});
});
server.put('/items/:item_id', (req, res, next) => {
if (!req.is('application/json')) {
return next(
new errors.InvalidContentError("Expects 'application/json'"),
);
}
let data = req.body || {};
if (!data._id) {
data = Object.assign({}, data, { _id: req.params.item_id });
}
Item.findOne({ _id: req.params.item_id }, function(err, doc) {
if (err) {
console.error(err);
return next(
new errors.InvalidContentError(err.errors.name.message),
);
} else if (!doc) {
return next(
new errors.ResourceNotFoundError(
'The resource you requested could not be found.',
),
);
}
Item.update({ _id: data._id }, data, function(err) {
if (err) {
console.error(err);
return next(
new errors.InvalidContentError(err.errors.name.message),
);
}
res.send(200, data);
next();
});
});
});
server.del('/items/:item_id', (req, res, next) => {
Item.deleteOne({ _id: req.params.item_id }, function(err) {
if (err) {
console.error(err);
return next(
new errors.InvalidContentError(err.errors.name.message),
);
}
res.send(204);
next();
});
});
};

116
routes/sales.js Normal file
View File

@@ -0,0 +1,116 @@
/**
* Module Dependencies
*/
const errors = require('restify-errors');
/**
* Model Schema
*/
const Sale = require('../models/sale');
module.exports = function(server) {
server.post('/sales', (req, res, next) => {
if (!req.is('application/json')) {
return next(
new errors.InvalidContentError("Expects 'application/json'"),
);
}
let data = req.body || {};
let sale = new Sale(data);
sale.save(function(err) {
if (err) {
console.error(err);
return next(new errors.InternalError(err.message));
next();
}
res.send(201);
next();
});
});
server.get('/sales', (req, res, next) => {
Sale.apiQuery(req.params, function(err, docs) {
if (err) {
console.error(err);
return next(
new errors.InvalidContentError(err.errors.name.message),
);
}
res.send(docs);
next();
});
});
server.get('/sales/:sale_id', (req, res, next) => {
Sale.findOne({ _id: req.params.sale_id }, function(err, doc) {
if (err) {
console.error(err);
return next(
new errors.InvalidContentError(err.errors.name.message),
);
}
res.send(doc);
next();
});
});
server.put('/sales/:sale_id', (req, res, next) => {
if (!req.is('application/json')) {
return next(
new errors.InvalidContentError("Expects 'application/json'"),
);
}
let data = req.body || {};
if (!data._id) {
data = Object.assign({}, data, { _id: req.params.sale_id });
}
Sale.findOne({ _id: req.params.sale_id }, function(err, doc) {
if (err) {
console.error(err);
return next(
new errors.InvalidContentError(err.errors.name.message),
);
} else if (!doc) {
return next(
new errors.ResourceNotFoundError(
'The resource you requested could not be found.',
),
);
}
Sale.update({ _id: data._id }, data, function(err) {
if (err) {
console.error(err);
return next(
new errors.InvalidContentError(err.errors.name.message),
);
}
res.send(200, data);
next();
});
});
});
server.del('/sales/:sale_id', (req, res, next) => {
Sale.deleteOne({ _id: req.params.sale_id }, function(err) {
if (err) {
console.error(err);
return next(
new errors.InvalidContentError(err.errors.name.message),
);
}
res.send(204);
next();
});
});
};

42
routes/signup.js Normal file
View File

@@ -0,0 +1,42 @@
const errors = require('restify-errors');
const User = require('../models/user');
module.exports = function (server, auth) {
const { passport } = auth;
server.post('/signup', (req, res, next) => {
const { body: { user = null } = {} } = req;
let errors = {};
let errorCount = 0;
if (!user) {
errors.user = 'is required - can\'t make something from nothing...'';
errorCount++;
}
if (errorCount) {
return res.send(422, { errors });
}
User.register(user, (err, user, info) => {
if (err) {
next(err);
}
if (info) {
res.send(200, {
registrationSuccess: false,
nextSteps: 'Please fix the problems indicated and try again.'
...info
});
return next();
}
res.send(200, {
registrationSuccess: true,
nextSteps: 'Check your email for our confirmation email, you will not be able to login without confirming.'
})
});
});
};

169
routes/users.js Normal file
View File

@@ -0,0 +1,169 @@
const aqp = require('api-query-params');
const errors = require('restify-errors');
const User = require('../models/user');
const { PUBLIC, PROTECTED } = require('../strategies/selects/user');
module.exports = function (server, auth) {
server.post('/users', auth.manager, (req, res, next) => {
let { password = null, ...data } = req.body || {};
let user = new User(data);
user.save(function(err) {
if (err) {
console.error(err);
return next(new errors.InternalError(err.message));
next();
}
if (password) {
user.setPassword(password);
}
res.send(201);
next();
});
});
server.get('/users', auth.manager, (req, res, next) => {
const { filter } = aqp(req.query);
const select = req.user.isManager ? PROTECTED : PUBLIC;
User.find(filter, select, function (err, docs) {
if (err) {
console.error(err);
return next(
new errors.InvalidContentError(err),
);
}
res.send(docs);
next();
});
});
server.get('/users/:user_id', auth.managerOrSelf, (req, res, next) => {
const select = req.user.isManager ? PROTECTED : PUBLIC;
User.findOne({ _id: req.params.user_id }, select, function (err, doc) {
if (err) {
console.error(err);
return next(
new errors.InvalidContentError(err),
);
}
res.send(doc);
next();
});
});
server.put('/users/:user_id', auth.managerOrSelf, (req, res, next) => {
let data = req.body || {};
if (!data._id) {
data = Object.assign({}, data, { _id: req.params.user_id });
}
User.findOne({ _id: req.params.user_id }, function (err, doc) {
if (err) {
console.error(err);
return next(
new errors.InvalidContentError(err),
);
} else if (!doc) {
return next(
new errors.ResourceNotFoundError(
'The resource you requested could not be found.',
),
);
}
User.update({ _id: data._id }, data, function (err) {
if (err) {
console.error(err);
return next(
new errors.InvalidContentError(err),
);
}
res.send(200, data);
next();
});
});
});
server.put('/users/password/:user_id/:reset_token?', function (req, res, next) {
let {
currentPassword = null,
newPassword = null,
...data
} = req.body || {};
if (!newPassword) {
return next(
new errors.InvalidContentError('Password cannot be empty.'),
);
}
let filter = { _id: req.params.user_id };
let resetToken = req.params.reset_token || null;
if (resetToken) {
fiter.resetToken = resetToken;
}
User.findOne(filter, function (err, user) {
if (err) {
console.error(err);
return next(
new errors.InvalidContentError(err),
);
}
if (!user) {
return next(
new errors.ResourceNotFoundError(
'The user you requested could not be found.',
),
);
}
if (!resetToken &&
!!user.getAuthStrategy('local') &&
!user.validatePassword(currentPassword)
) {
return next(
new errors.InvalidContentError(
'The current password was incorrect.',
),
);
}
user.setPassword(newPassword, function (err) {
if (err) {
console.error(err);
return next(
new errors.InvalidContentError(err),
);
}
res.send(200, data);
next();
});
});
});
server.del('/users/:user_id', auth.manager, (req, res, next) => {
User.deleteOne({ _id: req.params.user_id }, function (err) {
if (err) {
console.error(err);
return next(
new errors.InvalidContentError(err),
);
}
res.send(204);
next();
});
});
};

5
strategies/auth/apple.js Normal file
View File

@@ -0,0 +1,5 @@
const passport = require('passport');
module.exports = function(passport) {
return passport;
};

View File

@@ -0,0 +1,46 @@
const passport = require('passport');
const FacebookStrategy = require('passport-facebook').Strategy;
const config = require('../../config');
const User = require('../../models/user');
module.exports = function(passport) {
passport.use(new FacebookStrategy(
{
clientID: config.services.facebook.appId,
clientSecret: config.services.facebook.appSecret,
callbackURL: 'http://localhost:3001/auth/facebook/callback',
profileFields: ['id', 'email', 'first_name', 'last_name', 'picture'],
},
(accessToken, refreshToken, profile, done) => {
const {
email,
first_name: firstName,
id: userId,
last_name: lastName,
picture: { data: { url = null } = {} } = {},
} = profile._json;
const avatar = url;
User.findOneAndUpdateOrCreate(
{
email,
},
{
accessToken,
method: profile.provider,
userId,
},
{
avatar,
email,
firstName,
lastName,
},
(err, user) => {
return done(err, user, { accessToken, refreshToken });
}
);
}
));
};

40
strategies/auth/google.js Normal file
View File

@@ -0,0 +1,40 @@
const passport = require('passport');
const GoogleStrategy = require('passport-google-oauth').OAuth2Strategy;
const config = require('../../config');
const User = require('../../models/user');
module.exports = function(passport) {
passport.use(new GoogleStrategy({
clientID: config.services.google.appId,
clientSecret: config.services.google.appSecret,
callbackURL: "http://www.example.com/auth/google/callback",
},
(accessToken, refreshToken, profile, callback) => {
const googleUser = profile.getBasicProfile();
User.findOrCreate(
{
email: googleUser.getEmail(),
'credentials.method': 'google',
'credentials.userId': googleUser.getId(),
},
{
avatar: googleUser.getImageUrl(),
email: googleUser.getEmail(),
firstName: googleUser.getGivenName(),
lastName: googleUser.getFamilyName(),
credentials: [{
accessToken,
userId: googleUser.getId(),
method: 'facebook',
profile,
}],
},
(err, user) => {
return done(err, user, { accessToken, refreshToken });
}
);
}
));
};

82
strategies/auth/index.js Normal file
View File

@@ -0,0 +1,82 @@
const createRequestUserObject = (req, user) => ({
isGuest: !(user && user.id),
isManager: user && user.isEventManager(),
isSelf: user && user.id === req.params.user_id,
record: user || null,
});
const authenticateBasic = (passport) => (req, res, next) => (
passport.authenticate('jwt', { session: false }, (err, user, info) => {
if (err) {
next(err);
}
req.user = createRequestUserObject(req, user);
next();
})(req, res, next)
);
const authenticateEventManager = (passport) => (req, res, next) => (
passport.authenticate('jwt', { session: false }, (err, user, info) => {
if (err) {
next(err);
}
const record = createRequestUserObject(req, user);
if (!user || !record.isManager) {
return res.send(401);
}
req.user = record;
next();
})(req, res, next)
);
const authenticateEventManagerOrSelf = (passport) => (req, res, next) => (
passport.authenticate('jwt', { session: false }, (err, user, info) => {
if (err) {
next(err);
}
const record = createRequestUserObject(req, user);
if (user && (!record.isManager && !record.isSelf)) {
return res.send(401);
}
req.user = record;
next();
})(req, res, next)
);
const authenticateSecure = (passport) => (req, res, next) => (
passport.authenticate('jwt', { session: false }, (err, user, info) => {
if (err) {
next(err);
}
if (!user) {
return res.send(401);
}
req.user = createRequestUserObject(req, user);
next();
})(req, res, next)
);
module.exports = function (passport) {
require('./apple.js')(passport);
require('./facebook.js')(passport);
require('./google.js')(passport);
require('./jwt.js')(passport);
require('./local.js')(passport);
return {
basic: authenticateBasic(passport),
manager: authenticateEventManager(passport),
managerOrSelf: authenticateEventManagerOrSelf(passport),
passport,
secure: authenticateSecure(passport),
};
};

30
strategies/auth/jwt.js Normal file
View File

@@ -0,0 +1,30 @@
const passport = require('passport');
const JwtStrategy = require('passport-jwt').Strategy;
const ExtractJwt = require('passport-jwt').ExtractJwt;
const config = require('../../config');
const User = require('../../models/user');
module.exports = function(passport) {
passport.use(new JwtStrategy(
{
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: config.security.jwt.secret,
issuer: config.security.jwt.issuer,
audience: config.security.jwt.audience,
},
(jwt_payload, done) => {
User.findOne({ _id: jwt_payload.sub }, (err, user) => {
if (err) {
return done(err, false);
}
if (user) {
return done(null, user);
}
return done(null, false);
});
}
));
}

24
strategies/auth/local.js Normal file
View File

@@ -0,0 +1,24 @@
const passport = require('passport');
const LocalStrategy = require('passport-local').Strategy;
const User = require('../../models/user');
module.exports = function(passport) {
passport.use(new LocalStrategy(
{
usernameField: 'username',
passwordField: 'password',
},
(username, password, done) => {
User.findOne({ email: username }, (err, user) => {
if (err) { return done(err); }
if (!user || !user.validatePassword(password)) {
return done(null, false, { message: 'Incorrect username or password.' });
}
return done(null, user);
});
}
));
};

View File

@@ -0,0 +1,10 @@
module.exports = {
PUBLIC: {
credentials: 0,
isVerified: 0,
organizationIdentifier: 0,
paymentToken: 0,
},
PROTECTED: {},
};

1347
yarn.lock Normal file

File diff suppressed because it is too large Load Diff