diff --git a/.gitignore b/.gitignore index 5148e52..0f72e49 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ npm-debug.log* pids *.pid *.seed +*.pid.lock # Directory for instrumented libs generated by jscoverage/JSCover lib-cov @@ -29,9 +30,28 @@ build/Release # Dependency directories node_modules jspm_packages +typings +typings.json # Optional npm cache directory .npm +# Optional eslint cache +.eslintcache + # Optional REPL history .node_repl_history + +# Output of 'npm pack' +*.tgz +*.zip + +# Temp data +.tmp + +# VS Code +.vscode +launch.json + +#secrets +secret.js \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..8e64b70 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,21 @@ +{ + // Use IntelliSense to learn about possible Node.js debug attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "request": "launch", + "name": "Launch Program", + "program": "${workspaceRoot}/server.js", + "cwd": "${workspaceRoot}" + }, + { + "type": "node", + "request": "attach", + "name": "Attach to Process", + "port": 5858 + } + ] +} \ No newline at end of file diff --git a/configs/config.js b/configs/config.js new file mode 100644 index 0000000..faca8b7 --- /dev/null +++ b/configs/config.js @@ -0,0 +1,42 @@ +'use strict' +// this is just a test + +module.exports = { + goodOptions: { + ops: { + interval: 1000 + }, + reporters: { + myConsoleReporter: [{ + module: 'good-squeeze', + name: 'Squeeze', + args: [{ log: '*', response: '*' }] + }, { + module: 'good-console' + }, 'stdout'] + } + }, + swaggerOptions: { + info: { + title: 'MointainServer API Documentation', + description: `Api Documentation for the Mountain Server` + }, + expanded: 'full' + }, + mongo: { + url: '127.0.0.1:27017', + database: 'pss-api' + }, + credentials: { + 'facebook': { + 'AppID': 'facebookAppId', + 'AppSecret': 'facebookAppSecret', // + 'verificationURI': 'https://graph.facebook.com/me?access_token' + } + }, + JWT: { + secret: 'neverShareYourSecret' + }, + SALT_WORK_FACTOR: 10, + apiURL: 'http://localhost:8001' +} diff --git a/handlers/changeRoleHandler.js b/handlers/changeRoleHandler.js new file mode 100644 index 0000000..94d2e36 --- /dev/null +++ b/handlers/changeRoleHandler.js @@ -0,0 +1,14 @@ +const Boom = require('boom') +const User = require('../models/user.js').User + +module.exports = function (request, reply) { + const newRole = request.payload.role + User.findOneAndUpdate({_id: request.payload.userId}, { role: newRole }) + .then(result => { + delete result._doc.password + return reply(result) + }) + .catch(err => { + return reply(Boom.badData(err.message)) + }) +} diff --git a/handlers/getProfilesHandler.js b/handlers/getProfilesHandler.js new file mode 100644 index 0000000..7402fbc --- /dev/null +++ b/handlers/getProfilesHandler.js @@ -0,0 +1,40 @@ +'use strict' +const Boom = require('boom') +const User = require('../models/user.js').User + +module.exports = function (request, reply) { + const role = request.auth.credentials.scope + switch (role) { + case 'Administrator': { + User.find({}, { password: 0 }) + .then(result => { + return reply(result) + }) + .catch(err => { + return reply(Boom.system(err.message)) + }) + } + break + case 'MointainDispatcher': { + User.find({role: 'MountainRescuer'}, { password: 0 }).select('location') + .then(result => { + return reply(result) + }) + .catch(err => { + return reply(Boom.system(err.message)) + }) + } + break + case 'User': + case 'MountainRescuer': { + User.findOne({_id: request.auth.credentials.id}).select('location') + .then(result => { + return reply(result) + }) + .catch(err => { + return reply(Boom.system(err.message)) + }) + } + break + } +} diff --git a/handlers/getUserProfile.js b/handlers/getUserProfile.js new file mode 100644 index 0000000..df6d293 --- /dev/null +++ b/handlers/getUserProfile.js @@ -0,0 +1,13 @@ +'use strict' +const Boom = require('boom') +const User = require('../models/user.js').User + +module.exports = function (request, reply) { + User.findOne({_id: request.params.userId}, { password: 0 }) + .then(result => { + return reply(result) + }) + .catch(err => { + return reply(Boom.badData(err.message)) + }) +} diff --git a/handlers/loginFbHandler.js b/handlers/loginFbHandler.js new file mode 100644 index 0000000..cccc5da --- /dev/null +++ b/handlers/loginFbHandler.js @@ -0,0 +1,22 @@ +const Boom = require('boom') +const JWT = require('jsonwebtoken') +const config = require('../configs/config') +const Req = require('../lib/requests').Req +const req = new Req(config) +const User = require('../models/user.js').User + +module.exports = function (request, reply) { + const fbToken = request.payload.fb_token + + req.requestFbVerifyToken(fbToken).then((result) => { + User.findOne({ username: result.id }) + .then((np) => { + if (!np) return reply(Boom.unauthorized(null, 'Custom')) + else { + const token = JWT.sign({ id: np._id, email: np.email }, config.JWT.secret) + return reply({ token: token }) + } + }) + }) + .catch(err => { return reply(Boom.unauthorized(err.message)) }) +} diff --git a/handlers/loginHandler.js b/handlers/loginHandler.js new file mode 100644 index 0000000..a404e73 --- /dev/null +++ b/handlers/loginHandler.js @@ -0,0 +1,30 @@ +'use strict' +const Boom = require('boom') +const JWT = require('jsonwebtoken') +const bcrypt = require('bcrypt') +const config = require('../configs/config') +const User = require('../models/user.js').User + +module.exports = function (request, reply) { + const email = request.payload.email + const username = request.payload.username + const password = request.payload.password.toString() + + if (!email) var query = {username: username} + else query = {email: email} + + User.findOne(query) + .then((np) => { + bcrypt.compare(password, np.password, (err, result) => { + if (!err && result) { + const token = JWT.sign({ id: np._id, email: np.email }, config.JWT.secret) + return reply({token: token, fullname: np.full_name}) + } else if (!result) { + return reply(Boom.unauthorized('Wrong username/password')) + } else { + return err + } + }) + }) + .catch((err) => { return reply(Boom.unauthorized(err.message)) }) +} diff --git a/handlers/putLocationHandler.js b/handlers/putLocationHandler.js new file mode 100644 index 0000000..14a9666 --- /dev/null +++ b/handlers/putLocationHandler.js @@ -0,0 +1,28 @@ +'use strict' +const Boom = require('boom') +const Location = require('../models/location.js').Location + +module.exports = function (request, reply) { + const userId = request.auth.credentials.id + const location = request.payload + let updateValue = { + lat: location.lat, + lng: location.lng, + altitude: location.altitude + } + const options = { + upsert: true, + setDefaultsOnInsert: true + } + location.coordinateAccuracy ? (updateValue.coordinateAccuracy = location.coordinateAccuracy) : {} + Location.findOneAndUpdate({ + _user: userId + }, updateValue, options) + .then((result) => { + if (result) return reply({location: location}) + else return reply(Boom.badData('User has no initial location')) + }) + .catch(err => { + return reply(Boom.badData(err.message)) + }) +} diff --git a/handlers/registerFbHandler.js b/handlers/registerFbHandler.js new file mode 100644 index 0000000..5d6ad69 --- /dev/null +++ b/handlers/registerFbHandler.js @@ -0,0 +1,34 @@ +const Boom = require('boom') +const JWT = require('jsonwebtoken') +const config = require('../configs/config') +const Req = require('../lib/requests').Req +const addUser = require('../lib/utils').addUser + +let req = new Req(config) + +module.exports = function (request, reply) { + const fbToken = request.payload.fb_token + const role = request.payload.role + + req.requestFbSignUpToken(fbToken) + .then((userData) => { + let name = userData.name.split(' ') + let signUpData = { + fb_linked: userData.id, + email: userData.email, + first_name: name.shift(), + last_name: name.pop(), + password: '' + } + signUpData.location = request.payload.location + signUpData.role = role + + addUser(request, signUpData) + .then((np) => { + const userToken = { token: JWT.sign({ id: np._id, email: np.email }, config.JWT.secret) } + return reply(userToken) + }) +// .catch((err) => { return reply(Boom.unauthorized(err.message)) }) + }) + .catch((err) => { return reply(Boom.unauthorized(err.message)) }) +} diff --git a/handlers/registerHandler.js b/handlers/registerHandler.js new file mode 100644 index 0000000..6e7e64a --- /dev/null +++ b/handlers/registerHandler.js @@ -0,0 +1,21 @@ +'use strict' +const Boom = require('boom') +const JWT = require('jsonwebtoken') +const config = require('../configs/config') +const addUser = require('../lib/utils').addUser + +module.exports = function (request, reply) { + const signUpData = request.payload.user + signUpData.location = request.payload.location + + if (signUpData.password === signUpData.confirmed_password) { + addUser(request, signUpData) + .then((np) => { + const token = JWT.sign({ id: np._id, email: np.email }, config.JWT.secret) + return reply({token: token, fullname: np.full_name}) + }) + .catch((err) => { return reply(Boom.unauthorized(err.message)) }) + } else { + return reply(Boom.unauthorized('passwords do not match')) + } +} diff --git a/handlers/updateProfileHandler.js b/handlers/updateProfileHandler.js new file mode 100644 index 0000000..41aba7e --- /dev/null +++ b/handlers/updateProfileHandler.js @@ -0,0 +1,29 @@ +'use strict' +const Boom = require('boom') +const JWT = require('jsonwebtoken') +const bcrypt = require('bcrypt') +const config = require('../configs/config') +const User = require('../models/user.js').User + +module.exports = function (request, reply) { + let newProfileData = {} + const query = { email: request.auth.credentials.email } + for (let key of Object.keys(request.payload)) { + newProfileData[key] = request.payload[key] + } + if (newProfileData.email === query.email) { delete newProfileData.email } + if ((newProfileData.new_password) && (newProfileData.new_password === newProfileData.confirmed_password)) { + newProfileData.password = bcrypt.hashSync(newProfileData.new_password, config.SALT_WORK_FACTOR) + delete newProfileData.newPassword + delete newProfileData.confirmedPassword + } + + User.findOneAndUpdate(query, newProfileData) + .then((result) => { + const token = JWT.sign({id: result.id, email: result.email}, config.JWT.secret) + return reply({token: token}).state('session', token, config.cookie_options) + }) + .catch(err => { + return reply(Boom.badData(err.message)) + }) +} diff --git a/lib/database.js b/lib/database.js new file mode 100644 index 0000000..e14559a --- /dev/null +++ b/lib/database.js @@ -0,0 +1,15 @@ +'use strict' +const Mongoose = require('mongoose') +const db = Mongoose.connection +const config = require('../configs/config') + +Mongoose.Promise = global.Promise +Mongoose.connect(`mongodb://${config.mongo.url}/${config.mongo.database}`) + +db.on('error', console.error.bind(console, 'connection error')) +db.once('open', function callback () { + console.log('Connection with database succeeded.') +}) + +exports.Mongoose = Mongoose +exports.db = db diff --git a/lib/requests.js b/lib/requests.js new file mode 100644 index 0000000..8d34563 --- /dev/null +++ b/lib/requests.js @@ -0,0 +1,190 @@ +'use strict' +const request = require('request') + +class Req { + constructor (config) { + this.config = config + this.requestData = (object) => { + return new Promise((resolve, reject) => { + request(object, (e, r, body) => { + if (!e && r.statusCode === 200) { + let oBody = JSON.parse(body) + resolve(oBody) + } else { + if (!e) { + reject(new Error(`statusCode : ${r.statusCode} + body: ${r.body}`)) + } else { + reject(new Error(`Error: ${e}`)) + } + } + }) + }) + } + } + + requestToken (obj) { + return new Promise((resolve, reject) => { + let loginRequest = { + method: 'POST', + url: `${this.config.apiURL}/login`, + headers: { + 'content-type': 'application/json' + }, + body: JSON.stringify(obj) + } + this.requestData(loginRequest) + .then((token) => resolve(token)) + .catch((e) => reject(e)) + }) + } + + requestFbVerifyToken (fbToken) { + return new Promise((resolve, reject) => { + let fbRequest = { + method: 'GET', + url: `${this.config.credentials.facebook.verificationURI}=${fbToken}` + } + + this.requestData(fbRequest) + .then((result) => resolve(result)) + .catch((e) => reject(e)) + }) + } + + requestFbSignUpToken (fbToken) { + return new Promise((resolve, reject) => { + let fbSingUpRequest = { + method: 'GET', + url: `${this.config.credentials.facebook.verificationURI}=${fbToken}&fields=name,email` + } + this.requestData(fbSingUpRequest) + .then((result) => resolve(result)) + .catch((e) => reject(e)) + }) + } + + // === TEST === + + requestProfiles (token) { + return new Promise((resolve, reject) => { + let getProfilesRequest = { + method: 'GET', + url: `${this.config.apiURL}/profiles`, + headers: { + 'content-type': 'application/json', + 'Authentication-Token': `${token.token}` + } + } + + this.requestData(getProfilesRequest) + .then((result) => resolve(result)) + .catch((e) => reject(e)) + }) + } + + requestFbLogin (fbToken) { + return new Promise((resolve, reject) => { + let fbSignInRequest = { + method: 'POST', + url: `${this.config.apiURL}/login/facebook`, + headers: { + 'content-type': 'application/json' + }, + body: JSON.stringify({ + fb_token: fbToken + }) + } + this.requestData(fbSignInRequest) + .then((result) => resolve(result)) + .catch((e) => reject(e)) + }) + } + + requestFbRegister (userdata) { + return new Promise((resolve, reject) => { + let userFbSingUpRequest = { + method: 'POST', + url: `${this.config.apiURL}/register/facebook`, + headers: { + 'content-type': 'application/json' + }, + body: JSON.stringify(userdata) + } + + this.requestData(userFbSingUpRequest) + .then((result) => resolve(result)) + .catch((e) => reject(e)) + }) + } + + register (user) { + return new Promise((resolve, reject) => { + let loginRequest = { + method: 'POST', + url: `${this.config.apiURL}/register`, + headers: { + 'content-type': 'application/json' + }, + body: JSON.stringify(user) + } + this.requestData(loginRequest) + .then((result) => resolve(result)) + .catch((e) => reject(e)) + }) + } + + requestUpdateProfile (token, user) { + return new Promise((resolve, reject) => { + let loginRequest = { + method: 'POST', + url: `${this.config.apiURL}/profile/update`, + headers: { + 'content-type': 'application/json', + 'Authentication-Token': `${token.token}` + }, + body: JSON.stringify(user) + } + this.requestData(loginRequest) + .then((result) => resolve(result)) + .catch((e) => reject(e)) + }) + } + + requestChangeRole (adminToken, obj) { + return new Promise((resolve, reject) => { + let loginRequest = { + method: 'POST', + url: `${this.config.apiURL}/set-role`, + headers: { + 'content-type': 'application/json', + 'Authentication-Token': `${adminToken}` + }, + body: JSON.stringify(obj) + } + this.requestData(loginRequest) + .then((result) => resolve(result)) + .catch((e) => reject(e)) + }) + } + + + requestPutLocation (token, location) { + return new Promise((resolve, reject) => { + let locationRequest = { + method: 'PUT', + url: `${this.config.apiURL}/location`, + headers: { + 'content-type': 'application/json', + 'Authentication-Token': `${token.token}` + }, + body: JSON.stringify(location) + } + this.requestData(locationRequest) + .then((result) => resolve(result)) + .catch((e) => reject(e)) + }) + } +} + +module.exports = {Req: Req} diff --git a/lib/tests.js b/lib/tests.js new file mode 100644 index 0000000..8c83da4 --- /dev/null +++ b/lib/tests.js @@ -0,0 +1,93 @@ +let Req = require('./requests').Req +const config = require('../configs/config') +let req = new Req(config) + +// == login == + +let user = { +// email: 'admin@gmail.com', + username: 'vg11', + password: '!23#' +} + +// req.requestToken(user).then(r => console.log(r)).catch(e => console.log(e)) + +// == register == + +let newUser = { + user: { + first_name: 'GOGO', + last_name: 'GENEV', + phone_number: '+359099990', + username: 'vg11', + password: '!23#', + confirmed_password: '!23#', + email: 'gogogenev@gmail.com', + role: 'User' + } + // }, + // location: { + // lat: 12.4455, + // lng: 23.4553 + // } +} + +//req.register(newUser).then(r => console.log(r)).catch(e => console.log(e)) + +// == change location == + +let location = { + lat: 51.232, + lng: 22.212, + altitude: 145, + coordinateAccuracy: 3 +} + +// normal login +req.requestToken(user) + .then(t => req.requestPutLocation({token: t.token}, location) + .then((res) => { + console.log(res) + })) + .catch(e => console.log(e)) + +// fb login +// req.requestFbLogin(fbToken) +// .then(t => req.requestPutLocation({token: t.token}, location) +// .then((res) => { +// console.log(res) +// })) +// .catch(e => console.log(e)) + +const fbToken = 'EAAJwnGZAfhrABANOL0d6nkgpTKMb8Sk5vXPZCHX6piZC41wV7IEnhCgZBTbOxstn7nt5di03KAYem2IdtpHMoZBHIi4nt4BYrJrSZAdhW6PCuys93zAV7WfSl9J1bAridZCOViZAvOmQpb4cXMW3UiZCK7t32xoNf6xBZCHuQKfW2MDwZDZD' + +// req.requestFbLogin(fbToken).then(r => console.log(r)).catch(e => console.log(e)) + +let fbUser = { + fb_token: fbToken, + role: 'User', + location: location +} + +// req.requestFbRegister(fbUser).then(r => console.log(r)).catch(e => console.log(e)) + +// req.requestToken(user) +// .then(t => req.requestProfiles({token: t.token}) +// .then((res) => { +// console.log(res) +// })) +// .catch(e => console.log(e)) + +// req.requestToken(user) +// .then(t => req.requestUpdateProfile({token: t.token}, newUser.user) +// .then((res) => { +// console.log(res) +// })) +// .catch(e => console.log(e)) + +const adminToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjU4NTkzNWFjNDdiYzA0ZjY3NzEyNDAwZiIsImVtYWlsIjoiYWRtaW5AZ21haWwuY29tIiwiaWF0IjoxNDgyMjQxNDUyfQ.9pNH79kPHN-MB0KznqtnY2ANVfubUPlS60QiCaDm8kc' +const notAdminToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjU4NTkzMzg2MTc4ZGU0ZjQzYmM2NTA4YiIsImVtYWlsIjoidXNlckBnbWEuY29tIiwiaWF0IjoxNDgyMjQwOTAyfQ.k8hJxsLT_wGAVgW2ZIj-fN3h25jzNIhQoke-atvQcPQ' + +// req.requestChangeRole(adminToken, {userId: "585934da31286ff5722e2bb2", role: 'Administrator'}) +// .then(r => console.log(r)) +// .catch(e => console.log(e)) diff --git a/lib/utils.js b/lib/utils.js new file mode 100644 index 0000000..41159d9 --- /dev/null +++ b/lib/utils.js @@ -0,0 +1,26 @@ +const bcrypt = require('bcrypt') +const User = require('../models/user.js').User +const config = require('../configs/config') + +module.exports = { + addUser: function (request, signUpData) { + return new Promise((resolve, reject) => { + var user = new User() + // var location = new Location() + Object.assign(user, signUpData) + // Object.assign(location, locationData) + user.password = bcrypt.hashSync(signUpData.password, config.SALT_WORK_FACTOR) + user.save() + .then((result) => { + // location.save() + resolve(result) + }) + .catch(error => { + reject(error) + }) + }) + }, + test: function () { + console.log('test') + } +} diff --git a/lib/validate.js b/lib/validate.js new file mode 100644 index 0000000..c04a25e --- /dev/null +++ b/lib/validate.js @@ -0,0 +1,14 @@ +'use strict' +const User = require('../models/user.js').User + +module.exports = function (decoded, request, callback) { + User.findOne({email: decoded.email}) + .then((user) => { + if ((decoded.email) === (user.email)) { + decoded.scope = user.role + return callback(null, true, decoded) + } else { + return callback(null, false) + } + }) +} diff --git a/models/location.js b/models/location.js new file mode 100644 index 0000000..eb7d998 --- /dev/null +++ b/models/location.js @@ -0,0 +1,35 @@ +'use strict' + +const Mongoose = require('../lib/database').Mongoose +const db = require('../lib/database').db + +const locationSchema = new Mongoose.Schema({ + lat: Number, + lng: Number, + altitude: { + type: Number, + validate: { + validator: Number.isInteger, + message: '{VALUE} is not an integer value' + } + }, + coordinateAccuracy: { + type: Number, + validate: { + validator: Number.isInteger, + message: '{VALUE} is not an integer value' + } + }, + _user: { + type: Mongoose.Schema.ObjectId, + ref: 'User' + } +}, { + timestamps: true +}) + +const Location = db.model('Location', locationSchema, 'Locations') + +module.exports = { + Location: Location +} diff --git a/models/user.js b/models/user.js new file mode 100644 index 0000000..c293a7b --- /dev/null +++ b/models/user.js @@ -0,0 +1,36 @@ +'use strict' +// first_name, last_name, email_address, phone_number, password, username, +// location (latitude, longitude), role:Role Object, facebook_linked, twitter_linked, google_linked + +const Mongoose = require('../lib/database').Mongoose +const db = require('../lib/database').db +const roles = ['Administrator', 'MointainDispatcher', 'MountainRescuer', 'User'] + +const locationModel = { +} + +const userSchema = new Mongoose.Schema({ + first_name: { type: String }, + last_name: { type: String }, + phone_number: { type: String }, + username: { type: String, index: true, unique: true, sparse: true }, + password: { type: String }, + email: { type: String, unique: true, required: true }, + location: locationModel, + role: {type: String, enum: roles}, + fb_linked: {type: String}, + google_linked: {type: String}, + twitter_linked: {type: String} +}) + +userSchema.virtual('full_name').get(function () { + return `${this.first_name} ${this.last_name}` +}) + + +const User = db.model('User', userSchema, 'Users') + +module.exports = { + User: User, + Roles: roles +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..68297aa --- /dev/null +++ b/package.json @@ -0,0 +1,29 @@ +{ + "name": "pss-api", + "version": "0.0.1", + "description": "pss-tool-api", + "main": "server.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "Valentin Genev", + "license": "ISC", + "dependencies": { + "bcrypt": "^0.8.7", + "boom": "^4.1.0", + "good": "^7.0.2", + "good-console": "^6.3.1", + "good-http": "^6.1.1", + "good-squeeze": "^5.0.1", + "hapi": "^15.1.1", + "hapi-auth-jwt2": "^7.1.3", + "hapi-swagger": "^7.4.0", + "hoek": "^4.1.0", + "inert": "^4.0.2", + "joi": "^9.0.4", + "jsonwebtoken": "^7.1.9", + "mongoose": "^4.7.1", + "request": "^2.79.0", + "vision": "^4.1.1" + } +} diff --git a/routes/router.js b/routes/router.js new file mode 100644 index 0000000..cd1a6e0 --- /dev/null +++ b/routes/router.js @@ -0,0 +1,62 @@ +'use strict' +const Joi = require('joi') +const putLocationHandler = require('../handlers/putLocationHandler') +const changeRoleHandler = require('../handlers/changeRoleHandler') +const userRoutes = require('./userRoutes') + +const routes = [ + { + method: 'PUT', + path: '/location', + config: { + payload: { + output: 'data', + parse: true + }, + handler: putLocationHandler, + auth: 'jwt', + validate: { + payload: { + lat: Joi.number().required(), + lng: Joi.number().required(), + altitude: Joi.number().integer().required(), + coordinateAccuracy: Joi.number().integer().optional() + }, + headers: Joi.object({ + 'Content-Type': Joi.string().regex(/(application\/json)/), + 'Authentication-Token': Joi.string().token() + }).unknown() + } + } + }, + { + method: 'POST', + path: '/set-role', + handler: changeRoleHandler, + config: { + payload: { + output: 'data', + parse: true + }, + auth: { + strategies: ['jwt'], + scope: ['Administrator'] + }, + validate: { + payload: { + userId: Joi.string().required(), + role: Joi.string().required().allow('Administrator', 'MointainDispatcher', 'MountainRescuer', 'User') + }, + headers: Joi.object({ + 'Content-Type': Joi.string().regex(/(application\/json)/) + }).unknown() + } + } + } +] + +let route = routes.concat(userRoutes.userRoute) + +module.exports = { + route: route +} diff --git a/routes/userRoutes.js b/routes/userRoutes.js new file mode 100644 index 0000000..5d618b9 --- /dev/null +++ b/routes/userRoutes.js @@ -0,0 +1,162 @@ +const Joi = require('joi') +const loginHandler = require('../handlers/loginHandler') +const registerHandler = require('../handlers/registerHandler') +const loginFbHandler = require('../handlers/loginFbHandler') +const registerFbHandler = require('../handlers/registerFbHandler') +const updateProfileHandler = require('../handlers/updateProfileHandler') +const getProfilesHandler = require('../handlers/getProfilesHandler') +const getUserProfile = require('../handlers/getUserProfile') + +const UserRoutes = [ + { + method: 'POST', + path: '/login', + handler: loginHandler, + config: { + payload: { + output: 'data', + parse: true + }, + auth: false, + validate: { + payload: { + email: Joi.string().email(), + username: Joi.string(), + password: Joi.string().required() + } + } + } + }, + { + method: 'POST', + path: '/register', + config: { + handler: registerHandler, + payload: { + output: 'data', + parse: true + }, + auth: false, + validate: { + payload: { + user: Joi.object({ + email: Joi.string().email().required(), + password: Joi.string().required(), + confirmed_password: Joi.string().required(), + phone_number: Joi.string(), + username: Joi.string(), + role: Joi.string().required().allow('Administrator', 'MointainDispatcher', 'MountainRescuer', 'User'), + first_name: Joi.string(), + last_name: Joi.string() + }), + location: Joi.object({ + lat: Joi.number(), + lng: Joi.number() + }) + } + } + } + }, + { + method: 'POST', + path: '/login/facebook', + handler: loginFbHandler, + config: { + payload: { + output: 'data', + parse: true + }, + auth: false, + validate: { + payload: { + fb_token: Joi.string().required() + }, + headers: Joi.object({ + 'Content-Type': Joi.string().regex(/(application\/json)/) + }).unknown() + } + } + }, + { + method: 'POST', + path: '/register/facebook', + config: { + handler: registerFbHandler, + payload: { + output: 'data', + parse: true + }, + auth: false, + validate: { + payload: { + fb_token: Joi.string().token().required(), + role: Joi.string().required().allow('Administrator', 'MointainDispatcher', 'MountainRescuer', 'User'), + location: Joi.object({ + lat: Joi.number(), + lng: Joi.number() + }) + }, + headers: Joi.object({ + 'Content-Type': Joi.string().regex(/(application\/json)/) + }).unknown() + } + } + }, + { + method: 'GET', + path: '/profile/{userId}', + config: { + auth: 'jwt', + handler: getUserProfile, + validate: { + headers: Joi.object({ + 'Authentication-Token': Joi.string().token() + }).unknown() + } + } + }, + { + method: 'POST', + path: '/profile/update', + config: { + handler: updateProfileHandler, + payload: { + output: 'data', + parse: true + }, + auth: 'jwt', + validate: { + payload: { + email: Joi.string().email(), + new_password: Joi.string(), + confirmed_password: Joi.string(), + phone_number: Joi.string(), + username: Joi.string(), + first_name: Joi.string(), + last_name: Joi.string() + }, + headers: Joi.object({ + 'Content-Type': Joi.string().regex(/(application\/json)/), + 'Authentication-Token': Joi.string().token() + }).unknown() + } + } + }, + { + method: 'GET', + path: '/profiles', + config: { + auth: 'jwt', + handler: getProfilesHandler, + validate: { + headers: Joi.object({ + 'Authentication-Token': Joi.string().token() + }).unknown() + } + } + } +] + +module.exports = { + userRoute: UserRoutes +} diff --git a/server.js b/server.js new file mode 100644 index 0000000..3d99b72 --- /dev/null +++ b/server.js @@ -0,0 +1,40 @@ +'use strict' +// Load modules +const Hapi = require('hapi') +const Hoek = require('hoek') + +// Own modules +const config = require('./configs/config') +const router = require('./routes/router').route +const validate = require('./lib/validate') + +for (let route of router) { + route.config.tags = [] + route.config.tags.push('api') +} + +const server = new Hapi.Server() +server.connection({ port: 8001 }) + +server.register([ + require('inert'), + require('vision'), + require('hapi-auth-jwt2'), + { register: require('good'), options: config.goodOptions }, + { register: require('hapi-swagger'), options: config.swaggerOptions } +], (err) => { + Hoek.assert(!err, err) + server.auth.strategy('jwt', 'jwt', + { key: config.JWT.secret, + validateFunc: validate, + verifyOptions: { ignoreExpiration: true }, + headerKey: 'authentication-token' + }) + server.auth.default('jwt') + server.route(router) + server.start((err) => { + Hoek.assert(!err, err) + console.log('Server started at:', server.info.uri) + }) +}) +