Merge pull request #605 from lbryio/multisite-555

Multisite 555
This commit is contained in:
Travis Eden 2018-10-05 09:33:27 -04:00 committed by GitHub
commit fd86b7ce49
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 194 additions and 60 deletions

View file

@ -157,7 +157,7 @@ inquirer
method: 'channel_new', method: 'channel_new',
params: { params: {
channel_name: thumbnailChannelDefault, channel_name: thumbnailChannelDefault,
amount : 0.1, amount : '0.1',
}, },
}) })
.then(response => { .then(response => {

View file

@ -26,7 +26,11 @@
"thumbnailChannelId": null, "thumbnailChannelId": null,
"additionalClaimAddresses": [], "additionalClaimAddresses": [],
"disabled": false, "disabled": false,
"disabledMessage": "Default publishing disabled message" "disabledMessage": "Default publishing disabled message",
"closedRegistration": false,
"serveOnlyApproved": false,
"publishOnlyApproved": false,
"approvedChannels": []
}, },
"startup": { "startup": {
"performChecks": true, "performChecks": true,

View file

@ -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 (
<div>
<Row>
<h3>Log in to an existing channel:</h3>
<ChannelLoginForm />
</Row>
<Row>
<h3>Create a brand new channel:</h3>
<ChannelCreateForm />
</Row>
</div>
);
};
export default ChannelTools;

View file

@ -1,13 +1,18 @@
import {connect} from 'react-redux'; import {connect} from 'react-redux';
import {setPublishInChannel, updateSelectedChannel, updateError} from '../../actions/publish'; import {setPublishInChannel, updateSelectedChannel, updateError} from '../../actions/publish';
// import isApprovedChannel from '../../../../utils/isApprovedChannel';
import View from './view'; import View from './view';
const mapStateToProps = ({ channel, publish }) => { const mapStateToProps = ({ publish, site, channel: { loggedInChannel: { name, shortId, longId } } }) => {
return { return {
loggedInChannelName: channel.loggedInChannel.name, // isApprovedChannel : isApprovedChannel({ longId }, site.approvedChannels),
publishOnlyApproved: site.publishOnlyApproved,
// closedRegistration : site.closedRegistration,
loggedInChannelName: name,
publishInChannel : publish.publishInChannel, publishInChannel : publish.publishInChannel,
selectedChannel : publish.selectedChannel, selectedChannel : publish.selectedChannel,
channelError : publish.error.channel, channelError : publish.error.channel,
longId,
}; };
}; };

View file

@ -16,9 +16,12 @@ class ChannelSelect extends React.Component {
this.handleSelection = this.handleSelection.bind(this); this.handleSelection = this.handleSelection.bind(this);
} }
componentWillMount () { componentWillMount () {
const { loggedInChannelName } = this.props; const { loggedInChannelName, onChannelSelect, publishOnlyApproved, onPublishInChannelChange } = this.props;
if (loggedInChannelName) { if (loggedInChannelName) {
this.props.onChannelSelect(loggedInChannelName); onChannelSelect(loggedInChannelName);
}
if (publishOnlyApproved) {
onPublishInChannelChange(true);
} }
} }
toggleAnonymousPublish (event) { toggleAnonymousPublish (event) {
@ -34,7 +37,17 @@ class ChannelSelect extends React.Component {
this.props.onChannelSelect(selectedOption); this.props.onChannelSelect(selectedOption);
} }
render () { render () {
const { publishInChannel, channelError, selectedChannel, loggedInChannelName } = this.props; const { publishInChannel, channelError, selectedChannel, loggedInChannelName, publishOnlyApproved } = this.props;
if (publishOnlyApproved) {
return (
<div>
<RowLabeled
label={<Label value={'Channel:'} />}
content={<span>{loggedInChannelName}</span>}
/>
</div>
);
}
return ( return (
<div> <div>
<RowLabeled <RowLabeled

View file

@ -0,0 +1,10 @@
import { connect } from 'react-redux';
import View from './view';
const mapStateToProps = ({ site: { closedRegistration } }) => {
return {
closedRegistration,
};
};
export default connect(mapStateToProps, null)(View);

View file

@ -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 (
<div>
<Row>
<h3>Log in to an existing channel:</h3>
<ChannelLoginForm />
</Row>
{!this.props.closedRegistration && (<Row>
<h3>Create a brand new channel:</h3>
<ChannelCreateForm />
</Row>)}
</div>
);
}
}
export default ChannelTools;

View file

@ -1,12 +1,15 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { logOutChannel, checkForLoggedInChannel } from '../../actions/channel'; import { logOutChannel, checkForLoggedInChannel } from '../../actions/channel';
import isApprovedChannel from '../../../../utils/isApprovedChannel';
import View from './view'; import View from './view';
const mapStateToProps = ({ channel: { loggedInChannel: { name, shortId, longId } } }) => { const mapStateToProps = ({ site, channel: { loggedInChannel: { name, shortId, longId } } }) => {
return { return {
channelName : name, showPublish : (!site.publishOnlyApproved || isApprovedChannel({ longId }, site.approvedChannels)),
channelShortId: shortId, closedRegistration: site.closedRegistration,
channelLongId : longId, channelName : name,
channelShortId : shortId,
channelLongId : longId,
}; };
}; };

View file

@ -28,16 +28,17 @@ class NavigationLinks extends React.Component {
} }
} }
render () { render () {
const { channelName, showPublish, closedRegistration } = this.props;
return ( return (
<div className='navigation-links'> <div className='navigation-links'>
<NavLink {showPublish && <NavLink
className='nav-bar-link link--nav' className='nav-bar-link link--nav'
activeClassName='link--nav-active' activeClassName='link--nav-active'
to='/' to='/'
exact exact
> >
Publish Publish
</NavLink> </NavLink>}
<NavLink <NavLink
className='nav-bar-link link--nav' className='nav-bar-link link--nav'
activeClassName='link--nav-active' activeClassName='link--nav-active'
@ -45,7 +46,7 @@ class NavigationLinks extends React.Component {
> >
About About
</NavLink> </NavLink>
{ this.props.channelName ? ( { channelName ? (
<NavBarChannelOptionsDropdown <NavBarChannelOptionsDropdown
channelName={this.props.channelName} channelName={this.props.channelName}
handleSelection={this.handleSelection} handleSelection={this.handleSelection}
@ -53,7 +54,7 @@ class NavigationLinks extends React.Component {
VIEW={VIEW} VIEW={VIEW}
LOGOUT={LOGOUT} LOGOUT={LOGOUT}
/> />
) : ( ) : !closedRegistration && (
<NavLink <NavLink
id='nav-bar-login-link' id='nav-bar-login-link'
className='nav-bar-link link--nav' className='nav-bar-link link--nav'

View file

@ -3,12 +3,14 @@ import ErrorPage from '@pages/ErrorPage';
import ShowAssetLite from '@pages/ShowAssetLite'; import ShowAssetLite from '@pages/ShowAssetLite';
import ShowAssetDetails from '@pages/ShowAssetDetails'; import ShowAssetDetails from '@pages/ShowAssetDetails';
import ShowChannel from '@pages/ShowChannel'; import ShowChannel from '@pages/ShowChannel';
import { withRouter } from 'react-router-dom';
import { CHANNEL, ASSET_LITE, ASSET_DETAILS } from '../../constants/show_request_types'; import { CHANNEL, ASSET_LITE, ASSET_DETAILS } from '../../constants/show_request_types';
class ContentPageWrapper extends React.Component { class ContentPageWrapper extends React.Component {
componentDidMount () { componentDidMount () {
this.props.onHandleShowPageUri(this.props.match.params); const { onHandleShowPageUri, match, homeChannel } = this.props;
onHandleShowPageUri(homeChannel ? { claim: homeChannel } : match.params);
} }
componentWillReceiveProps (nextProps) { componentWillReceiveProps (nextProps) {
if (nextProps.match.params !== this.props.match.params) { if (nextProps.match.params !== this.props.match.params) {
@ -35,4 +37,4 @@ class ContentPageWrapper extends React.Component {
} }
}; };
export default ContentPageWrapper; export default withRouter(ContentPageWrapper);

View file

@ -2,10 +2,11 @@ import { connect } from 'react-redux';
import { onHandleShowPageUri } from '../../actions/show'; import { onHandleShowPageUri } from '../../actions/show';
import View from './view'; import View from './view';
const mapStateToProps = ({ show }) => { const mapStateToProps = ({ show, site, channel }) => {
return { return {
error : show.request.error, error : show.request.error,
requestType: show.request.type, requestType: show.request.type,
homeChannel: site.publishOnlyApproved && !channel.loggedInChannel.name ? `${site.approvedChannels[0].name}:${site.approvedChannels[0].longId}` : null,
}; };
}; };

View file

@ -1,11 +1,14 @@
import React from 'react'; import React from 'react';
import PageLayout from '@components/PageLayout'; import PageLayout from '@components/PageLayout';
import PublishTool from '@containers/PublishTool'; import PublishTool from '@containers/PublishTool';
import ContentPageWrapper from '@pages/ContentPageWrapper';
class HomePage extends React.Component { class HomePage extends React.Component {
render () { render () {
return ( const { homeChannel } = this.props;
return homeChannel ? (
<ContentPageWrapper homeChannel={homeChannel} />
) : (
<PageLayout <PageLayout
pageTitle={'Speech'} pageTitle={'Speech'}
pageUri={''} pageUri={''}

View file

@ -4,7 +4,7 @@ import PageLayout from '@components/PageLayout';
import HorizontalSplit from '@components/HorizontalSplit'; import HorizontalSplit from '@components/HorizontalSplit';
import ChannelAbout from '@components/ChannelAbout'; import ChannelAbout from '@components/ChannelAbout';
import ChannelTools from '@components/ChannelTools'; import ChannelTools from '@containers/ChannelTools';
class LoginPage extends React.Component { class LoginPage extends React.Component {
componentWillReceiveProps (newProps) { componentWillReceiveProps (newProps) {

View file

@ -1,13 +1,18 @@
import siteConfig from '@config/siteConfig.json'; import siteConfig from '@config/siteConfig.json';
let initialState = { let initialState = {
description : 'default description', description : 'default description',
googleAnalyticsId : 'default google id', googleAnalyticsId : 'default google id',
host : 'default host', host : 'default host',
title : 'default title', title : 'default title',
twitter : 'default twitter', twitter : 'default twitter',
defaultDescription: 'default description', defaultDescription : 'default description',
defaultThumbnail : 'default thumbnail', defaultThumbnail : 'default thumbnail',
closedRegistration : false,
serveOnlyApproved : false,
publishOnlyApproved: false,
approvedChannels : [],
}; };
if (siteConfig) { if (siteConfig) {
@ -25,6 +30,12 @@ if (siteConfig) {
title, title,
twitter, twitter,
}, },
publishing: {
closedRegistration,
serveOnlyApproved,
publishOnlyApproved,
approvedChannels,
},
} = siteConfig; } = siteConfig;
initialState = { initialState = {
@ -35,6 +46,10 @@ if (siteConfig) {
twitter, twitter,
defaultDescription, defaultDescription,
defaultThumbnail, defaultThumbnail,
closedRegistration,
serveOnlyApproved,
publishOnlyApproved,
approvedChannels,
}; };
} }

View file

@ -1,8 +1,12 @@
const { handleErrorResponse } = require('../../../utils/errorHandlers.js'); const { handleErrorResponse } = require('../../../utils/errorHandlers.js');
const getChannelData = require('./getChannelData.js'); const getChannelData = require('./getChannelData.js');
const isApprovedChannel = require('../../../../../utils/isApprovedChannel');
const { publishing: { serveOnlyApproved, approvedChannels } } = require('@config/siteConfig');
const NO_CHANNEL = 'NO_CHANNEL'; const NO_CHANNEL = 'NO_CHANNEL';
const LONG_ID = 'longId';
const SHORT_ID = 'shortId';
const LONG_CLAIM_LENGTH = 40;
/* /*
@ -14,6 +18,16 @@ const channelData = ({ ip, originalUrl, body, params }, res) => {
const channelName = params.channelName; const channelName = params.channelName;
let channelClaimId = params.channelClaimId; let channelClaimId = params.channelClaimId;
if (channelClaimId === 'none') channelClaimId = null; 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) getChannelData(channelName, channelClaimId)
.then(data => { .then(data => {
res.status(200).json({ res.status(200).json({

View file

@ -18,7 +18,7 @@ const createPublishParams = (filePath, name, title, description, license, nsfw,
const publishParams = { const publishParams = {
name, name,
file_path: filePath, file_path: filePath,
bid : 0.01, bid : '0.01',
metadata : { metadata : {
description, description,
title, title,

View file

@ -10,7 +10,7 @@ const createThumbnailPublishParams = (thumbnailFilePath, claimName, license, nsf
return { return {
name : `${claimName}-thumb`, name : `${claimName}-thumb`,
file_path: thumbnailFilePath, file_path: thumbnailFilePath,
bid : 0.01, bid : '0.01',
metadata : { metadata : {
title : `${claimName} thumbnail`, title : `${claimName} thumbnail`,
description: `a thumbnail for ${claimName}`, description: `a thumbnail for ${claimName}`,

View file

@ -3,6 +3,8 @@ const logger = require('winston');
const { details: { host }, publishing: { disabled, disabledMessage } } = require('@config/siteConfig'); const { details: { host }, publishing: { disabled, disabledMessage } } = require('@config/siteConfig');
const { sendGATimingEvent } = require('../../../../utils/googleAnalytics.js'); const { sendGATimingEvent } = require('../../../../utils/googleAnalytics.js');
const isApprovedChannel = require('../../../../../utils/isApprovedChannel');
const { publishing: { publishOnlyApproved, approvedChannels } } = require('@config/siteConfig');
const { handleErrorResponse } = require('../../../utils/errorHandlers.js'); const { handleErrorResponse } = require('../../../utils/errorHandlers.js');
@ -16,6 +18,7 @@ const parsePublishApiRequestFiles = require('./parsePublishApiRequestFiles.js');
const authenticateUser = require('./authentication.js'); const authenticateUser = require('./authentication.js');
const CLAIM_TAKEN = 'CLAIM_TAKEN'; 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 // check channel authorization
authenticateUser(channelName, channelId, channelPassword, user) authenticateUser(channelName, channelId, channelPassword, user)
.then(({ channelName, channelClaimId }) => { .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([ return Promise.all([
checkClaimAvailability(name), checkClaimAvailability(name),
createPublishParams(filePath, name, title, description, license, nsfw, thumbnail, channelName, channelClaimId), 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()); sendGATimingEvent('end-to-end', 'publish', fileType, gaStartTime, Date.now());
}) })
.catch(error => { .catch(error => {
if (error.name === CLAIM_TAKEN) { if ([CLAIM_TAKEN, UNAPPROVED_CHANNEL].includes(error.name)) {
res.status(400).json({ res.status(400).json({
success: false, success: false,
message: error.message, message: error.message,

View file

@ -1,6 +1,7 @@
const logger = require('winston'); const logger = require('winston');
const db = require('../../../models'); const db = require('../../../models');
const isApprovedChannel = require('../../../../utils/isApprovedChannel');
const getClaimId = require('../../utils/getClaimId.js'); const getClaimId = require('../../utils/getClaimId.js');
const { handleErrorResponse } = require('../../utils/errorHandlers.js'); const { handleErrorResponse } = require('../../utils/errorHandlers.js');
@ -11,17 +12,28 @@ const NO_CHANNEL = 'NO_CHANNEL';
const NO_CLAIM = 'NO_CLAIM'; const NO_CLAIM = 'NO_CLAIM';
const BLOCKED_CLAIM = 'BLOCKED_CLAIM'; const BLOCKED_CLAIM = 'BLOCKED_CLAIM';
const NO_FILE = 'NO_FILE'; 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) => { const getClaimIdAndServeAsset = (channelName, channelClaimId, claimName, claimId, originalUrl, ip, res) => {
getClaimId(channelName, channelClaimId, claimName, claimId) getClaimId(channelName, channelClaimId, claimName, claimId)
.then(fullClaimId => { .then(fullClaimId => {
claimId = fullClaimId; claimId = fullClaimId;
logger.debug('Full claim id:', fullClaimId); logger.debug('Full claim id:', fullClaimId);
return db.Claim.getOutpoint(claimName, fullClaimId); return db.Claim.findOne({
where: {
name : claimName,
claimId: fullClaimId,
},
});
}) })
.then(outpoint => { .then(claim => {
logger.debug('Outpoint:', outpoint); if (serveOnlyApproved && !isApprovedChannel({ longId: claim.dataValues.certificateId }, approvedChannels)) {
return db.Blocked.isNotBlocked(outpoint); throw new Error(CONTENT_UNAVAILABLE);
}
logger.debug('Outpoint:', claim.dataValues.outpoint);
return db.Blocked.isNotBlocked(claim.dataValues.outpoint);
}) })
.then(() => { .then(() => {
return db.File.findOne({ 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', 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) { if (error === BLOCKED_CLAIM) {
logger.debug('claim was blocked'); logger.debug('claim was blocked');
return res.status(451).json({ return res.status(451).json({

View file

@ -116,7 +116,7 @@ module.exports = {
method: 'channel_new', method: 'channel_new',
params: { params: {
channel_name: name, channel_name: name,
amount : 0.1, amount : '0.1',
}, },
}) })
.then(response => { .then(response => {

View file

@ -1,6 +1,8 @@
const logger = require('winston'); const logger = require('winston');
const returnShortId = require('./utils/returnShortId.js'); const returnShortId = require('./utils/returnShortId.js');
const isApprovedChannel = require('../../utils/isApprovedChannel');
const { assetDefaults: { thumbnail: defaultThumbnail }, details: { host } } = require('@config/siteConfig'); const { assetDefaults: { thumbnail: defaultThumbnail }, details: { host } } = require('@config/siteConfig');
const { publishing: { serveOnlyApproved, approvedChannels } } = require('@config/siteConfig');
const NO_CLAIM = 'NO_CLAIM'; 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}`); logger.debug(`Claim.resolveClaim: ${name} ${claimId}`);
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
this 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) { Claim.getOutpoint = function (name, claimId) {
logger.debug(`finding outpoint for ${name}#${claimId}`); logger.debug(`finding outpoint for ${name}#${claimId}`);
return this return this

View file

@ -2,6 +2,7 @@ const PassportLocalStrategy = require('passport-local').Strategy;
const { createChannel } = require('../../lbrynet'); const { createChannel } = require('../../lbrynet');
const logger = require('winston'); const logger = require('winston');
const db = require('../../models'); const db = require('../../models');
const { publishing: { closedRegistration } } = require('@config/siteConfig');
module.exports = new PassportLocalStrategy( module.exports = new PassportLocalStrategy(
{ {
@ -9,10 +10,13 @@ module.exports = new PassportLocalStrategy(
passwordField: 'password', passwordField: 'password',
}, },
(username, password, done) => { (username, password, done) => {
if (closedRegistration) {
return done('Registration is disabled');
}
logger.verbose(`new channel signup request. user: ${username} pass: ${password} .`); logger.verbose(`new channel signup request. user: ${username} pass: ${password} .`);
let userInfo = {}; let userInfo = {};
// server-side validaton of inputs (username, password) // server-side validaton of inputs (username, password)
// create the channel and retrieve the metadata // create the channel and retrieve the metadata
return createChannel(`@${username}`) return createChannel(`@${username}`)
.then(tx => { .then(tx => {

View file

@ -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;