diff --git a/cli/configure.js b/cli/configure.js index 0d59cbbd..c50b81fa 100644 --- a/cli/configure.js +++ b/cli/configure.js @@ -157,7 +157,7 @@ inquirer method: 'channel_new', params: { channel_name: thumbnailChannelDefault, - amount : 0.1, + amount : '0.1', }, }) .then(response => { diff --git a/cli/defaults/siteConfig.json b/cli/defaults/siteConfig.json index 8359f0e6..91efe08f 100644 --- a/cli/defaults/siteConfig.json +++ b/cli/defaults/siteConfig.json @@ -26,7 +26,11 @@ "thumbnailChannelId": null, "additionalClaimAddresses": [], "disabled": false, - "disabledMessage": "Default publishing disabled message" + "disabledMessage": "Default publishing disabled message", + "closedRegistration": false, + "serveOnlyApproved": false, + "publishOnlyApproved": false, + "approvedChannels": [] }, "startup": { "performChecks": true, diff --git a/client/src/components/ChannelTools/index.jsx b/client/src/components/ChannelTools/index.jsx deleted file mode 100644 index 5e640dd9..00000000 --- a/client/src/components/ChannelTools/index.jsx +++ /dev/null @@ -1,21 +0,0 @@ -import React from 'react'; -import ChannelLoginForm from '@containers/ChannelLoginForm'; -import ChannelCreateForm from '@containers/ChannelCreateForm'; -import Row from '@components/Row'; - -const ChannelTools = () => { - return ( -
- -

Log in to an existing channel:

- -
- -

Create a brand new channel:

- -
-
- ); -}; - -export default ChannelTools; diff --git a/client/src/containers/ChannelSelect/index.js b/client/src/containers/ChannelSelect/index.js index 9efce26d..23d731ad 100644 --- a/client/src/containers/ChannelSelect/index.js +++ b/client/src/containers/ChannelSelect/index.js @@ -1,13 +1,18 @@ import {connect} from 'react-redux'; import {setPublishInChannel, updateSelectedChannel, updateError} from '../../actions/publish'; +// import isApprovedChannel from '../../../../utils/isApprovedChannel'; import View from './view'; -const mapStateToProps = ({ channel, publish }) => { +const mapStateToProps = ({ publish, site, channel: { loggedInChannel: { name, shortId, longId } } }) => { return { - loggedInChannelName: channel.loggedInChannel.name, + // isApprovedChannel : isApprovedChannel({ longId }, site.approvedChannels), + publishOnlyApproved: site.publishOnlyApproved, + // closedRegistration : site.closedRegistration, + loggedInChannelName: name, publishInChannel : publish.publishInChannel, selectedChannel : publish.selectedChannel, channelError : publish.error.channel, + longId, }; }; diff --git a/client/src/containers/ChannelSelect/view.jsx b/client/src/containers/ChannelSelect/view.jsx index 4a4dc5fe..d04007d5 100644 --- a/client/src/containers/ChannelSelect/view.jsx +++ b/client/src/containers/ChannelSelect/view.jsx @@ -16,9 +16,12 @@ class ChannelSelect extends React.Component { this.handleSelection = this.handleSelection.bind(this); } componentWillMount () { - const { loggedInChannelName } = this.props; + const { loggedInChannelName, onChannelSelect, publishOnlyApproved, onPublishInChannelChange } = this.props; if (loggedInChannelName) { - this.props.onChannelSelect(loggedInChannelName); + onChannelSelect(loggedInChannelName); + } + if (publishOnlyApproved) { + onPublishInChannelChange(true); } } toggleAnonymousPublish (event) { @@ -34,7 +37,17 @@ class ChannelSelect extends React.Component { this.props.onChannelSelect(selectedOption); } render () { - const { publishInChannel, channelError, selectedChannel, loggedInChannelName } = this.props; + const { publishInChannel, channelError, selectedChannel, loggedInChannelName, publishOnlyApproved } = this.props; + if (publishOnlyApproved) { + return ( +
+ } + content={{loggedInChannelName}} + /> +
+ ); + } return (
{ + return { + closedRegistration, + }; +}; + +export default connect(mapStateToProps, null)(View); diff --git a/client/src/containers/ChannelTools/view.jsx b/client/src/containers/ChannelTools/view.jsx new file mode 100644 index 00000000..0be7bef0 --- /dev/null +++ b/client/src/containers/ChannelTools/view.jsx @@ -0,0 +1,23 @@ +import React from 'react'; +import ChannelLoginForm from '@containers/ChannelLoginForm'; +import ChannelCreateForm from '@containers/ChannelCreateForm'; +import Row from '@components/Row'; + +class ChannelTools extends React.Component { + render () { + return ( +
+ +

Log in to an existing channel:

+ +
+ {!this.props.closedRegistration && ( +

Create a brand new channel:

+ +
)} +
+ ); + } +} + +export default ChannelTools; diff --git a/client/src/containers/NavigationLinks/index.jsx b/client/src/containers/NavigationLinks/index.jsx index 264bb027..d5a9c454 100644 --- a/client/src/containers/NavigationLinks/index.jsx +++ b/client/src/containers/NavigationLinks/index.jsx @@ -1,12 +1,15 @@ import { connect } from 'react-redux'; import { logOutChannel, checkForLoggedInChannel } from '../../actions/channel'; +import isApprovedChannel from '../../../../utils/isApprovedChannel'; import View from './view'; -const mapStateToProps = ({ channel: { loggedInChannel: { name, shortId, longId } } }) => { +const mapStateToProps = ({ site, channel: { loggedInChannel: { name, shortId, longId } } }) => { return { - channelName : name, - channelShortId: shortId, - channelLongId : longId, + showPublish : (!site.publishOnlyApproved || isApprovedChannel({ longId }, site.approvedChannels)), + closedRegistration: site.closedRegistration, + channelName : name, + channelShortId : shortId, + channelLongId : longId, }; }; diff --git a/client/src/containers/NavigationLinks/view.jsx b/client/src/containers/NavigationLinks/view.jsx index 929006d1..01775688 100644 --- a/client/src/containers/NavigationLinks/view.jsx +++ b/client/src/containers/NavigationLinks/view.jsx @@ -28,16 +28,17 @@ class NavigationLinks extends React.Component { } } render () { + const { channelName, showPublish, closedRegistration } = this.props; return (
- Publish - + } About - { this.props.channelName ? ( + { channelName ? ( - ) : ( + ) : !closedRegistration && ( { +const mapStateToProps = ({ show, site, channel }) => { return { error : show.request.error, requestType: show.request.type, + homeChannel: site.publishOnlyApproved && !channel.loggedInChannel.name ? `${site.approvedChannels[0].name}:${site.approvedChannels[0].longId}` : null, }; }; diff --git a/client/src/pages/HomePage/view.jsx b/client/src/pages/HomePage/view.jsx index 8ca1e065..05adfe45 100644 --- a/client/src/pages/HomePage/view.jsx +++ b/client/src/pages/HomePage/view.jsx @@ -1,11 +1,14 @@ import React from 'react'; import PageLayout from '@components/PageLayout'; - import PublishTool from '@containers/PublishTool'; +import ContentPageWrapper from '@pages/ContentPageWrapper'; class HomePage extends React.Component { render () { - return ( + const { homeChannel } = this.props; + return homeChannel ? ( + + ) : ( { const channelName = params.channelName; let channelClaimId = params.channelClaimId; if (channelClaimId === 'none') channelClaimId = null; + const chanObj = {}; + if (channelName) chanObj.name = channelName; + if (channelClaimId) chanObj[(channelClaimId.length === LONG_CLAIM_LENGTH ? LONG_ID : SHORT_ID)] = channelClaimId; + if (serveOnlyApproved && !isApprovedChannel(chanObj, approvedChannels)) { + return res.status(404).json({ + success: false, + message: 'This content is unavailable', + }); + } + getChannelData(channelName, channelClaimId) .then(data => { res.status(200).json({ diff --git a/server/controllers/api/claim/publish/createPublishParams.js b/server/controllers/api/claim/publish/createPublishParams.js index facdc32f..a278bbee 100644 --- a/server/controllers/api/claim/publish/createPublishParams.js +++ b/server/controllers/api/claim/publish/createPublishParams.js @@ -18,7 +18,7 @@ const createPublishParams = (filePath, name, title, description, license, nsfw, const publishParams = { name, file_path: filePath, - bid : 0.01, + bid : '0.01', metadata : { description, title, diff --git a/server/controllers/api/claim/publish/createThumbnailPublishParams.js b/server/controllers/api/claim/publish/createThumbnailPublishParams.js index 1032c9a3..668bd07c 100644 --- a/server/controllers/api/claim/publish/createThumbnailPublishParams.js +++ b/server/controllers/api/claim/publish/createThumbnailPublishParams.js @@ -10,7 +10,7 @@ const createThumbnailPublishParams = (thumbnailFilePath, claimName, license, nsf return { name : `${claimName}-thumb`, file_path: thumbnailFilePath, - bid : 0.01, + bid : '0.01', metadata : { title : `${claimName} thumbnail`, description: `a thumbnail for ${claimName}`, diff --git a/server/controllers/api/claim/publish/index.js b/server/controllers/api/claim/publish/index.js index d636de8d..f51d4055 100644 --- a/server/controllers/api/claim/publish/index.js +++ b/server/controllers/api/claim/publish/index.js @@ -3,6 +3,8 @@ const logger = require('winston'); const { details: { host }, publishing: { disabled, disabledMessage } } = require('@config/siteConfig'); const { sendGATimingEvent } = require('../../../../utils/googleAnalytics.js'); +const isApprovedChannel = require('../../../../../utils/isApprovedChannel'); +const { publishing: { publishOnlyApproved, approvedChannels } } = require('@config/siteConfig'); const { handleErrorResponse } = require('../../../utils/errorHandlers.js'); @@ -16,6 +18,7 @@ const parsePublishApiRequestFiles = require('./parsePublishApiRequestFiles.js'); const authenticateUser = require('./authentication.js'); const CLAIM_TAKEN = 'CLAIM_TAKEN'; +const UNAPPROVED_CHANNEL = 'UNAPPROVED_CHANNEL'; /* @@ -54,6 +57,13 @@ const claimPublish = ({ body, files, headers, ip, originalUrl, user, tor }, res) // check channel authorization authenticateUser(channelName, channelId, channelPassword, user) .then(({ channelName, channelClaimId }) => { + if (publishOnlyApproved && !isApprovedChannel({ longId: channelClaimId }, approvedChannels)) { + const error = { + name : UNAPPROVED_CHANNEL, + message: 'This spee.ch instance only allows publishing to approved channels', + }; + throw error; + } return Promise.all([ checkClaimAvailability(name), createPublishParams(filePath, name, title, description, license, nsfw, thumbnail, channelName, channelClaimId), @@ -92,7 +102,7 @@ const claimPublish = ({ body, files, headers, ip, originalUrl, user, tor }, res) sendGATimingEvent('end-to-end', 'publish', fileType, gaStartTime, Date.now()); }) .catch(error => { - if (error.name === CLAIM_TAKEN) { + if ([CLAIM_TAKEN, UNAPPROVED_CHANNEL].includes(error.name)) { res.status(400).json({ success: false, message: error.message, diff --git a/server/controllers/assets/utils/getClaimIdAndServeAsset.js b/server/controllers/assets/utils/getClaimIdAndServeAsset.js index 00bf9532..1feb5ba2 100644 --- a/server/controllers/assets/utils/getClaimIdAndServeAsset.js +++ b/server/controllers/assets/utils/getClaimIdAndServeAsset.js @@ -1,6 +1,7 @@ const logger = require('winston'); const db = require('../../../models'); +const isApprovedChannel = require('../../../../utils/isApprovedChannel'); const getClaimId = require('../../utils/getClaimId.js'); const { handleErrorResponse } = require('../../utils/errorHandlers.js'); @@ -11,17 +12,28 @@ const NO_CHANNEL = 'NO_CHANNEL'; const NO_CLAIM = 'NO_CLAIM'; const BLOCKED_CLAIM = 'BLOCKED_CLAIM'; const NO_FILE = 'NO_FILE'; +const CONTENT_UNAVAILABLE = 'CONTENT_UNAVAILABLE'; + +const { publishing: { serveOnlyApproved, approvedChannels } } = require('@config/siteConfig'); const getClaimIdAndServeAsset = (channelName, channelClaimId, claimName, claimId, originalUrl, ip, res) => { getClaimId(channelName, channelClaimId, claimName, claimId) .then(fullClaimId => { claimId = fullClaimId; logger.debug('Full claim id:', fullClaimId); - return db.Claim.getOutpoint(claimName, fullClaimId); + return db.Claim.findOne({ + where: { + name : claimName, + claimId: fullClaimId, + }, + }); }) - .then(outpoint => { - logger.debug('Outpoint:', outpoint); - return db.Blocked.isNotBlocked(outpoint); + .then(claim => { + if (serveOnlyApproved && !isApprovedChannel({ longId: claim.dataValues.certificateId }, approvedChannels)) { + throw new Error(CONTENT_UNAVAILABLE); + } + logger.debug('Outpoint:', claim.dataValues.outpoint); + return db.Blocked.isNotBlocked(claim.dataValues.outpoint); }) .then(() => { return db.File.findOne({ @@ -52,6 +64,13 @@ const getClaimIdAndServeAsset = (channelName, channelClaimId, claimName, claimId message: 'No matching channel id could be found for that url', }); } + if (error === CONTENT_UNAVAILABLE) { + logger.debug('unapproved channel'); + return res.status(400).json({ + success: false, + message: 'This content is unavailable', + }); + } if (error === BLOCKED_CLAIM) { logger.debug('claim was blocked'); return res.status(451).json({ diff --git a/server/lbrynet/index.js b/server/lbrynet/index.js index 4ff64521..655909fa 100644 --- a/server/lbrynet/index.js +++ b/server/lbrynet/index.js @@ -116,7 +116,7 @@ module.exports = { method: 'channel_new', params: { channel_name: name, - amount : 0.1, + amount : '0.1', }, }) .then(response => { diff --git a/server/models/claim.js b/server/models/claim.js index 6c1f4092..63aa8b94 100644 --- a/server/models/claim.js +++ b/server/models/claim.js @@ -1,6 +1,8 @@ const logger = require('winston'); const returnShortId = require('./utils/returnShortId.js'); +const isApprovedChannel = require('../../utils/isApprovedChannel'); const { assetDefaults: { thumbnail: defaultThumbnail }, details: { host } } = require('@config/siteConfig'); +const { publishing: { serveOnlyApproved, approvedChannels } } = require('@config/siteConfig'); const NO_CLAIM = 'NO_CLAIM'; @@ -354,7 +356,7 @@ module.exports = (sequelize, { STRING, BOOLEAN, INTEGER, TEXT, DECIMAL }) => { } }; - Claim.resolveClaim = function (name, claimId) { + Claim.fetchClaim = function (name, claimId) { logger.debug(`Claim.resolveClaim: ${name} ${claimId}`); return new Promise((resolve, reject) => { this @@ -378,6 +380,23 @@ module.exports = (sequelize, { STRING, BOOLEAN, INTEGER, TEXT, DECIMAL }) => { }); }; + Claim.resolveClaim = function (name, claimId) { + return new Promise((resolve, reject) => { + this + .fetchClaim(name, claimId) + .then(claim => { + logger.info('resolveClaim claims:', claim); + if (serveOnlyApproved && !isApprovedChannel({ longId: claim.certificateId }, approvedChannels)) { + throw new Error('This content is unavailable'); + } + return resolve(claim); + }) + .catch(error => { + reject(error); + }); + }); + }; + Claim.getOutpoint = function (name, claimId) { logger.debug(`finding outpoint for ${name}#${claimId}`); return this diff --git a/server/speechPassport/utils/local-signup.js b/server/speechPassport/utils/local-signup.js index 1a2067e8..2662b721 100644 --- a/server/speechPassport/utils/local-signup.js +++ b/server/speechPassport/utils/local-signup.js @@ -2,6 +2,7 @@ const PassportLocalStrategy = require('passport-local').Strategy; const { createChannel } = require('../../lbrynet'); const logger = require('winston'); const db = require('../../models'); +const { publishing: { closedRegistration } } = require('@config/siteConfig'); module.exports = new PassportLocalStrategy( { @@ -9,10 +10,13 @@ module.exports = new PassportLocalStrategy( passwordField: 'password', }, (username, password, done) => { + if (closedRegistration) { + return done('Registration is disabled'); + } + logger.verbose(`new channel signup request. user: ${username} pass: ${password} .`); let userInfo = {}; // server-side validaton of inputs (username, password) - // create the channel and retrieve the metadata return createChannel(`@${username}`) .then(tx => { diff --git a/utils/isApprovedChannel.js b/utils/isApprovedChannel.js new file mode 100644 index 00000000..aaf8dcf8 --- /dev/null +++ b/utils/isApprovedChannel.js @@ -0,0 +1,9 @@ +function isApprovedChannel (channel, channels) { + const { name, shortId: short, longId: long } = channel; + return Boolean( + (long && channels.find(chan => chan.longId === long)) || + (name && short && channels.find(chan => chan.name === name && chan.shortId === short)) + ); +} + +module.exports = isApprovedChannel;