From bae570232cd30b1c5c8a4b5367d806c17b5dd534 Mon Sep 17 00:00:00 2001 From: Sagi Dayan Date: Sat, 28 Apr 2018 17:44:33 +0300 Subject: [PATCH] All basic Auth and upload/download functions. No editing nor deleting --- .gitignore | 4 +- Server/API/API.js | 23 +++- Server/API/Routers/AccountRouter.js | 91 ++++++++++++++ Server/API/Routers/FrameRouter.js | 182 ++++++++++++++++++++++++++++ Server/Config/Config.js | 15 +++ Server/Schemas/Account.js | 50 ++++++++ Server/Schemas/AppVersion.js | 2 +- Server/Schemas/Frame.js | 13 ++ Server/Schemas/Photo.js | 14 +++ Server/Schemas/User.js | 12 ++ Server/Utils/AuthUtil.js | 34 ++++++ Server/Utils/DBUtil.js | 15 ++- Server/Utils/TokenGenerator.js | 5 + 13 files changed, 446 insertions(+), 14 deletions(-) create mode 100644 Server/API/Routers/AccountRouter.js create mode 100644 Server/API/Routers/FrameRouter.js create mode 100644 Server/Schemas/Account.js create mode 100644 Server/Schemas/Frame.js create mode 100644 Server/Schemas/Photo.js create mode 100644 Server/Schemas/User.js create mode 100644 Server/Utils/AuthUtil.js create mode 100644 Server/Utils/TokenGenerator.js diff --git a/.gitignore b/.gitignore index f704d3c..4a659d9 100644 --- a/.gitignore +++ b/.gitignore @@ -4,7 +4,7 @@ ### Linux ### *~ -# temporary files which can be created if a process still has a handle open of a +# temporary files which can be created if a process still has a handle open of a deleted file .fuse_hidden* @@ -67,7 +67,7 @@ coverage # nyc test coverage .nyc_output -# Grunt intermediate storage +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) .grunt diff --git a/Server/API/API.js b/Server/API/API.js index b7d4358..22d1bd0 100644 --- a/Server/API/API.js +++ b/Server/API/API.js @@ -1,8 +1,25 @@ -var express = require("express"); -var router = express.Router(); +const express = require("express"); +const router = express.Router(); +const bodyParser = require('body-parser'); //Routers const UpdateRouter = require('./Routers/UpdateRouter'); -router.use('/update', UpdateRouter); +const AccountRouter = require('./Routers/AccountRouter'); +const FrameRouter = require('./Routers/FrameRouter'); + +// parse application/x-www-form-urlencoded +router.use(bodyParser.urlencoded({ extended: true })) + +// parse application/json +router.use(bodyParser.json()) +// bodyParser.parse('image/*') = function(req, options, next) { +// req.image = req. +// // for your needs it will probably be this: +// next(); +// } + +router.use('/update', UpdateRouter); +router.use('/account', AccountRouter) +router.use('/frame', FrameRouter) module.exports = router; diff --git a/Server/API/Routers/AccountRouter.js b/Server/API/Routers/AccountRouter.js new file mode 100644 index 0000000..369e701 --- /dev/null +++ b/Server/API/Routers/AccountRouter.js @@ -0,0 +1,91 @@ +const express = require("express"); +const DBUtils = require('../../Utils/DBUtil'); +const Config = require('../../Config/Config'); + + +const router = express.Router(); + +router.post('/create/', (req, res) => { + const body = req.body; + if (!body.username || !body.password) { + res.status(400).json({ + message: 'username and password are required' + }); + } else if (body.username.length < Config.validators.account.username_min_length) { + res.status(400).json({ + message: `username must be at least ${Config.validators.account.username_min_length} chars long` + }); + } else if (body.password.length < Config.validators.account.password_min_length) { + res.status(400).json({ + message: `password must be at least ${Config.validators.account.password_min_length} chars long` + }); + } else { + // Create a new Account - status 201 + // create a user a new user + const account = new DBUtils.Models.Account({ + username: body.username, + password: body.password, + }); + + account.save((err, doc) => { + if (err) { + res.status(400).json({ + message: "Failed to save account in DB, username taken" + }) + } else { + // create an empty user object + const user = new DBUtils.Models.User({ + account_id: account._id, + nickname: account.username + }); + user.save((err, doc) => { + if (err) { + //TODO delete the created account.... + res.status(400).json({ + message: "Failed to save account in DB, username taken" + }); //FIXME - Lies!!! + } else { + res.status(201).json({ + user: user, + token: account.auth_token + }); + } + }) + + } + }); + } + +}); + +router.post('/login/', (req, res) => { + const body = req.body; + DBUtils.Models.Account.findOne({ + username: body.username + }, (err, account) => { + if (err) throw err; + if (account) { + // test a matching password + account.comparePassword(body.password, account.password, (err, isMatch) => { + if (err) throw err; + if (!isMatch) { + res.status(401).json({ + message: 'Authentication Fail' + }); + return; + } + res.json({ + token: account.auth_token + }); + }); + } else { + res.status(401).json({ + message: 'Authentication Fail' + }); + } + + + }); +}); + +module.exports = router; diff --git a/Server/API/Routers/FrameRouter.js b/Server/API/Routers/FrameRouter.js new file mode 100644 index 0000000..76aa9c6 --- /dev/null +++ b/Server/API/Routers/FrameRouter.js @@ -0,0 +1,182 @@ +const express = require("express"); +const DBUtils = require('../../Utils/DBUtil') +const AuthUtil = require('../../Utils/AuthUtil') +const bodyParser = require('body-parser'); + +const router = express.Router(); + +router.use(bodyParser.raw({ + uploadDir: '/tmp/uploads', + keepExtensions: true, + limit: '5mb', + type: 'image/*' + })) + +router.get('/:frameId', (req, res) => { // by Frame Id + const token = req.get('token'); + const frameId = req.params.frameId; + AuthUtil.getAccountByToken(token) + .then((account) => { + if(account.frames.indexOf(frameId) >= 0) { + DBUtils.Models.Frame.findOne({_id: frameId}, (err, doc) => { + if(err) { + res.status(400).json({ + message: err.message + }); + return; + } else if(!doc) { + res.status(400).json({ + message: 'Unable to find a Frame with id: ' + frameId + }); + return; + } + const frame = doc.toObject(); + /// lets get all images ids... + DBUtils.Models.Photo.find({frame_id: frameId}, (err, docs)=>{ + if(err) { + res.status(400).json({ + message: err.message + }); + return; + } + console.log(docs); + frame.photos = docs.map(p => p._id); + res.json(frame); + }) + }); + }else{ + res.status(403).json({ + message: 'Account has no access to frame with id of: ' + frameId + }); + } + }) + .catch((reason) => { + res.status(401).json({ + message: reason + }); + }) +}) + +router.post('/create', (req, res) => { + const token = req.get('token'); + const body = req.body; + if(!body.name) { + res.status(400).json({ + message: 'Must provide a name for your new frame' + }); + return; + } + AuthUtil.getAccountByToken(token) + .then((account)=>{ + // If account valid - create new frame + const frame = new DBUtils.Models.Frame({ + name: body.name, + admin: account._id, + members: [account._id] + }); + // save frame + frame.save((err, doc)=>{ + if(err) { + res.status(400).json({ + message: err.message + }); + return; + } + // frame created - now add its id to the account object + account.frames.push(doc._id); + account.save((err)=>{ + if( err ) throw err; + res.status(201).json(frame); + }) + }) + }) + .catch((reason)=>{ + res.status(401).json({ + message: reason + }); + }) + +}); + +router.post('/:frameId/upload/photo', (req, res) => { + const token = req.get('token'); + + AuthUtil.getAccountByToken(token) + .then(account => { + const frameId = req.params.frameId; + if(account.frames.indexOf(frameId) >= 0) { + // User can upload image to the frame + AuthUtil.getUserByAccountId(account._id) + .then((user)=>{ + // Upload Photo... + const photo = new DBUtils.Models.Photo({ + frame_id: frameId, + photo: req.body, + timestamp: Date.now(), + contentType: req.get('Content-Type'), + user:user.id}); + + // Save photo + photo.save((err) => { + if(err) { + res.status(400).json({ + message: err.message + }); + return; + } + res.status(201).json(photo) + }); + }) + .catch(reason => { + res.status(500).json({ + message: 'Unexpected error: ' + reason + }); + }) + } else { + res.status(403).json({ + message: 'Account has no access to frame with id of: ' + frameId + }); + } + }) + .catch(reason => { + res.status(401).json({ + message: reason + }); + }) +}); + +router.get('/:frameId/download/photo/:photoId', (req, res) => { + const token = req.get('token'); + const photoId = req.params.photoId; + const frameId = req.params.frameId; + + AuthUtil.getAccountByToken(token) + .then((account) => { + if(account.frames.indexOf(frameId) >= 0) { + DBUtils.Models.Photo.findOne({_id: photoId}, (err, doc) => { + if(err) { + res.status(400).json({ + message: err.message + }); + return; + } + if(doc) res.contentType(doc.contentType).send(doc.photo); + else res.status(400).json({ + message: 'Photo not found' + }); + }); + } else { + res.status(403).json({ + message: 'Account has no access to frame with id of: ' + photoId + }); + } + }) + .catch(reason => { + res.status(401).json({ + message: reason + }); + }) +}) + + +module.exports = router; diff --git a/Server/Config/Config.js b/Server/Config/Config.js index e4a3404..2b25438 100644 --- a/Server/Config/Config.js +++ b/Server/Config/Config.js @@ -17,4 +17,19 @@ module.exports = { mongoURL: `mongodb://${DBAuth.username}:${DBAuth.password}@ds159489.mlab.com:59489/framez_db`, + defaults: { + user: { + avatar: 'http://www.top-madagascar.com/assets/images/admin/user-admin.png' + } + }, + + validators: { + account: { + password_min_length: 8, + username_min_length: 3 + } + }, + + // Password hash + salt_work_factor: 10 }; diff --git a/Server/Schemas/Account.js b/Server/Schemas/Account.js new file mode 100644 index 0000000..46840da --- /dev/null +++ b/Server/Schemas/Account.js @@ -0,0 +1,50 @@ +const Config = require('../Config/Config') +const TokenGen = require('../Utils/TokenGenerator'); +const mongoose = require('mongoose'); +const bcrypt = require('bcrypt'); +const Schema = mongoose.Schema; +const SALT_WORK_FACTOR = Config.salt_work_factor; + +const Account = new Schema({ + username: { type: String, required: true, index: { unique: true } }, + password: { type: String, required: true }, + auth_token: {type: String, require: false, index: { unique: true }}, + frames: {type: [Schema.ObjectId], default: []} +}); + + +Account.pre('save', function(next) { + var user = this; + // only geerate auth_token if it was modified (or is new) + if(!user.auth_token){ + user.auth_token = TokenGen.generate() + } + // only hash the password if it has been modified (or is new) + if (user.isModified('password')){ + // generate a salt + bcrypt.genSalt(SALT_WORK_FACTOR, function(err, salt) { + if (err) return next(err); + + // hash the password using our new salt + bcrypt.hash(user.password, salt, function(err, hash) { + if (err) return next(err); + + // override the cleartext password with the hashed one + user.password = hash; + next(); + }); + }); + + } else { + next() + } + +}); + +Account.methods.comparePassword = (password, hash,cb) => { + bcrypt.compare(password, hash, (err, isMatch) => { + if (err) return cb(err); + cb(null, isMatch); + }); +}; +module.exports = mongoose.model('Account', Account); diff --git a/Server/Schemas/AppVersion.js b/Server/Schemas/AppVersion.js index 31e99b9..1e088f3 100644 --- a/Server/Schemas/AppVersion.js +++ b/Server/Schemas/AppVersion.js @@ -9,4 +9,4 @@ const AppVersion = new Schema({ uploaded: Date }); -module.exports = AppVersion +module.exports = mongoose.model('AppVersion', AppVersion); diff --git a/Server/Schemas/Frame.js b/Server/Schemas/Frame.js new file mode 100644 index 0000000..5a320c7 --- /dev/null +++ b/Server/Schemas/Frame.js @@ -0,0 +1,13 @@ +const mongoose = require('mongoose'); + +const Schema = mongoose.Schema; + +const Frame = new Schema({ + name: String, + icon: {type: String, default: 'https://image.flaticon.com/icons/png/128/847/847930.png'}, + admin: Schema.ObjectId, + members: {type: [Schema.ObjectId], default: []}, + tv_clients: {type: [Schema.ObjectId], default: []}, +}); + +module.exports = mongoose.model('Frame', Frame); diff --git a/Server/Schemas/Photo.js b/Server/Schemas/Photo.js new file mode 100644 index 0000000..d3d5858 --- /dev/null +++ b/Server/Schemas/Photo.js @@ -0,0 +1,14 @@ +const mongoose = require('mongoose'); + +const Schema = mongoose.Schema; + +const Photo = new Schema({ + frame_id: Schema.ObjectId, + contentType: String, + user: Schema.ObjectId, + photo: Buffer, + // thumbnail: Buffer, + timestamp: Number +}); + +module.exports = mongoose.model('Photo', Photo); diff --git a/Server/Schemas/User.js b/Server/Schemas/User.js new file mode 100644 index 0000000..3002307 --- /dev/null +++ b/Server/Schemas/User.js @@ -0,0 +1,12 @@ +const mongoose = require('mongoose'); +const Config = require('../Config/Config') +const Schema = mongoose.Schema; + +const User = new Schema({ + account_id: Schema.ObjectId, + nickname: { type: String, required: true, index: { unique: true } }, + avatar: { type: String, default: Config.defaults.user.avatar }, + auth_token: {type: String, default: null} +}); + +module.exports = mongoose.model('User', User); diff --git a/Server/Utils/AuthUtil.js b/Server/Utils/AuthUtil.js new file mode 100644 index 0000000..b90b76f --- /dev/null +++ b/Server/Utils/AuthUtil.js @@ -0,0 +1,34 @@ +const DBUtils = require('./DBUtil'); + +module.exports = { + getAccountByToken: (token) => { + return new Promise((resolve, reject) => { + if(!token){ + reject('Must provide token'); + return; + } + DBUtils.Models.Account.findOne({auth_token:token}, (err, account)=>{ + if(err) { + reject('Invalid token'); + return; + } + resolve(account); + }); + }) + }, + getUserByAccountId: (id) => { + return new Promise((resolve, reject) => { + if(!id) { + reject('No account id provided'); + return; + } + DBUtils.Models.User.findOne({account_id: id}, (err, doc) => { + if(err) { + reject('Unable to find user'); + return; + } + resolve(doc); + }); + }); + } +} diff --git a/Server/Utils/DBUtil.js b/Server/Utils/DBUtil.js index 43a73d4..b7c3a26 100644 --- a/Server/Utils/DBUtil.js +++ b/Server/Utils/DBUtil.js @@ -5,20 +5,19 @@ const mongoose = require('mongoose'); mongoose.connect(Config.mongoURL); -const Schemas = {} +const Models = {} var normalizedPath = require("path").join(__dirname, "..", "Schemas"); // Load All Schemas and put them in Shemas Obj -require("fs").readdirSync(normalizedPath).forEach(function(file) { - Schemas[file.split('.')[0]] = require(normalizedPath + '/' + file); -}); +require("fs") + .readdirSync(normalizedPath) + .forEach((file) => { + Models[file.split('.')[0]] = require(normalizedPath + '/' + file); + }); const util = { - getSomething: () => { - - }, - Schemas: Schemas + Models: Models } module.exports = util; diff --git a/Server/Utils/TokenGenerator.js b/Server/Utils/TokenGenerator.js new file mode 100644 index 0000000..9e2e65a --- /dev/null +++ b/Server/Utils/TokenGenerator.js @@ -0,0 +1,5 @@ +const uuidv4 = require('uuid/v4'); + +module.exports = { + generate: ()=>uuidv4() +}