diff --git a/auth/authentication.js b/auth/authentication.js index c1e69def..290aeac5 100644 --- a/auth/authentication.js +++ b/auth/authentication.js @@ -2,44 +2,73 @@ const db = require('../models'); const logger = require('winston'); module.exports = { - authenticateChannelCredentials (channelName, userPassword) { + authenticateUser (channelName, channelId, channelPassword, user) { + // case: no channelName or channel Id are provided (anonymous), regardless of whether user token is provided + if (!channelName && !channelId) { + return { + channelName : null, + channelClaimId: null, + }; + } + // case: channelName or channel Id are provided with user token + if (user) { + if (channelName && channelName !== user.channelName) { + throw new Error('the provided channel name does not match user credentials'); + } + if (channelId && channelId !== user.channelClaimId) { + throw new Error('the provided channel id does not match user credentials'); + } + return { + channelName : user.channelName, + channelClaimId: user.channelClaimId, + }; + } + // case: channelName or channel Id are provided with password instead of user token + if (!channelPassword) throw new Error('no channel password provided'); + return module.exports.authenticateChannelCredentials(channelName, channelId, channelPassword); + }, + authenticateChannelCredentials (channelName, channelId, userPassword) { return new Promise((resolve, reject) => { - const userName = channelName.substring(1); - logger.debug(`authenticateChannelCredentials > channelName: ${channelName} username: ${userName} pass: ${userPassword}`); - db.User - .findOne({where: { userName }}) + // hoisted variables + let channelData; + // build the params for finding the channel + let channelFindParams = {}; + if (channelName) channelFindParams['channelName'] = channelName; + if (channelId) channelFindParams['channelClaimId'] = channelId; + // find the channel + db.Channel + .findOne({ + where: channelFindParams, + }) + .then(channel => { + if (!channel) { + logger.debug('no channel found'); + throw new Error('Authentication failed, you do not have access to that channel'); + } + channelData = channel.get(); + logger.debug('channel data:', channelData); + return db.User.findOne({ + where: { userName: channelData.channelName.substring(1) }, + }); + }) .then(user => { if (!user) { logger.debug('no user found'); - resolve(false); - return; + throw new Error('Authentication failed, you do not have access to that channel'); } - return user.comparePassword(userPassword, (passwordErr, isMatch) => { - if (passwordErr) { - logger.error('comparePassword error:', passwordErr); - resolve(false); - return; - } - if (!isMatch) { - logger.debug('incorrect password'); - resolve(false); - return; - } - logger.debug('...password was a match...'); - resolve(true); - }); + return user.comparePassword(userPassword); + }) + .then(isMatch => { + if (!isMatch) { + logger.debug('incorrect password'); + throw new Error('Authentication failed, you do not have access to that channel'); + } + logger.debug('...password was a match...'); + resolve(channelData); }) .catch(error => { reject(error); }); }); }, - authenticateIfNoUserToken (channelName, channelPassword, user) { - return new Promise((resolve, reject) => { - if (user || !channelName) { - return resolve(true); - } - return resolve(module.exports.authenticateChannelCredentials(channelName, channelPassword)); - }); - }, }; diff --git a/config/speechConfig.js.example b/config/speechConfig.js.example index 863e11ef..af4b1de7 100644 --- a/config/speechConfig.js.example +++ b/config/speechConfig.js.example @@ -38,7 +38,8 @@ module.exports = { defaultDescription: 'Open-source, decentralized image and video sharing.', }, testing: { - testChannel : '@testpublishchannel', // a channel to make test publishes in + testChannel : '@testpublishchannel', // a channel to make test publishes in + testChannelId : 'xyz123...', // the claim id for the test channel testChannelPassword: 'password', // password for the test channel }, api: { diff --git a/controllers/publishController.js b/controllers/publishController.js index 9e912cd7..18fa640e 100644 --- a/controllers/publishController.js +++ b/controllers/publishController.js @@ -86,31 +86,24 @@ module.exports = { }); }); }, - checkClaimNameAvailability (name) { - return new Promise((resolve, reject) => { - // find any records where the name is used - db.File.findAll({ where: { name } }) - .then(result => { - if (result.length >= 1) { - const claimAddress = config.wallet.lbryClaimAddress; - // filter out any results that were not published from spee.ch's wallet address - const filteredResult = result.filter((claim) => { - return (claim.address === claimAddress); - }); - // return based on whether any non-spee.ch claims were left - if (filteredResult.length >= 1) { - resolve(false); - } else { - resolve(true); - } - } else { - resolve(true); - } - }) - .catch(error => { - reject(error); - }); - }); + claimNameIsAvailable (name) { + // find any records where the name is used + return db.File.findAll({ where: { name } }) + .then(result => { + if (result.length >= 1) { + const claimAddress = config.wallet.lbryClaimAddress; + // filter out any results that were not published from spee.ch's wallet address + const filteredResult = result.filter((claim) => { + return (claim.address === claimAddress); + }); + // return based on whether any non-spee.ch claims were left + if (filteredResult.length >= 1) { + throw new Error('That claim is already in use'); + }; + return name; + }; + return name; + }); }, checkChannelAvailability (name) { return new Promise((resolve, reject) => { diff --git a/helpers/publishHelpers.js b/helpers/publishHelpers.js index 9a25ec42..5e515834 100644 --- a/helpers/publishHelpers.js +++ b/helpers/publishHelpers.js @@ -58,30 +58,6 @@ module.exports = { thumbnailFileType: (thumbnail ? thumbnail.type : null), }; }, - parsePublishApiChannel ({channelName, channelPassword}, user) { - logger.debug('api/claim/publish, channel data:', {channelName, channelPassword, user}); - // if anonymous or '' provided, publish will be anonymous (even if client is logged in) - // if a channel name is provided... - if (channelName) { - // make sure a password was provided if no user token is provided - if (!user && !channelPassword) { - throw new Error('Unauthenticated channel name provided without password'); - } - // if request comes from the client with a token - // ensure this publish uses that channel name - if (user) { - channelName = user.channelName; - } ; - // add the @ if the channel name is missing it - if (channelName.indexOf('@') !== 0) { - channelName = `@${channelName}`; - } - } - return { - channelName, - channelPassword, - }; - }, validateFileTypeAndSize (file) { // check file type and size switch (file.type) { @@ -111,7 +87,7 @@ module.exports = { } return file; }, - createPublishParams (filePath, name, title, description, license, nsfw, thumbnail, channelName) { + createBasicPublishParams (filePath, name, title, description, license, nsfw, thumbnail) { logger.debug(`Creating Publish Parameters`); // provide defaults for title if (title === null || title.trim() === '') { @@ -144,10 +120,6 @@ module.exports = { if (thumbnail) { publishParams['metadata']['thumbnail'] = thumbnail; } - // add channel to params, if applicable - if (channelName) { - publishParams['channel_name'] = channelName; - } return publishParams; }, createThumbnailPublishParams (thumbnailFilePath, claimName, license, nsfw) { diff --git a/models/user.js b/models/user.js index 55f75114..ed3de074 100644 --- a/models/user.js +++ b/models/user.js @@ -24,8 +24,8 @@ module.exports = (sequelize, { STRING }) => { User.hasOne(db.Channel); }; - User.prototype.comparePassword = function (password, callback) { - bcrypt.compare(password, this.password, callback); + User.prototype.comparePassword = function (password) { + return bcrypt.compare(password, this.password); }; User.prototype.changePassword = function (newPassword) { diff --git a/passport/local-login.js b/passport/local-login.js index 99e9fc50..89b5a968 100644 --- a/passport/local-login.js +++ b/passport/local-login.js @@ -30,33 +30,33 @@ module.exports = new PassportLocalStrategy( passwordField: 'password', }, (username, password, done) => { - logger.debug('logging user in'); - return db - .User - .findOne({where: {userName: username}}) + return db.User + .findOne({ + where: {userName: username}, + }) .then(user => { if (!user) { - // logger.debug('no user found'); + logger.debug('no user found'); return done(null, false, {message: 'Incorrect username or password'}); } - user.comparePassword(password, (passwordErr, isMatch) => { - if (passwordErr) { - logger.error('passwordErr:', passwordErr); - return done(null, false, {message: passwordErr}); - } - if (!isMatch) { - // logger.debug('incorrect password'); - return done(null, false, {message: 'Incorrect username or password'}); - } - logger.debug('Password was a match, returning User'); - return returnUserAndChannelInfo(user) - .then((userInfo) => { - return done(null, userInfo); - }) - .catch(error => { - return done(error); - }); - }); + return user.comparePassword(password) + .then(isMatch => { + if (!isMatch) { + logger.debug('incorrect password'); + return done(null, false, {message: 'Incorrect username or password'}); + } + logger.debug('Password was a match, returning User'); + return returnUserAndChannelInfo(user) + .then(userInfo => { + return done(null, userInfo); + }) + .catch(error => { + return error; + }); + }) + .catch(error => { + return error; + }); }) .catch(error => { return done(error); diff --git a/react/containers/PublishUrlInput/view.jsx b/react/containers/PublishUrlInput/view.jsx index 8c238118..ed782f1a 100644 --- a/react/containers/PublishUrlInput/view.jsx +++ b/react/containers/PublishUrlInput/view.jsx @@ -37,13 +37,8 @@ class PublishUrlInput extends React.Component { } checkClaimIsAvailable (claim) { request(`/api/claim/availability/${claim}`) - .then(isAvailable => { - console.log('checkClaimIsAvailable request response:', isAvailable); - if (isAvailable) { - this.props.onUrlError(null); - } else { - this.props.onUrlError('That url has already been claimed'); - } + .then(() => { + this.props.onUrlError(null); }) .catch((error) => { this.props.onUrlError(error.message); diff --git a/routes/api-routes.js b/routes/api-routes.js index af50684e..eb5cd49e 100644 --- a/routes/api-routes.js +++ b/routes/api-routes.js @@ -3,12 +3,14 @@ const multipart = require('connect-multiparty'); const { files, site } = require('../config/speechConfig.js'); const multipartMiddleware = multipart({uploadDir: files.uploadDirectory}); const db = require('../models'); -const { checkClaimNameAvailability, checkChannelAvailability, publish } = require('../controllers/publishController.js'); +const { claimNameIsAvailable, checkChannelAvailability, publish } = require('../controllers/publishController.js'); const { getClaimList, resolveUri, getClaim } = require('../helpers/lbryApi.js'); -const { addGetResultsToFileData, createFileData, createPublishParams, createThumbnailPublishParams, parsePublishApiRequestBody, parsePublishApiRequestFiles, parsePublishApiChannel } = require('../helpers/publishHelpers.js'); + +const { addGetResultsToFileData, createBasicPublishParams, createThumbnailPublishParams, parsePublishApiRequestBody, parsePublishApiRequestFiles, createFileData } = require('../helpers/publishHelpers.js'); + const errorHandlers = require('../helpers/errorHandlers.js'); const { sendGAAnonymousPublishTiming, sendGAChannelPublishTiming } = require('../helpers/googleAnalytics.js'); -const { authenticateIfNoUserToken } = require('../auth/authentication.js'); +const { authenticateUser } = require('../auth/authentication.js'); const { getChannelData, getChannelClaims, getClaimId } = require('../controllers/serveController.js'); const NO_CHANNEL = 'NO_CHANNEL'; @@ -108,13 +110,9 @@ module.exports = (app) => { }); // route to check whether this site published to a claim app.get('/api/claim/availability/:name', ({ ip, originalUrl, params }, res) => { - checkClaimNameAvailability(params.name) + claimNameIsAvailable(params.name) .then(result => { - if (result === true) { - res.status(200).json(true); - } else { - res.status(200).json(false); - } + res.status(200).json(result); }) .catch(error => { errorHandlers.handleErrorResponse(originalUrl, ip, error, res); @@ -135,7 +133,7 @@ module.exports = (app) => { logger.debug('api/claim/publish req.body:', body); logger.debug('api/claim/publish req.files:', files); // define variables - let name, fileName, filePath, fileType, thumbnailFileName, thumbnailFilePath, thumbnailFileType, nsfw, license, title, description, thumbnail, channelName, channelPassword; + let name, fileName, filePath, fileType, thumbnailFileName, thumbnailFilePath, thumbnailFileType, nsfw, license, title, description, thumbnail, channelName, channelId, channelPassword; // record the start time of the request const publishStartTime = Date.now(); // validate the body and files of the request @@ -143,30 +141,23 @@ module.exports = (app) => { // validateApiPublishRequest(body, files); ({name, nsfw, license, title, description, thumbnail} = parsePublishApiRequestBody(body)); ({fileName, filePath, fileType, thumbnailFileName, thumbnailFilePath, thumbnailFileType} = parsePublishApiRequestFiles(files)); - ({channelName, channelPassword} = parsePublishApiChannel(body, user)); + ({channelName, channelId, channelPassword} = body); } catch (error) { return res.status(400).json({success: false, message: error.message}); } // check channel authorization - authenticateIfNoUserToken(channelName, channelPassword, user) - .then(authenticated => { - if (!authenticated) { - throw new Error('Authentication failed, you do not have access to that channel'); + Promise.all([ + authenticateUser(channelName, channelId, channelPassword, user), + claimNameIsAvailable(name), + createBasicPublishParams(filePath, name, title, description, license, nsfw, thumbnail), + createThumbnailPublishParams(thumbnailFilePath, name, license, nsfw), + ]) + .then(([{channelName, channelClaimId}, validatedClaimName, publishParams, thumbnailPublishParams]) => { + // add channel details to the publish params + if (channelName && channelClaimId) { + publishParams['channel_name'] = channelName; + publishParams['channel_id'] = channelClaimId; } - // make sure the claim name is available - return checkClaimNameAvailability(name); - }) - .then(result => { - if (!result) { - throw new Error('That name is already claimed by another user.'); - } - // create publish parameters object - return Promise.all([ - createPublishParams(filePath, name, title, description, license, nsfw, thumbnail, channelName), - createThumbnailPublishParams(thumbnailFilePath, name, license, nsfw), - ]); - }) - .then(([publishParams, thumbnailPublishParams]) => { // publish the thumbnail if (thumbnailPublishParams) { publish(thumbnailPublishParams, thumbnailFileName, thumbnailFileType); diff --git a/task-scripts/update-password.js b/task-scripts/update-password.js index 89cff11f..e5bfb7d5 100644 --- a/task-scripts/update-password.js +++ b/task-scripts/update-password.js @@ -26,18 +26,17 @@ db.sequelize.sync() // sync sequelize if (!user) { throw new Error('no user found'); } - return new Promise((resolve, reject) => { - user.comparePassword(oldPassword, (passwordErr, isMatch) => { - if (passwordErr) { - return reject(passwordErr); - } - if (!isMatch) { - return reject('Incorrect old password.'); - } - logger.debug('Password was a match, updating password'); - return resolve(user.changePassword(newPassword)); - }); - }); + return Promise.all([ + user.comparePassword(oldPassword), + user, + ]); + }) + .then(([isMatch, user]) => { + if (!isMatch) { + throw new Error('Incorrect old password.'); + } + logger.debug('Password was a match, updating password'); + return user.changePassword(newPassword); }) .then(() => { logger.debug('Password successfully updated'); diff --git a/test/end-to-end/end-to-end.tests.js b/test/end-to-end/end-to-end.tests.js index 09290733..f6ed4bc4 100644 --- a/test/end-to-end/end-to-end.tests.js +++ b/test/end-to-end/end-to-end.tests.js @@ -3,7 +3,7 @@ const expect = chai.expect; const chaiHttp = require('chai-http'); const { site, testing } = require('../../config/speechConfig.js'); const { host } = site; -const { testChannel, testChannelPassword } = testing; +const { testChannel, testChannelId, testChannelPassword } = testing; const requestTimeout = 20000; const publishTimeout = 120000; const fs = require('fs'); @@ -109,19 +109,64 @@ describe('end-to-end', function () { const filePath = './test/mock-data/bird.jpeg'; const fileName = 'byrd.jpeg'; const channelName = testChannel; + const channelId = testChannelId; const channelPassword = testChannelPassword; + const date = new Date(); + const name = `test-publish-${date.getFullYear()}-${date.getMonth()}-${date.getDate()}-${date.getTime()}`; - describe('anonymous publishes', function () { - it(`should receive a status code 200 within ${publishTimeout}ms @usesLbc`, function (done) { - const date = new Date(); - const name = `test-publish-${date.getFullYear()}-${date.getMonth()}-${date.getDate()}-${date.getTime()}`; + describe('api/claim/publish', function () { + + it(`should receive a status code 400 if username does not exist`, function (done) { chai.request(host) .post(publishUrl) .type('form') .attach('file', fs.readFileSync(filePath), fileName) .field('name', name) + .field('channelName', `@${name}`) + .field('channelPassword', channelPassword) + .end(function (err, res) { + expect(res).to.have.status(400); + done(); + }); + }).timeout(publishTimeout); + + it(`should receive a status code 400 if the wrong password is used with the channel name`, function (done) { + chai.request(host) + .post(publishUrl) + .type('form') + .attach('file', fs.readFileSync(filePath), fileName) + .field('name', name) + .field('channelName', channelName) + .field('channelPassword', 'xxxxx') + .end(function (err, res) { + expect(res).to.have.status(400); + done(); + }); + }).timeout(publishTimeout); + + it(`should receive a status code 400 if the wrong password is used with the channel id`, function (done) { + chai.request(host) + .post(publishUrl) + .type('form') + .attach('file', fs.readFileSync(filePath), fileName) + .field('name', name) + .field('channelName', channelName) + .field('channelPassword', 'xxxxx') + .end(function (err, res) { + expect(res).to.have.status(400); + done(); + }); + }).timeout(publishTimeout); + }); + + describe('anonymous publishes', function () { + it(`should receive a status code 200 within ${publishTimeout}ms @usesLbc`, function (done) { + chai.request(host) + .post(publishUrl) + .type('form') + .attach('file', fs.readFileSync(filePath), fileName) + .field('name', `${name}-anonymous`) .end(function (err, res) { - // expect(err).to.be.null; expect(res).to.have.status(200); done(); }); @@ -130,17 +175,14 @@ describe('end-to-end', function () { describe('in-channel publishes', function () { it(`should receive a status code 200 within ${publishTimeout}ms @usesLbc`, function (done) { - const date = new Date(); - const name = `test-publish-${date.getFullYear()}-${date.getMonth()}-${date.getDate()}-${date.getTime()}`; chai.request(host) .post(publishUrl) .type('form') .attach('file', fs.readFileSync(filePath), fileName) - .field('name', name) + .field('name', `${name}-channel`) .field('channelName', channelName) .field('channelPassword', channelPassword) .end(function (err, res) { - // expect(err).to.be.null; expect(res).to.have.status(200); done(); });