Merge pull request #170 from lbryio/authentication

Authentication
This commit is contained in:
Bill Bittner 2017-09-28 17:29:21 -07:00 committed by GitHub
commit d7da83a77a
59 changed files with 1776 additions and 766 deletions

1
.gitignore vendored
View file

@ -1,2 +1,3 @@
node_modules
.idea
config/config.json

33
auth/authentication.js Normal file
View file

@ -0,0 +1,33 @@
const db = require('../models');
const logger = require('winston');
module.exports = {
authenticateApiPublish (username, password) {
return new Promise((resolve, reject) => {
if (username === 'none') {
resolve(true);
return;
}
db.User
.findOne({where: {userName: username}})
.then(user => {
if (!user) {
logger.debug('no user found');
resolve(false);
return;
}
if (!user.validPassword(password, user.password)) {
logger.debug('incorrect password');
resolve(false);
return;
}
logger.debug('user found:', user.dataValues);
resolve(true);
})
.catch(error => {
logger.error(error);
reject();
});
});
},
};

View file

@ -1,16 +1,20 @@
{
"WalletConfig": {
"LbryClaimAddress": "none"
"LbryClaimAddress": null,
"DefaultChannel": null
},
"AnalyticsConfig":{
"GoogleId": "none"
"GoogleId": null
},
"Database": {
"Database": "lbry",
"Username": "none",
"Password": "none"
"Username": null,
"Password": null
},
"Logging": {
"LogLevel": "none"
"LogLevel": null,
"SlackWebHook": null,
"SlackErrorChannel": null,
"SlackInfoChannel": null
}
}

View file

@ -1,13 +1,10 @@
{
"WalletConfig": {
"LbryClaimAddress": "none"
"DefaultChannel": "@speechDev"
},
"AnalyticsConfig":{
"GoogleId": "UA-100747990-1"
},
"Database": {
"MySqlConnectionUri": "none"
},
"Logging": {
"LogLevel": "silly",
"SlackErrorChannel": "#staging_speech-errors",

View file

@ -12,10 +12,6 @@ module.exports = (winston, logLevel) => {
],
});
// winston.on('error', (err) => {
// console.log('unhandled exception in winston >> ', err);
// });
winston.error('Level 0');
winston.warn('Level 1');
winston.info('Level 2');

View file

@ -1,13 +1,10 @@
{
"WalletConfig": {
"LbryClaimAddress": "none"
"DefaultChannel": "@speech"
},
"AnalyticsConfig":{
"GoogleId": "UA-60403362-3"
},
"Database": {
"MySqlConnectionUri": "none"
},
"Logging": {
"LogLevel": "verbose",
"SlackErrorChannel": "#speech-errors",

View file

@ -5,23 +5,28 @@ const SLACK_INFO_CHANNEL = config.get('Logging.SlackInfoChannel');
const winstonSlackWebHook = require('winston-slack-webhook').SlackWebHook;
module.exports = (winston) => {
// add a transport for errors
winston.add(winstonSlackWebHook, {
name : 'slack-errors-transport',
level : 'error',
webhookUrl: SLACK_WEB_HOOK,
channel : SLACK_ERROR_CHANNEL,
username : 'spee.ch',
iconEmoji : ':face_with_head_bandage:',
});
winston.add(winstonSlackWebHook, {
name : 'slack-info-transport',
level : 'info',
webhookUrl: SLACK_WEB_HOOK,
channel : SLACK_INFO_CHANNEL,
username : 'spee.ch',
iconEmoji : ':nerd_face:',
});
// send test message
winston.error('Testing slack logging... slack logging is online.');
if (SLACK_WEB_HOOK) {
// add a transport for errors to slack
winston.add(winstonSlackWebHook, {
name : 'slack-errors-transport',
level : 'error',
webhookUrl: SLACK_WEB_HOOK,
channel : SLACK_ERROR_CHANNEL,
username : 'spee.ch',
iconEmoji : ':face_with_head_bandage:',
});
winston.add(winstonSlackWebHook, {
name : 'slack-info-transport',
level : 'info',
webhookUrl: SLACK_WEB_HOOK,
channel : SLACK_INFO_CHANNEL,
username : 'spee.ch',
iconEmoji : ':nerd_face:',
});
// send test message
winston.error('Slack error logging is online.');
winston.info('Slack info logging is online.');
} else {
winston.error('Slack logging is not enabled because no SLACK_WEB_HOOK env var provided.');
}
};

View file

@ -8,22 +8,24 @@ module.exports = {
return new Promise((resolve, reject) => {
let publishResults = {};
// 1. make sure the name is available
publishHelpers.checkNameAvailability(publishParams.name)
publishHelpers.checkClaimNameAvailability(publishParams.name)
// 2. publish the file
.then(result => {
if (result === true) {
return lbryApi.publishClaim(publishParams);
} else {
return new Error('That name has already been claimed by spee.ch. Please choose a new claim name.');
return new Error('That name is already in use by spee.ch.');
}
})
// 3. upsert File record (update is in case the claim has been published before by this daemon)
.then(result => {
let fileRecord;
let upsertCriteria;
publishResults = result;
logger.info(`Successfully published ${fileName}`, publishResults);
fileRecord = {
.then(tx => {
logger.info(`Successfully published ${fileName}`, tx);
publishResults = tx;
return db.Channel.findOne({where: {channelName: publishParams.channel_name}});
})
.then(user => {
if (user) { logger.debug('successfully found user in User table') } else { logger.error('user for publish not found in User table') };
const fileRecord = {
name : publishParams.name,
claimId : publishResults.claim_id,
title : publishParams.metadata.title,
@ -36,14 +38,32 @@ module.exports = {
fileType,
nsfw : publishParams.metadata.nsfw,
};
upsertCriteria = {
const claimRecord = {
name : publishParams.name,
claimId : publishResults.claim_id,
title : publishParams.metadata.title,
description : publishParams.metadata.description,
address : publishParams.claim_address,
outpoint : `${publishResults.txid}:${publishResults.nout}`,
height : 0,
contentType : fileType,
nsfw : publishParams.metadata.nsfw,
certificateId: user.channelClaimId,
amount : publishParams.bid,
};
const upsertCriteria = {
name : publishParams.name,
claimId: publishResults.claim_id,
};
return Promise.all([db.upsert(db.File, fileRecord, upsertCriteria, 'File'), db.upsert(db.Claim, fileRecord, upsertCriteria, 'Claim')]);
// create the records
return Promise.all([db.upsert(db.File, fileRecord, upsertCriteria, 'File'), db.upsert(db.Claim, claimRecord, upsertCriteria, 'Claim')]);
})
.then(([file, claim]) => {
logger.debug('File and Claim records successfully created');
return Promise.all([file.setClaim(claim), claim.setFile(file)]);
})
.then(() => {
logger.debug('File and Claim records successfully created');
logger.debug('File and Claim records successfully associated');
resolve(publishResults); // resolve the promise with the result from lbryApi.publishClaim;
})
.catch(error => {

View file

@ -97,7 +97,7 @@ function getAssetByLongClaimId (fullClaimId, name) {
}
function chooseThumbnail (claimInfo, defaultThumbnail) {
if (!claimInfo.thumbnail || claimInfo.thumbnail === '') {
if (!claimInfo.thumbnail || claimInfo.thumbnail.trim() === '') {
return defaultThumbnail;
}
return claimInfo.thumbnail;
@ -150,7 +150,7 @@ module.exports = {
// 2. get all claims for that channel
.then(result => {
longChannelId = result;
return db.getShortChannelIdFromLongChannelId(channelName, longChannelId);
return db.getShortChannelIdFromLongChannelId(longChannelId, channelName);
})
// 3. get all Claim records for this channel
.then(result => {
@ -168,7 +168,12 @@ module.exports = {
element['thumbnail'] = chooseThumbnail(element, DEFAULT_THUMBNAIL);
});
}
return resolve(allChannelClaims);
return resolve({
channelName,
longChannelId,
shortChannelId,
claims: allChannelClaims,
});
})
.catch(error => {
reject(error);

21
helpers/configVarCheck.js Normal file
View file

@ -0,0 +1,21 @@
const config = require('config');
const logger = require('winston');
const fs = require('fs');
module.exports = function () {
// get the config file
const defaultConfigFile = JSON.parse(fs.readFileSync('./config/default.json'));
for (let configCategoryKey in defaultConfigFile) {
if (defaultConfigFile.hasOwnProperty(configCategoryKey)) {
// get the final variables for each config category
const configVariables = config.get(configCategoryKey);
for (let configVarKey in configVariables) {
if (configVariables.hasOwnProperty(configVarKey)) {
// print each variable
logger.debug(`CONFIG CHECK: ${configCategoryKey}.${configVarKey} === ${configVariables[configVarKey]}`);
}
}
}
}
};

View file

@ -0,0 +1,96 @@
const Handlebars = require('handlebars');
const config = require('config');
module.exports = {
// define any extra helpers you may need
googleAnalytics () {
const googleApiKey = config.get('AnalyticsConfig.GoogleId');
return new Handlebars.SafeString(
`<script>
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
ga('create', '${googleApiKey}', 'auto');
ga('send', 'pageview');
</script>`
);
},
addOpenGraph (title, mimeType, showUrl, source, description, thumbnail) {
let basicTags = `<meta property="og:title" content="${title}">
<meta property="og:url" content="${showUrl}" >
<meta property="og:site_name" content="Spee.ch" >
<meta property="og:description" content="${description}">`;
if (mimeType === 'video/mp4') {
return new Handlebars.SafeString(
`${basicTags} <meta property="og:image" content="${thumbnail}" >
<meta property="og:image:type" content="image/png" >
<meta property="og:image:width" content="600" >
<meta property="og:image:height" content="315" >
<meta property="og:type" content="video" >
<meta property="og:video" content="${source}" >
<meta property="og:video:secure_url" content="${source}" >
<meta property="og:video:type" content="${mimeType}" >`
);
} else if (mimeType === 'image/gif') {
return new Handlebars.SafeString(
`${basicTags} <meta property="og:image" content="${source}" >
<meta property="og:image:type" content="${mimeType}" >
<meta property="og:image:width" content="600" >
<meta property="og:image:height" content="315" >
<meta property="og:type" content="video.other" >`
);
} else {
return new Handlebars.SafeString(
`${basicTags} <meta property="og:image" content="${source}" >
<meta property="og:image:type" content="${mimeType}" >
<meta property="og:image:width" content="600" >
<meta property="og:image:height" content="315" >
<meta property="og:type" content="article" >`
);
}
},
addTwitterCard (mimeType, source, embedUrl, directFileUrl) {
let basicTwitterTags = `<meta name="twitter:site" content="@speechch" >`;
if (mimeType === 'video/mp4') {
return new Handlebars.SafeString(
`${basicTwitterTags} <meta name="twitter:card" content="player" >
<meta name="twitter:player" content="${embedUrl}>
<meta name="twitter:player:width" content="600" >
<meta name="twitter:text:player_width" content="600" >
<meta name="twitter:player:height" content="337" >
<meta name="twitter:player:stream" content="${directFileUrl}" >
<meta name="twitter:player:stream:content_type" content="video/mp4" >
`
);
} else {
return new Handlebars.SafeString(
`${basicTwitterTags} <meta name="twitter:card" content="summary_large_image" >`
);
}
},
ifConditional (varOne, operator, varTwo, options) {
switch (operator) {
case '===':
return (varOne === varTwo) ? options.fn(this) : options.inverse(this);
case '!==':
return (varOne !== varTwo) ? options.fn(this) : options.inverse(this);
case '<':
return (varOne < varTwo) ? options.fn(this) : options.inverse(this);
case '<=':
return (varOne <= varTwo) ? options.fn(this) : options.inverse(this);
case '>':
return (varOne > varTwo) ? options.fn(this) : options.inverse(this);
case '>=':
return (varOne >= varTwo) ? options.fn(this) : options.inverse(this);
case '&&':
return (varOne && varTwo) ? options.fn(this) : options.inverse(this);
case '||':
return (varOne || varTwo) ? options.fn(this) : options.inverse(this);
case 'mod3':
return ((parseInt(varOne) % 3) === 0) ? options.fn(this) : options.inverse(this);
default:
return options.inverse(this);
}
},
};

View file

@ -1,23 +1,23 @@
const axios = require('axios');
const logger = require('winston');
function handleResponse ({ data }, resolve, reject) {
logger.debug('handling lbry api response');
if (data.result) {
// check for an error
if (data.result.error) {
reject(data.result.error);
return;
};
logger.debug('data.result', data.result);
resolve(data.result);
return;
}
// fallback in case the just timed out
reject(JSON.stringify(data));
}
module.exports = {
getWalletList () {
logger.debug('lbryApi >> getting wallet list');
return new Promise((resolve, reject) => {
axios
.post('http://localhost:5279/lbryapi', {
method: 'wallet_list',
})
.then(response => {
const result = response.data.result;
resolve(result);
})
.catch(error => {
reject(error);
});
});
},
publishClaim (publishParams) {
logger.debug(`lbryApi >> Publishing claim to "${publishParams.name}"`);
return new Promise((resolve, reject) => {
@ -27,8 +27,7 @@ module.exports = {
params: publishParams,
})
.then(response => {
const result = response.data.result;
resolve(result);
handleResponse(response, resolve, reject);
})
.catch(error => {
reject(error);
@ -43,18 +42,8 @@ module.exports = {
method: 'get',
params: { uri, timeout: 20 },
})
.then(({ data }) => {
// check to make sure the daemon didn't just time out
if (!data.result) {
reject(JSON.stringify(data));
}
if (data.result.error) {
reject(data.result.error);
}
/*
note: put in a check to make sure we do not resolve until the download is actually complete (response.data.completed === true)?
*/
resolve(data.result);
.then(response => {
handleResponse(response, resolve, reject);
})
.catch(error => {
reject(error);
@ -69,8 +58,8 @@ module.exports = {
method: 'claim_list',
params: { name: claimName },
})
.then(({ data }) => {
resolve(data.result);
.then(response => {
handleResponse(response, resolve, reject);
})
.catch(error => {
reject(error);
@ -94,7 +83,6 @@ module.exports = {
}
})
.catch(error => {
console.log('error with resolve', error);
reject(error);
});
});
@ -110,7 +98,6 @@ module.exports = {
if (data.result) {
resolve(data.result.download_directory);
} else {
// reject(new Error('Successfully connected to lbry daemon, but unable to retrieve the download directory.'));
return new Error('Successfully connected to lbry daemon, but unable to retrieve the download directory.');
}
})
@ -120,4 +107,22 @@ module.exports = {
});
});
},
createChannel (name) {
return new Promise((resolve, reject) => {
axios
.post('http://localhost:5279/lbryapi', {
method: 'channel_new',
params: {
channel_name: name,
amount : 0.1,
},
})
.then(response => {
handleResponse(response, resolve, reject);
})
.catch(error => {
reject(error);
});
});
},
};

View file

@ -2,7 +2,6 @@ const logger = require('winston');
const config = require('config');
const fs = require('fs');
const db = require('../models');
const { getWalletList } = require('./lbryApi.js');
module.exports = {
validateFile (file, name, license, nsfw) {
@ -29,10 +28,10 @@ module.exports = {
// validate claim name
const invalidCharacters = /[^A-Za-z0-9,-]/.exec(name);
if (invalidCharacters) {
throw new Error('The claim name you provided is not allowed. Only the following characters are allowed: A-Z, a-z, 0-9, and "-"');
throw new Error('The url name you provided is not allowed. Only the following characters are allowed: A-Z, a-z, 0-9, and "-"');
}
// validate license
if ((license.indexOf('Public Domain') === -1) && (license.indexOf('Creative Commons') === -1) && (license.indecOf('CC Attribution-NonCommercial 4.0 International') === -1)) {
if ((license.indexOf('Public Domain') === -1) && (license.indexOf('Creative Commons') === -1)) {
throw new Error('Only posts with a "Public Domain" license, or one of the Creative Commons licenses are eligible for publishing through spee.ch');
}
switch (nsfw) {
@ -51,28 +50,27 @@ module.exports = {
throw new Error('NSFW value was not accepted. NSFW must be set to either true, false, "on", or "off"');
}
},
createPublishParams (name, filePath, title, description, license, nsfw) {
createPublishParams (name, filePath, title, description, license, nsfw, channel) {
logger.debug(`Creating Publish Parameters for "${name}"`);
const claimAddress = config.get('WalletConfig.LbryClaimAddress');
const defaultChannel = config.get('WalletConfig.DefaultChannel');
// filter nsfw and ensure it is a boolean
if (nsfw === false) {
nsfw = false;
} else if (nsfw.toLowerCase === 'false') {
nsfw = false;
} else if (nsfw.toLowerCase === 'off') {
nsfw = false;
} else if (typeof nsfw === 'string') {
if (nsfw.toLowerCase === 'false' || nsfw.toLowerCase === 'off' || nsfw === '0') {
nsfw = false;
}
} else if (nsfw === 0) {
nsfw = false;
} else if (nsfw === '0') {
nsfw = false;
} else {
nsfw = true;
}
// provide defaults for title & description
if (title === '' || title === null) {
if (title === null || title === '') {
title = name;
}
if (description === '' || title === null) {
if (description === null || description.trim() === '') {
description = `${name} published via spee.ch`;
}
// create the publish params
@ -89,8 +87,14 @@ module.exports = {
nsfw,
},
claim_address: claimAddress,
// change_address: changeAddress,
};
// add channel if applicable
if (channel !== 'none') {
publishParams['channel_name'] = channel;
} else {
publishParams['channel_name'] = defaultChannel;
}
logger.debug('publishParams:', publishParams);
return publishParams;
},
@ -100,27 +104,23 @@ module.exports = {
logger.debug(`successfully deleted ${filePath}`);
});
},
checkNameAvailability (name) {
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) {
// filter out any results that were not published from a spee.ch wallet address
getWalletList()
.then((walletList) => {
const filteredResult = result.filter((claim) => {
return walletList.includes(claim.address);
});
if (filteredResult.length >= 1) {
resolve(false);
} else {
resolve(true);
}
})
.catch((error) => {
reject(error);
const claimAddress = config.get('WalletConfig.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);
}
@ -130,4 +130,19 @@ module.exports = {
});
});
},
checkChannelAvailability (name) {
return new Promise((resolve, reject) => {
// find any records where the name is used
db.Channel.findAll({ where: { channelName: name } })
.then(result => {
if (result.length >= 1) {
return resolve(false);
}
resolve(true);
})
.catch(error => {
reject(error);
});
});
},
};

View file

@ -0,0 +1,79 @@
module.exports = {
up: (queryInterface, Sequelize) => {
// logic for transforming into the new state
const p1 = queryInterface.removeColumn(
'Certificate',
'UserId'
);
const p2 = queryInterface.addColumn(
'Certificate',
'ChannelId',
{
type : Sequelize.INTEGER,
allowNull: true,
}
);
const p3 = queryInterface.addConstraint(
'Certificate',
['ChannelId'],
{
type : 'FOREIGN KEY',
name : 'Certificate_ibfk_1',
references: {
table: 'Channel',
field: 'id',
},
onUpdate: 'cascade',
onDelete: 'cascade',
}
);
const p4 = queryInterface.changeColumn(
'Claim',
'FileId',
{
type : Sequelize.INTEGER,
allowNull: true,
}
);
const p5 = queryInterface.addConstraint(
'Claim',
['FileId'],
{
type : 'FOREIGN KEY',
name : 'Claim_ibfk_1',
references: {
table: 'File',
field: 'id',
},
onUpdate: 'cascade',
onDelete: 'cascade',
}
);
const p6 = queryInterface.removeColumn(
'File',
'UserId'
);
return Promise.all([p1, p2, p3, p4, p5, p6]);
},
down: (queryInterface, Sequelize) => {
// logic for reverting the changes
const p1 = queryInterface.addColumn(
'Certificate',
'UserId',
{
type : Sequelize.INTEGER,
allowNull: true,
}
);
const p2 = queryInterface.addColumn(
'File',
'UserId',
{
type : Sequelize.INTEGER,
allowNull: true,
}
);
return Promise.all([p1, p2]);
},
};

View file

@ -0,0 +1,46 @@
module.exports = {
up: (queryInterface, Sequelize) => {
// logic for transforming into the new state
const p1 = queryInterface.addColumn(
'User',
'userName',
{
type : Sequelize.STRING,
allowNull: true,
}
);
const p2 = queryInterface.removeColumn(
'User',
'channelName'
);
const p3 = queryInterface.removeColumn(
'User',
'channelClaimId'
);
return Promise.all([p1, p2, p3]);
},
down: (queryInterface, Sequelize) => {
// logic for reverting the changes
const p1 = queryInterface.removeColumn(
'User',
'userName'
);
const p2 = queryInterface.addColumn(
'User',
'channelName',
{
type : Sequelize.STRING,
allowNull: true,
}
);
const p3 = queryInterface.addColumn(
'User',
'channelClaimId',
{
type : Sequelize.STRING,
allowNull: true,
}
);
return Promise.all([p1, p2, p3]);
},
};

View file

@ -87,5 +87,15 @@ module.exports = (sequelize, { STRING, BOOLEAN, INTEGER, TEXT, ARRAY, DECIMAL, D
freezeTableName: true,
}
);
Certificate.associate = db => {
Certificate.belongsTo(db.Channel, {
onDelete : 'cascade',
foreignKey: {
allowNull: true,
},
});
};
return Certificate;
};

25
models/channel.js Normal file
View file

@ -0,0 +1,25 @@
module.exports = (sequelize, { STRING }) => {
const Channel = sequelize.define(
'Channel',
{
channelName: {
type : STRING,
allowNull: false,
},
channelClaimId: {
type : STRING,
allowNull: false,
},
},
{
freezeTableName: true,
}
);
Channel.associate = db => {
Channel.belongsTo(db.User);
Channel.hasOne(db.Certificate);
};
return Channel;
};

View file

@ -140,5 +140,14 @@ module.exports = (sequelize, { STRING, BOOLEAN, INTEGER, TEXT, ARRAY, DECIMAL, D
}
);
Claim.associate = db => {
Claim.belongsTo(db.File, {
onDelete : 'cascade',
foreignKey: {
allowNull: true,
},
});
};
return Claim;
};

View file

@ -52,6 +52,7 @@ module.exports = (sequelize, { STRING, BOOLEAN, INTEGER }) => {
File.associate = db => {
File.hasMany(db.Request);
File.hasOne(db.Claim);
};
return File;

View file

@ -66,7 +66,7 @@ function getLongClaimIdFromShortClaimId (name, shortId) {
function getTopFreeClaimIdByClaimName (name) {
return new Promise((resolve, reject) => {
db
.sequelize.query(`SELECT claimId FROM Claim WHERE name = '${name}' ORDER BY amount DESC, height ASC LIMIT 1`, { type: db.sequelize.QueryTypes.SELECT })
.sequelize.query(`SELECT claimId FROM Claim WHERE name = '${name}' ORDER BY effectiveAmount DESC, height ASC LIMIT 1`, { type: db.sequelize.QueryTypes.SELECT })
.then(result => {
switch (result.length) {
case 0:
@ -190,9 +190,9 @@ db['getShortClaimIdFromLongClaimId'] = (claimId, claimName) => {
});
};
db['getShortChannelIdFromLongChannelId'] = (channelName, longChannelId) => {
db['getShortChannelIdFromLongChannelId'] = (longChannelId, channelName) => {
return new Promise((resolve, reject) => {
logger.debug('finding short channel id');
logger.debug(`finding short channel id for ${longChannelId} ${channelName}`);
db
.sequelize.query(`SELECT claimId, height FROM Certificate WHERE name = '${channelName}' ORDER BY height;`, { type: db.sequelize.QueryTypes.SELECT })
.then(result => {

29
models/user.js Normal file
View file

@ -0,0 +1,29 @@
module.exports = (sequelize, { STRING }) => {
const User = sequelize.define(
'User',
{
userName: {
type : STRING,
allowNull: false,
},
password: {
type : STRING,
allowNull: false,
},
},
{
freezeTableName: true,
}
);
User.associate = db => {
User.hasOne(db.Channel);
};
User.prototype.validPassword = (givenpassword, thispassword) => {
console.log(`${givenpassword} === ${thispassword}`);
return (givenpassword === thispassword);
};
return User;
};

View file

@ -32,9 +32,14 @@
"connect-multiparty": "^2.0.0",
"express": "^4.15.2",
"express-handlebars": "^3.0.0",
"express-session": "^1.15.5",
"helmet": "^3.8.1",
"mysql2": "^1.3.5",
"nodemon": "^1.11.0",
"passport": "^0.4.0",
"passport-local": "^1.0.0",
"sequelize": "^4.1.0",
"sequelize-cli": "^3.0.0-3",
"sleep": "^5.1.1",
"socket.io": "^2.0.1",
"socketio-file-upload": "^0.6.0",

34
passport/local-login.js Normal file
View file

@ -0,0 +1,34 @@
const PassportLocalStrategy = require('passport-local').Strategy;
const db = require('../models');
const logger = require('winston');
module.exports = new PassportLocalStrategy(
{
usernameField : 'username', // username key in the request body
passwordField : 'password', // password key in the request body
session : false,
passReqToCallback: true,
},
(req, username, password, done) => {
logger.debug(`verifying loggin attempt ${username} ${password}`);
return db.User
.findOne({where: {userName: username}})
.then(user => {
if (!user) {
logger.debug('no user found');
return done(null, false, {message: 'Incorrect username or password.'});
}
if (!user.validPassword(password, user.password)) {
logger.debug('incorrect password');
return done(null, false, {message: 'Incorrect username or password.'});
}
logger.debug('user found:', user.dataValues);
return user.getChannel().then(channel => {
return done(null, user);
});
})
.catch(error => {
return done(error);
});
}
);

60
passport/local-signup.js Normal file
View file

@ -0,0 +1,60 @@
const db = require('../models');
const PassportLocalStrategy = require('passport-local').Strategy;
const lbryApi = require('../helpers/lbryApi.js');
const logger = require('winston');
module.exports = new PassportLocalStrategy(
{
usernameField : 'username', // sets the custom name of parameters in the POST body message
passwordField : 'password', // sets the custom name of parameters in the POST body message
session : false, // set to false because we will use token approach to auth
passReqToCallback: true, // we want to be able to read the post body message parameters in the callback
},
(req, username, password, done) => {
logger.debug(`new channel signup request: ${username} ${password}`);
let user;
// server-side validaton of inputs (username, password)
// create the channel and retrieve the metadata
return lbryApi.createChannel(`@${username}`)
.then(tx => {
// create user record
const userData = {
userName: username,
password: password,
};
logger.debug('userData >', userData);
// create user record
const channelData = {
channelName : `@${username}`,
channelClaimId: tx.claim_id,
};
logger.debug('channelData >', channelData);
// create certificate record
const certificateData = {
claimId: tx.claim_id,
name : `@${username}`,
// address,
};
logger.debug('certificateData >', certificateData);
// save user and certificate to db
return Promise.all([db.User.create(userData), db.Channel.create(channelData), db.Certificate.create(certificateData)]);
})
.then(([newUser, newChannel, newCertificate]) => {
user = newUser;
logger.debug('user and certificate successfully created');
logger.debug('user result >', newUser.dataValues);
logger.debug('user result >', newChannel.dataValues);
logger.debug('certificate result >', newCertificate.dataValues);
// associate the instances
return Promise.all([newCertificate.setChannel(newChannel), newChannel.setUser(newUser)]);
}).then(() => {
logger.debug('user and certificate successfully associated');
return done(null, user);
})
.catch(error => {
logger.error('signup error', error);
return done(error);
});
}
);

276
public/assets/css/BEM.css Normal file
View file

@ -0,0 +1,276 @@
/* GENERAL */
/* TEXT */
body, button, input, textarea, label, select, option {
font-family: serif;
}
p {
padding-left: 0.3em;
}
.center-text {
text-align: center;
}
.url-text {
margin:0px;
padding:0px;
}
/* HEADERS */
h1 {
font-size: x-large;
}
h2 {
font-size: medium;
margin-top: 1em;
border-top: 1px #999 solid;
background-color: lightgray;
padding: 6px;
}
h3 {
color: black;;
}
.h3--secondary {
color: lightgray;
}
h4 {
padding: 3px;
}
/* CONTAINERS */
.wrapper {
margin-left: 20%;
width:60%;
}
.full {
clear: both;
}
.main {
float: left;
width: 65%;
}
.panel {
overflow: auto;
word-wrap: break-word;
}
.sidebar {
float: right;
width: 33%;
}
footer {
display: inline-block;
width: 100%;
margin-bottom: 2px;
padding-bottom: 2px;
border-bottom: 1px lightgrey solid;
margin-top: 2px;
padding-top: 2px;
border-top: 1px lightgrey solid;
text-align: center;
color: grey;
}
/* COLUMNS AND ROWS */
.col-left, .col-right {
overflow: auto;
margin: 0px;
width: 48%;
}
.col-left {
padding: 5px 10px 5px 0px;
float: left;
}
.col-right {
padding: 5px 0px 5px 10px;
float: right;
}
.row {
padding: 1em 2% 1em 2%;
margin: 0px;
}
.row--wide {
padding-right: 0px;
padding-left: 0px;
}
.row--thin {
padding-top: 0.5em;
padding-bottom: 0.5em;
}
.top-bar {
margin: 2em 0px 2px 0px;
padding: 0px 0px 2px 0px;
border-bottom: 1px lightgrey solid;
overflow: auto;
text-align: right;
vertical-align: text-bottom;
}
.column {
display: inline-block;
padding: 0px;
margin: 0px;
}
.column--1 {
width: 8%;
}
.column--2 {
width: 16%;
}
.column--3 {
width: 24%;
}
.column--4 {
width: 32%;
}
.column--5 {
width: 40%;
}
.column--6 {
width: 48%;
}
.column--7 {
width: 56%;
}
.column--8 {
width: 64%;
}
.column--9 {
width: 72%;
}
.column--10 {
width: 80%;
}
.column--11 {
width: 88%;
}
.column--12 {
width: 96%;
}
/* LINKS */
a, a:visited {
color: blue;
text-decoration: none;
}
/* ERROR MESSAGES */
.info-message {
font-weight: bold;
}
.info-message--success {
color: green;
}
.info-message--failure {
color: red;
}
/* INPUT FIELDS */
input:-webkit-autofill {
-webkit-box-shadow: 0 0 0px 1000px white inset;
}
.label, .input-text, .select, .textarea {
font-size: medium;
padding: 0.3em;
outline: none;
border: 0px;
background-color: white;
}
.input-text--primary, .select--primary, .textarea--primary {
border-bottom: 1px solid blue;
}
.input-text--primary:focus, .select--primary:focus, .textarea--primary:focus {
border-bottom: 1px solid grey;
}
.input-checkbox, .input-textarea {
border: 1px solid grey;
}
/* BUTTONS */
button {
border: 1px solid black;
padding: 0.5em;
margin: 0.5em 0.3em 0.5em 0.3em;
color: black;
background-color: white;
}
button:hover {
border: 1px solid blue;
color: white;
background-color: blue;
}
button:active{
border: 1px solid blue;
color: white;
background-color: white;
}
/* TABLES */
table {
width: 100%;
text-align: left;
}
/* other */
.stop-float {
clear: both;
}
.toggle-link {
float: right;
}
.wrap-words {
word-wrap: break-word;
}

View file

@ -38,30 +38,6 @@
margin-bottom: 1em;
}
#claim-name-input-area {
border-bottom: 1px blue solid;
float: left;
margin-bottom: 1em;
font-weight: bold;
}
.publish-input, #publish-license {
border: 1px solid lightgrey;
}
.publish-input {
padding: 1%;
width: 90%
}
#claim-name-input {
border: 0px;
}
#claim-name-input:focus {
outline: none
}
/* show routes */
.show-asset {
width: 100%;
@ -116,8 +92,6 @@ button.copy-button {
/* learn more */
.learn-more {
text-align: center;
margin-top: 2px;
padding-top: 2px;
border-top: 1px solid lightgrey;
}
@ -208,42 +182,4 @@ button.copy-button {
word-wrap: break-word;
}
@media (max-width: 750px) {
.all-claims-asset {
width:30%;
}
.all-claims-details {
font-size: small;
}
.show-asset-lite {
width: 100%;
}
.top-bar-tagline {
clear: both;
text-align: left;
width: 100%;
}
}
@media (max-width: 475px) {
div#publish-active-area {
margin-left: 2em;
margin-right: 2em;
}
.all-claims-asset {
width:50%;
}
.top-bar-right {
display: none;
}
}

View file

@ -1,185 +0,0 @@
body, button, input, textarea, label, select, option {
font-family: serif;
}
/* Containters */
.wrapper {
margin-left: 20%;
width:60%;
}
.top-bar {
width: 100%;
margin-bottom: 2px;
padding-bottom: 2px;
border-bottom: 1px lightgrey solid;
margin-top: 2em;
overflow: auto;
text-align: right;
display: inline-block;
vertical-align: text-bottom;
}
.full {
clear: both;
}
.main {
float: left;
width: 65%;
}
.sidebar {
float: right;
width: 33%;
}
footer {
display: inline-block;
width: 100%;
margin-bottom: 2px;
padding-bottom: 2px;
border-bottom: 1px lightgrey solid;
margin-top: 2px;
padding-top: 2px;
border-top: 1px lightgrey solid;
text-align: center;
color: grey;
}
/* panels */
.panel {
overflow: auto;
word-wrap: break-word;
}
.col-left, .col-right {
overflow: auto;
margin: 0px;
width: 48%;
}
.col-left {
padding: 5px 10px 5px 0px;
float: left;
}
.col-right {
padding: 5px 0px 5px 10px;
float: right;
}
/* text */
a, a:visited {
color: blue;
text-decoration: none;
}
h1 {
font-size: x-large;
}
h2 {
font-size: medium;
margin-top: 1em;
border-top: 1px #999 solid;
background-color: lightgray;
padding: 6px;
}
.subheader {
margin-top: 0px;
}
h4 {
padding: 3px;
}
.center-text {
text-align: center;
}
/* other */
input {
padding: 0.3em;
}
table {
width: 100%;
text-align: left;
}
.stop-float {
clear: both;
}
.toggle-link {
float: right;
}
.wrap-words {
word-wrap: break-word;
}
.input-error {
font-weight: bold;
color: red;
font-size: small;
}
@media (max-width: 1250px) {
.wrapper {
margin-left: 10%;
width:80%;
}
}
@media (max-width: 1000px) {
.wrapper {
margin-left: 10%;
width:80%;
}
.main {
float: none;
width: 100%;
margin-right: 0px;
padding-right: 0px;
border-right: 0px;
margin-bottom: 5px;
}
.sidebar {
border-top: 1px solid lightgray;
float: none;
width: 100%;
}
}
@media (max-width: 750px ) {
.col-left, .col-right {
float: none;
margin: 0px;
padding: 0px;
width: 100%;
}
.col-right {
padding-top: 20px;
}
}
@media (max-width: 475px) {
.wrapper {
margin: 0px;
width: 100%;
}
}

View file

@ -0,0 +1,59 @@
@media (max-width: 1250px) {
.wrapper {
margin-left: 10%;
width:80%;
}
}
@media (max-width: 1000px) {
.wrapper {
margin-left: 10%;
width:80%;
}
.main {
float: none;
width: 100%;
margin-right: 0px;
padding-right: 0px;
border-right: 0px;
margin-bottom: 5px;
}
.sidebar {
border-top: 1px solid lightgray;
float: none;
width: 100%;
}
}
@media (max-width: 750px ) {
.col-left, .col-right {
float: none;
margin: 0px;
padding: 0px;
width: 100%;
}
.col-right {
padding-top: 20px;
}
.all-claims-asset {
width:30%;
}
.all-claims-details {
font-size: small;
}
.show-asset-lite {
width: 100%;
}
.top-bar-tagline {
clear: both;
text-align: left;
width: 100%;
}
}

View file

View file

@ -0,0 +1,4 @@
function sendAuthRequest (channelName, password, url) {
const params = `username=${channelName}&password=${password}`;
return postRequest(url, params);
}

View file

@ -1,3 +1,46 @@
function getRequest (url) {
console.log('making GET request to', url)
return new Promise((resolve, reject) => {
let xhttp = new XMLHttpRequest();
xhttp.open('GET', url, true);
xhttp.responseType = 'json';
xhttp.onreadystatechange = () => {
if (xhttp.readyState == 4 ) {
console.log(xhttp);
if ( xhttp.status == 200) {
console.log('response:', xhttp.response);
resolve(xhttp.response);
} else {
reject('request failed with status:' + xhttp.status);
};
}
};
xhttp.send();
})
}
function postRequest (url, params) {
console.log('making POST request to', url)
return new Promise((resolve, reject) => {
let xhttp = new XMLHttpRequest();
xhttp.open('POST', url, true);
xhttp.responseType = 'json';
xhttp.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
xhttp.onreadystatechange = () => {
if (xhttp.readyState == 4 ) {
console.log(xhttp);
if ( xhttp.status == 200) {
console.log('response:', xhttp.response);
resolve(xhttp.response);
} else {
reject('request failed with status:' + xhttp.status);
};
}
};
xhttp.send(params);
})
}
function toggleSection(event){
event.preventDefault();
@ -5,14 +48,16 @@ function toggleSection(event){
var status = dataSet.open;
var masterElement = document.getElementById(event.target.id||event.srcElement.id);
var slaveElement = document.getElementById(dataSet.slaveelementid);
var closedLabel = dataSet.closedlabel;
var openLabel = dataSet.openlabel;
if (status === "false") {
slaveElement.hidden = false;
masterElement.innerText = "[close]";
masterElement.innerText = openLabel;
masterElement.dataset.open = "true";
} else {
slaveElement.hidden = true;
masterElement.innerText = "[open]";
masterElement.innerText = closedLabel;
masterElement.dataset.open = "false";
}
}
@ -35,38 +80,6 @@ function createProgressBar(element, size){
setInterval(addOne, 300);
}
function dataURItoBlob(dataURI) {
// convert base64/URLEncoded data component to raw binary data held in a string
var byteString;
if (dataURI.split(',')[0].indexOf('base64') >= 0)
byteString = atob(dataURI.split(',')[1]);
else
byteString = unescape(dataURI.split(',')[1]);
// separate out the mime component
var mimeString = dataURI.split(',')[0].split(':')[1].split(';')[0];
// write the bytes of the string to a typed array
var ia = new Uint8Array(byteString.length);
for (var i = 0; i < byteString.length; i++) {
ia[i] = byteString.charCodeAt(i);
}
return new Blob([ia], {type:mimeString});
}
function showError(elementId, errorMsg) {
var errorDisplay = document.getElementById(elementId);
errorDisplay.hidden = false;
errorDisplay.innerText = errorMsg;
}
function clearError(elementId) {
var errorDisplay = document.getElementById(elementId);
errorDisplay.hidden = true;
errorDisplay.innerText = '';
}
// Create new error objects, that prototypically inherit from the Error constructor
function FileError(message) {
this.name = 'FileError';
@ -83,3 +96,19 @@ function NameError(message) {
}
NameError.prototype = Object.create(Error.prototype);
NameError.prototype.constructor = NameError;
function ChannelNameError(message) {
this.name = 'ChannelNameError';
this.message = message || 'Default Message';
this.stack = (new Error()).stack;
}
ChannelNameError.prototype = Object.create(Error.prototype);
ChannelNameError.prototype.constructor = ChannelNameError;
function ChannelPasswordError(message) {
this.name = 'ChannelPasswordError';
this.message = message || 'Default Message';
this.stack = (new Error()).stack;
}
ChannelPasswordError.prototype = Object.create(Error.prototype);
ChannelPasswordError.prototype.constructor = ChannelPasswordError;

View file

@ -1,9 +1,38 @@
// update the publish status
function updatePublishStatus(msg){
document.getElementById('publish-status').innerHTML = msg;
/* drop zone functions */
function drop_handler(ev) {
ev.preventDefault();
// if dropped items aren't files, reject them
var dt = ev.dataTransfer;
if (dt.items) {
if (dt.items[0].kind == 'file') {
var droppedFile = dt.items[0].getAsFile();
previewAndStageFile(droppedFile);
}
}
}
/* publish helper functions */
function dragover_handler(ev) {
ev.preventDefault();
}
function dragend_handler(ev) {
var dt = ev.dataTransfer;
if (dt.items) {
for (var i = 0; i < dt.items.length; i++) {
dt.items.remove(i);
}
} else {
ev.dataTransfer.clearData();
}
}
/* publish functions */
// update the publish status
function updatePublishStatus(msg){
document.getElementById('publish-status').innerHTML = msg;
}
// When a file is selected for publish, validate that file and
// stage it so it will be ready when the publish button is clicked.
@ -19,7 +48,7 @@ function previewAndStageFile(selectedFile){
showError('input-error-file-selection', error.message);
return;
}
// set the image preview, if a preview was provided
// set the image preview, if an image was provided
if (selectedFile.type !== 'video/mp4') {
previewReader.readAsDataURL(selectedFile);
previewReader.onloadend = function () {
@ -40,49 +69,26 @@ function previewAndStageFile(selectedFile){
// Validate the publish submission and then trigger publishing.
function publishSelectedImage(event) {
event.preventDefault();
var name = document.getElementById('claim-name-input').value;
validateSubmission(stagedFiles, name)
.then(function() {
var claimName = document.getElementById('claim-name-input').value;
var channelName = document.getElementById('channel-name-select').value;
// prevent default so this script can handle submission
event.preventDefault();
// validate, submit, and handle response
validateFilePublishSubmission(stagedFiles, claimName, channelName)
.then(() => {
uploader.submitFiles(stagedFiles);
})
.catch(function(error) {
if (error.name === 'FileError'){
showError('input-error-file-selection', error.message);
.catch(error => {
if (error.name === 'FileError') {
showError(document.getElementById('input-error-file-selection'), error.message);
} else if (error.name === 'NameError') {
showError('input-error-claim-name', error.message);
showError(document.getElementById('input-error-claim-name'), error.message);
} else if (error.name === 'ChannelNameError'){
console.log(error);
showError(document.getElementById('input-error-channel-select'), error.message);
} else {
showError('input-error-publish-submit', error.message);
showError(document.getElementById('input-error-publish-submit'), error.message);
}
return;
})
};
/* drop zone functions */
function drop_handler(ev) {
ev.preventDefault();
// if dropped items aren't files, reject them
var dt = ev.dataTransfer;
if (dt.items) {
if (dt.items[0].kind == 'file') {
var droppedFile = dt.items[0].getAsFile();
previewAndStageFile(droppedFile);
}
}
}
function dragover_handler(ev) {
ev.preventDefault();
}
function dragend_handler(ev) {
var dt = ev.dataTransfer;
if (dt.items) {
for (var i = 0; i < dt.items.length; i++) {
dt.items.remove(i);
}
} else {
ev.dataTransfer.clearData();
}
}

View file

@ -27,95 +27,170 @@ function validateFile(file) {
throw new Error(file.type + ' is not a supported file type. Only, .jpeg, .png, .gif, and .mp4 files are currently supported.')
}
}
// validation function that checks to make sure the claim name is not already claimed
function isNameAvailable (name) {
return new Promise(function(resolve, reject) {
// make sure the claim name is still available
var xhttp;
xhttp = new XMLHttpRequest();
xhttp.open('GET', '/api/isClaimAvailable/' + name, true);
xhttp.responseType = 'json';
xhttp.onreadystatechange = function() {
if (this.readyState == 4 ) {
if ( this.status == 200) {
if (this.response == true) {
resolve();
} else {
reject( new NameError("That name has already been claimed by another user. Please choose a different name."));
}
} else {
reject("request to check claim name failed with status:" + this.status);
};
}
};
xhttp.send();
});
}
// validation function that checks to make sure the claim name is valid
function validateClaimName (name) {
// ensure a name was entered
if (name.length < 1) {
throw new NameError("You must enter a name for your claim");
throw new NameError("You must enter a name for your url");
}
// validate the characters in the 'name' field
const invalidCharacters = /[^A-Za-z0-9,-]/g.exec(name);
if (invalidCharacters) {
throw new NameError('"' + invalidCharacters + '" characters are not allowed in the title.');
throw new NameError('"' + invalidCharacters + '" characters are not allowed in the url.');
}
}
function validateChannelName (name) {
name = name.substring(name.indexOf('@') + 1);
// ensure a name was entered
if (name.length < 1) {
throw new ChannelNameError("You must enter a name for your channel");
}
// validate the characters in the 'name' field
const invalidCharacters = /[^A-Za-z0-9,-,@]/g.exec(name);
if (invalidCharacters) {
throw new ChannelNameError('"' + invalidCharacters + '" characters are not allowed in the channel name.');
}
}
function validatePassword (password) {
if (password.length < 1) {
throw new ChannelPasswordError("You must enter a password for you channel");
}
}
function cleanseClaimName(name) {
name = name.replace(/\s+/g, '-'); // replace spaces with dashes
name = name.replace(/[^A-Za-z0-9-]/g, ''); // remove all characters that are not A-Z, a-z, 0-9, or '-'
return name;
}
// validaiton function to check claim name as the input changes
function checkClaimName(name){
try {
// check to make sure the characters are valid
validateClaimName(name);
clearError('input-error-claim-name');
// check to make sure it is availabe
isNameAvailable(name)
.then(function() {
document.getElementById('claim-name-available').hidden = false;
})
.catch(function(error) {
document.getElementById('claim-name-available').hidden = true;
showError('input-error-claim-name', error.message);
});
} catch (error) {
showError('input-error-claim-name', error.message);
document.getElementById('claim-name-available').hidden = true;
}
// validation functions to check claim & channel name eligibility as the inputs change
function isNameAvailable (name, apiUrl) {
const url = apiUrl + name;
return getRequest(url)
}
function showError(errorDisplay, errorMsg) {
errorDisplay.hidden = false;
errorDisplay.innerText = errorMsg;
}
function hideError(errorDisplay) {
errorDisplay.hidden = true;
errorDisplay.innerText = '';
}
function showSuccess (successElement) {
successElement.hidden = false;
successElement.innerHTML = "&#x2714";
}
function hideSuccess (successElement) {
successElement.hidden = true;
successElement.innerHTML = "";
}
function checkAvailability(name, successDisplayElement, errorDisplayElement, validateName, isNameAvailable, errorMessage, apiUrl) {
try {
// check to make sure the characters are valid
validateName(name);
// check to make sure it is available
isNameAvailable(name, apiUrl)
.then(result => {
console.log('result:', result)
if (result === true) {
hideError(errorDisplayElement);
showSuccess(successDisplayElement)
} else {
hideSuccess(successDisplayElement);
showError(errorDisplayElement, errorMessage);
}
})
.catch(error => {
hideSuccess(successDisplayElement);
showError(errorDisplayElement, error.message);
});
} catch (error) {
hideSuccess(successDisplayElement);
showError(errorDisplayElement, error.message);
}
}
function checkClaimName(name){
const successDisplayElement = document.getElementById('input-success-claim-name');
const errorDisplayElement = document.getElementById('input-error-claim-name');
checkAvailability(name, successDisplayElement, errorDisplayElement, validateClaimName, isNameAvailable, 'Sorry, that url ending has been taken by another user', '/api/isClaimAvailable/');
}
function checkChannelName(name){
const successDisplayElement = document.getElementById('input-success-channel-name');
const errorDisplayElement = document.getElementById('input-error-channel-name');
name = `@${name}`;
checkAvailability(name, successDisplayElement, errorDisplayElement, validateChannelName, isNameAvailable, 'Sorry, that Channel has been taken by another user', '/api/isChannelAvailable/');
}
// validation function which checks all aspects of the publish submission
function validateSubmission(stagedFiles, name){
function validateFilePublishSubmission(stagedFiles, claimName, channelName){
return new Promise(function (resolve, reject) {
// make sure only 1 file was selected
// 1. make sure only 1 file was selected
if (!stagedFiles) {
reject(new FileError("Please select a file"));
return reject(new FileError("Please select a file"));
} else if (stagedFiles.length > 1) {
reject(new FileError("Only one file is allowed at a time"));
return reject(new FileError("Only one file is allowed at a time"));
}
// validate the file's name, type, and size
// 2. validate the file's name, type, and size
try {
validateFile(stagedFiles[0]);
} catch (error) {
reject(error);
return reject(error);
}
// make sure the claim name has not already been used
// 3. validate that a channel was chosen
if (channelName === 'new' || channelName === 'login') {
return reject(new ChannelNameError("Please select a valid channel"));
};
// 4. validate the claim name
try {
validateClaimName(name);
validateClaimName(claimName);
} catch (error) {
reject(error);
return reject(error);
}
isNameAvailable(name)
.then(function() {
// if all validation passes, check availability of the name
isNameAvailable(claimName, '/api/isClaimAvailable/')
.then(() => {
resolve();
})
.catch(function(error) {
.catch(error => {
reject(error);
});
});
}
// validation function which checks all aspects of the publish submission
function validateNewChannelSubmission(channelName, password){
return new Promise(function (resolve, reject) {
// 1. validate name
try {
validateChannelName(channelName);
} catch (error) {
return reject(error);
}
// 2. validate password
try {
validatePassword(password);
} catch (error) {
return reject(error);
}
// 3. if all validation passes, check availability of the name
isNameAvailable(channelName, '/api/isChannelAvailable/') // validate the availability
.then(() => {
console.log('channel is avaliable');
resolve();
})
.catch( error => {
console.log('error: channel is not avaliable');
reject(error);
});
});
}

View file

@ -1,13 +1,15 @@
const logger = require('winston');
const multipart = require('connect-multiparty');
const multipartMiddleware = multipart();
const db = require('../models');
const { publish } = require('../controllers/publishController.js');
const { getClaimList, resolveUri } = require('../helpers/lbryApi.js');
const { createPublishParams, validateFile, checkNameAvailability } = require('../helpers/publishHelpers.js');
const { createPublishParams, validateFile, checkClaimNameAvailability, checkChannelAvailability } = require('../helpers/publishHelpers.js');
const errorHandlers = require('../helpers/errorHandlers.js');
const { postToStats, sendGoogleAnalytics } = require('../controllers/statsController.js');
const { authenticateApiPublish } = require('../auth/authentication.js');
module.exports = (app, hostedContentPath) => {
module.exports = (app) => {
// route to run a claim_list request on the daemon
app.get('/api/claim_list/:name', ({ headers, ip, originalUrl, params }, res) => {
// google analytics
@ -25,12 +27,12 @@ module.exports = (app, hostedContentPath) => {
// route to check whether spee.ch has published to a claim
app.get('/api/isClaimAvailable/:name', ({ ip, originalUrl, params }, res) => {
// send response
checkNameAvailability(params.name)
checkClaimNameAvailability(params.name)
.then(result => {
if (result === true) {
res.status(200).json(true);
} else {
logger.debug(`Rejecting publish request because ${params.name} has already been published via spee.ch`);
logger.debug(`Rejecting '${params.name}' because that name has already been claimed on spee.ch`);
res.status(200).json(false);
}
})
@ -38,6 +40,22 @@ module.exports = (app, hostedContentPath) => {
res.status(500).json(error);
});
});
// route to check whether spee.ch has published to a channel
app.get('/api/isChannelAvailable/:name', ({ params }, res) => {
checkChannelAvailability(params.name)
.then(result => {
if (result === true) {
res.status(200).json(true);
} else {
logger.debug(`Rejecting '${params.name}' because that channel has already been claimed on spee.ch`);
res.status(200).json(false);
}
})
.catch(error => {
logger.debug('api/isChannelAvailable/ error', error);
res.status(500).json(error);
});
});
// route to run a resolve request on the daemon
app.get('/api/resolve/:uri', ({ headers, ip, originalUrl, params }, res) => {
// google analytics
@ -52,7 +70,6 @@ module.exports = (app, hostedContentPath) => {
errorHandlers.handleRequestError('publish', originalUrl, ip, error, res);
});
});
// route to run a publish request on the daemon
app.post('/api/publish', multipartMiddleware, ({ body, files, headers, ip, originalUrl }, res) => {
// google analytics
@ -60,8 +77,13 @@ module.exports = (app, hostedContentPath) => {
// validate that a file was provided
const file = files.speech || files.null;
const name = body.name || file.name.substring(0, file.name.indexOf('.'));
const title = body.title || null;
const description = body.description || null;
const license = body.license || 'No License Provided';
const nsfw = body.nsfw || true;
const nsfw = body.nsfw || null;
const channelName = body.channelName || 'none';
const channelPassword = body.channelPassword || null;
logger.debug(`name: ${name}, license: ${license}, nsfw: ${nsfw}`);
try {
validateFile(file, name, license, nsfw);
} catch (error) {
@ -70,19 +92,54 @@ module.exports = (app, hostedContentPath) => {
res.status(400).send(error.message);
return;
}
// prepare the publish parameters
const fileName = file.name;
const filePath = file.path;
const fileType = file.type;
const publishParams = createPublishParams(name, filePath, license, nsfw);
// publish the file
publish(publishParams, fileName, fileType)
// channel authorization
authenticateApiPublish(channelName, channelPassword)
.then(result => {
if (!result) {
res.status(401).send('Authentication failed, you do not have access to that channel');
throw new Error('authentication failed');
}
return createPublishParams(name, filePath, title, description, license, nsfw, channelName);
})
// create publish parameters object
.then(publishParams => {
return publish(publishParams, fileName, fileType);
})
// publish the asset
.then(result => {
postToStats('publish', originalUrl, ip, null, null, 'success');
res.status(200).json(result);
})
.catch(error => {
errorHandlers.handleRequestError('publish', originalUrl, ip, error, res);
logger.error('publish api error', error);
});
});
// route to get a short claim id from long claim Id
app.get('/api/shortClaimId/:longId/:name', ({ originalUrl, ip, params }, res) => {
// serve content
db.getShortClaimIdFromLongClaimId(params.longId, params.name)
.then(shortId => {
res.status(200).json(shortId);
})
.catch(error => {
logger.error('api error getting short channel id', error);
res.status(400).json(error.message);
});
});
// route to get a short channel id from long channel Id
app.get('/api/shortChannelId/:longId/:name', ({ params }, res) => {
// serve content
db.getShortChannelIdFromLongChannelId(params.longId, params.name)
.then(shortId => {
console.log('sending back short channel id', shortId);
res.status(200).json(shortId);
})
.catch(error => {
logger.error('api error getting short channel id', error);
res.status(400).json(error.message);
});
});
};

15
routes/auth-routes.js Normal file
View file

@ -0,0 +1,15 @@
const logger = require('winston');
const passport = require('passport');
module.exports = (app) => {
// route for sign up
app.post('/signup', passport.authenticate('local-signup'), (req, res) => {
logger.debug('successful signup');
res.status(200).json(true);
});
// route for log in
app.post('/login', passport.authenticate('local-login'), (req, res) => {
logger.debug('successful login');
res.status(200).json(true);
});
};

View file

@ -1,8 +1,20 @@
const errorHandlers = require('../helpers/errorHandlers.js');
const db = require('../models');
const { postToStats, getStatsSummary, getTrendingClaims, getRecentClaims } = require('../controllers/statsController.js');
module.exports = (app) => {
// route to log out
app.get('/logout', (req, res) => {
req.logout();
res.redirect('/');
});
// route to display login page
app.get('/login', (req, res) => {
if (req.user) {
res.status(200).redirect(`/${req.user.channelName}`);
} else {
res.status(200).render('login');
}
});
// route to show 'about' page for spee.ch
app.get('/about', (req, res) => {
// get and render the content
@ -19,7 +31,9 @@ module.exports = (app) => {
getTrendingClaims(dateTime)
.then(result => {
// logger.debug(result);
res.status(200).render('trending', { trendingAssets: result });
res.status(200).render('trending', {
trendingAssets: result,
});
})
.catch(error => {
errorHandlers.handleRequestError(error, res);
@ -30,21 +44,26 @@ module.exports = (app) => {
getRecentClaims()
.then(result => {
// logger.debug(result);
res.status(200).render('new', { newClaims: result });
res.status(200).render('new', {
newClaims: result,
});
})
.catch(error => {
errorHandlers.handleRequestError(error, res);
});
});
// route to show statistics for spee.ch
app.get('/stats', ({ ip, originalUrl }, res) => {
app.get('/stats', ({ ip, originalUrl, user }, res) => {
// get and render the content
const startDate = new Date();
startDate.setDate(startDate.getDate() - 1);
getStatsSummary(startDate)
.then(result => {
postToStats('show', originalUrl, ip, null, null, 'success');
res.status(200).render('statistics', result);
res.status(200).render('statistics', {
user,
result,
});
})
.catch(error => {
errorHandlers.handleRequestError(error, res);
@ -60,20 +79,8 @@ module.exports = (app) => {
res.status(200).render('embed', { layout: 'embed', claimId, name });
});
// route to display all free public claims at a given name
app.get('/:name/all', ({ ip, originalUrl, params }, res) => {
app.get('/:name/all', (req, res) => {
// get and render the content
db
.getAllFreeClaims(params.name)
.then(orderedFreeClaims => {
if (!orderedFreeClaims) {
res.status(307).render('noClaims');
return;
}
postToStats('show', originalUrl, ip, null, null, 'success');
res.status(200).render('allClaims', { claims: orderedFreeClaims });
})
.catch(error => {
errorHandlers.handleRequestError('show', originalUrl, ip, error, res);
});
res.status(410).send('/:name/all is no longer supported');
});
};

View file

@ -124,15 +124,12 @@ module.exports = (app) => {
// 1. retrieve the channel contents
getChannelContents(channelName, channelId)
// 2. respond to the request
.then(channelContents => {
if (!channelContents) {
.then(result => {
logger.debug('result');
if (!result.claims) {
res.status(200).render('noChannel');
} else {
const handlebarsData = {
channelName,
channelContents,
};
res.status(200).render('channel', handlebarsData);
res.status(200).render('channel', result);
}
})
.catch(error => {

View file

@ -35,8 +35,15 @@ module.exports = (app, siofu, hostedContentPath) => {
if (file.success) {
logger.debug(`Client successfully uploaded ${file.name}`);
socket.emit('publish-status', 'File upload successfully completed. Your image is being published to LBRY (this might take a second)...');
/*
NOTE: need to validate that client has the credentials to the channel they chose
otherwise they could circumvent security client side.
*/
// prepare the publish parameters
const publishParams = publishHelpers.createPublishParams(file.meta.name, file.pathName, file.meta.title, file.meta.description, file.meta.license, file.meta.nsfw);
const publishParams = publishHelpers.createPublishParams(file.meta.name, file.pathName, file.meta.title, file.meta.description, file.meta.license, file.meta.nsfw, file.meta.channel);
logger.debug(publishParams);
// publish the file
publishController.publish(publishParams, file.name, file.meta.type)
.then(result => {
@ -52,7 +59,7 @@ module.exports = (app, siofu, hostedContentPath) => {
logger.error(`An error occurred in uploading the client's file`);
socket.emit('publish-failure', 'File uploaded, but with errors');
postToStats('PUBLISH', '/', null, null, null, 'File uploaded, but with errors');
// to-do: remove the file if not done automatically
// to-do: remove the file, if not done automatically
}
});
// handle disconnect

161
speech.js
View file

@ -4,133 +4,99 @@ const bodyParser = require('body-parser');
const siofu = require('socketio-file-upload');
const expressHandlebars = require('express-handlebars');
const Handlebars = require('handlebars');
const handlebarsHelpers = require('./helpers/handlebarsHelpers.js');
const config = require('config');
const logger = require('winston');
const { getDownloadDirectory } = require('./helpers/lbryApi');
const helmet = require('helmet');
const PORT = 3000; // set port
const app = express(); // create an Express application
const db = require('./models'); // require our models for syncing
const passport = require('passport');
const session = require('express-session');
// configure logging
const logLevel = config.get('Logging.LogLevel');
require('./config/loggerConfig.js')(logger, logLevel);
require('./config/slackLoggerConfig.js')(logger);
// check for global config variables
require('./helpers/configVarCheck.js')();
// trust the proxy to get ip address for us
app.enable('trust proxy');
// add middleware
app.use(express.static(`${__dirname}/public`)); // 'express.static' to serve static files from public directory
app.use(helmet()); // set HTTP headers to protect against well-known web vulnerabilties
app.use(express.static(`${__dirname}/public`)); // 'express.static' to serve static files from public directory
app.use(bodyParser.json()); // 'body parser' for parsing application/json
app.use(bodyParser.urlencoded({ extended: true })); // 'body parser' for parsing application/x-www-form-urlencoded
app.use(siofu.router); // 'socketio-file-upload' router for uploading with socket.io
app.use((req, res, next) => { // custom logging middleware to log all incomming http requests
app.use((req, res, next) => { // custom logging middleware to log all incoming http requests
logger.verbose(`Request on ${req.originalUrl} from ${req.ip}`);
logger.debug(req.body);
next();
});
// initialize passport
app.use(session({ secret: 'cats' }));
app.use(passport.initialize());
app.use(passport.session());
passport.serializeUser((user, done) => {
done(null, user.id);
});
passport.deserializeUser((id, done) => { // this populates req.user
let userInfo = {};
db.User.findOne({ where: { id } })
.then(user => {
userInfo['id'] = user.id;
userInfo['userName'] = user.userName;
return user.getChannel();
})
.then(channel => {
userInfo['channelName'] = channel.channelName;
userInfo['channelClaimId'] = channel.channelClaimId;
return db.getShortChannelIdFromLongChannelId(channel.channelClaimId, channel.channelName);
})
.then(shortChannelId => {
userInfo['shortChannelId'] = shortChannelId;
done(null, userInfo);
return null;
})
.catch(error => {
logger.error('sequelize error', error);
done(error, null);
});
});
const localSignupStrategy = require('./passport/local-signup.js');
const localLoginStrategy = require('./passport/local-login.js');
passport.use('local-signup', localSignupStrategy);
passport.use('local-login', localLoginStrategy);
// configure handlebars & register it with express app
const hbs = expressHandlebars.create({
defaultLayout: 'main', // sets the default layout
handlebars : Handlebars, // includes basic handlebars for access to that library
helpers : {
// define any extra helpers you may need
googleAnalytics () {
const googleApiKey = config.get('AnalyticsConfig.GoogleId');
return new Handlebars.SafeString(
`<script>
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
ga('create', '${googleApiKey}', 'auto');
ga('send', 'pageview');
</script>`
);
},
addOpenGraph (title, mimeType, showUrl, source, description, thumbnail) {
let basicTags = `<meta property="og:title" content="${title}">
<meta property="og:url" content="${showUrl}" >
<meta property="og:site_name" content="Spee.ch" >
<meta property="og:description" content="${description}">`;
if (mimeType === 'video/mp4') {
return new Handlebars.SafeString(
`${basicTags} <meta property="og:image" content="${thumbnail}" >
<meta property="og:image:type" content="image/png" >
<meta property="og:image:width" content="600" >
<meta property="og:image:height" content="315" >
<meta property="og:type" content="video" >
<meta property="og:video" content="${source}" >
<meta property="og:video:secure_url" content="${source}" >
<meta property="og:video:type" content="${mimeType}" >`
);
} else if (mimeType === 'image/gif') {
return new Handlebars.SafeString(
`${basicTags} <meta property="og:image" content="${source}" >
<meta property="og:image:type" content="${mimeType}" >
<meta property="og:image:width" content="600" >
<meta property="og:image:height" content="315" >
<meta property="og:type" content="video.other" >`
);
} else {
return new Handlebars.SafeString(
`${basicTags} <meta property="og:image" content="${source}" >
<meta property="og:image:type" content="${mimeType}" >
<meta property="og:image:width" content="600" >
<meta property="og:image:height" content="315" >
<meta property="og:type" content="article" >`
);
}
},
addTwitterCard (mimeType, source, embedUrl, directFileUrl) {
let basicTwitterTags = `<meta name="twitter:site" content="@speechch" >`;
if (mimeType === 'video/mp4') {
return new Handlebars.SafeString(
`${basicTwitterTags} <meta name="twitter:card" content="player" >
<meta name="twitter:player" content="${embedUrl}>
<meta name="twitter:player:width" content="600" >
<meta name="twitter:text:player_width" content="600" >
<meta name="twitter:player:height" content="337" >
<meta name="twitter:player:stream" content="${directFileUrl}" >
<meta name="twitter:player:stream:content_type" content="video/mp4" >
`
);
} else {
return new Handlebars.SafeString(
`${basicTwitterTags} <meta name="twitter:card" content="summary_large_image" >`
);
}
},
ifConditional (varOne, operator, varTwo, options) {
switch (operator) {
case '===':
return (varOne === varTwo) ? options.fn(this) : options.inverse(this);
case '!==':
return (varOne !== varTwo) ? options.fn(this) : options.inverse(this);
case '<':
return (varOne < varTwo) ? options.fn(this) : options.inverse(this);
case '<=':
return (varOne <= varTwo) ? options.fn(this) : options.inverse(this);
case '>':
return (varOne > varTwo) ? options.fn(this) : options.inverse(this);
case '>=':
return (varOne >= varTwo) ? options.fn(this) : options.inverse(this);
case '&&':
return (varOne && varTwo) ? options.fn(this) : options.inverse(this);
case '||':
return (varOne || varTwo) ? options.fn(this) : options.inverse(this);
case 'mod3':
return ((parseInt(varOne) % 3) === 0) ? options.fn(this) : options.inverse(this);
default:
return options.inverse(this);
}
},
},
helpers : handlebarsHelpers, // custom defined helpers
});
app.engine('handlebars', hbs.engine);
app.set('view engine', 'handlebars');
// middleware to pass user info back to client (for handlebars access), if user is logged in
app.use((req, res, next) => {
if (req.user) {
logger.verbose(req.user);
res.locals.user = {
id : req.user.id,
userName : req.user.userName,
channelName : req.user.channelName,
channelClaimId: req.user.channelClaimId,
shortChannelId: req.user.shortChannelId,
};
}
next();
});
// start the server
db.sequelize
.sync() // sync sequelize
@ -142,7 +108,8 @@ db.sequelize
// add the hosted content folder at a static path
app.use('/media', express.static(hostedContentPath));
// require routes & wrap in socket.io
require('./routes/api-routes.js')(app, hostedContentPath);
require('./routes/auth-routes.js')(app);
require('./routes/api-routes.js')(app);
require('./routes/page-routes.js')(app);
require('./routes/serve-routes.js')(app);
require('./routes/home-routes.js')(app);

View file

@ -1,23 +0,0 @@
<div class="wrapper">
{{> topBar}}
<div>
<h3>All Claims</h3>
<p>These are all the free, public assets at that claim. You can publish more at <a href="/">spee.ch</a>.</p>
{{#each claims}}
<div class="all-claims-item">
<img class="all-claims-asset" src="/{{this.claimId}}/{{this.name}}.test" />
<div class="all-claims-details">
<ul style="list-style-type:none">
<li>claim: {{this.name}}</li>
<li>claim_id: {{this.claim_id}}</li>
<li>link: <a href="/{{this.claimId}}/{{this.name}}">spee.ch/{{this.name}}/{{this.claimId}}</a></li>
<li>author: {{this.value.stream.metadata.author}}</li>
<li>description: {{this.value.stream.metadata.description}}</li>
<li>license: {{this.value.stream.metadata.license}}</li>
</ul>
</div>
</div>
{{/each}}
</div>
{{> footer}}
</div>

View file

@ -1,9 +1,9 @@
<div class="wrapper">
{{> topBar}}
<div>
<h3>{{this.channelName}}</h3>
<h3>{{this.channelName}}<span class="h3--secondary">:{{this.longChannelId}}</span></h3>
<p>Below is all the free content in this channel.</p>
{{#each channelContents}}
{{#each this.claims}}
{{> contentListItem}}
{{/each}}
</div>

View file

@ -1,17 +1,19 @@
<div class="wrapper">
{{> topBar}}
<div class="full">
{{> publish}}
{{> learnMore}}
</div>
{{> footer}}
<script src="/assets/js/generalFunctions.js"></script>
<div class="row">
<div class="column column--2"></div>
<div class="column column--8">
{{> topBar}}
{{> publishForm}}
{{> learnMore}}
{{> footer}}
</div>
</div>
<script src="/socket.io/socket.io.js"></script>
<script src="/siofu/client.js"></script>
<script src="/assets/js/generalFunctions.js"></script>
<script src="/assets/js/validationFunctions.js"></script>
<script src="/assets/js/publishFunctions.js"></script>
<script src="/assets/js/publishFileFunctions.js"></script>
<script typ="text/javascript">
// define variables
var socket = io();
@ -24,12 +26,14 @@
var description = document.getElementById('publish-description').value;
var license = document.getElementById('publish-license').value;
var nsfw = document.getElementById('publish-nsfw').checked;
var channel = document.getElementById('channel-name-select').value;
event.file.meta.name = name;
event.file.meta.title = title;
event.file.meta.description = description;
event.file.meta.license = license;
event.file.meta.nsfw = nsfw;
event.file.meta.type = stagedFiles[0].type;
event.file.meta.channel = channel;
// re-set the html in the publish area
document.getElementById('publish-active-area').innerHTML = '<div id="publish-status"></div><div id="progress-bar"></div>';
// start a progress animation
@ -54,7 +58,7 @@
});
socket.on('publish-complete', function(msg){
var publishResults;
var showUrl = msg.name + '/' + msg.result.claim_id;
var showUrl = msg.result.claim_id + "/" + msg.name;
// build new publish area
publishResults = '<p>Your publish is complete! You are being redirected to it now.</p>';
publishResults += '<p><a target="_blank" href="' + showUrl + '">If you do not get redirected, click here.</a></p>';
@ -62,5 +66,4 @@
document.getElementById('publish-active-area').innerHTML = publishResults;
window.location.href = showUrl;
});
</script>

View file

@ -5,8 +5,10 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1, user-scalable=no">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Spee.ch</title>
<link rel="stylesheet" href="/assets/css/generalStyle.css" type="text/css">
<link rel="stylesheet" href="/assets/css/reset.css" type="text/css">
<link rel="stylesheet" href="/assets/css/BEM.css" type="text/css">
<link rel="stylesheet" href="/assets/css/componentStyle.css" type="text/css">
<link rel="stylesheet" href="/assets/css/mediaQueries.css" type="text/css">
<meta name="twitter:card" content="summary" />
<meta name="twitter:site" content="@lbryio" />
<meta property="og:title" content="spee.ch">

View file

@ -5,8 +5,10 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1, user-scalable=no">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Spee.ch</title>
<link rel="stylesheet" href="/assets/css/generalStyle.css" type="text/css">
<link rel="stylesheet" href="/assets/css/componentStyle.css" type="text/css">
<link rel="stylesheet" href="/assets/css/reset.css" type="text/css">
<link rel="stylesheet" href="/assets/css/BEM.css" type="text/css">
<link rel="stylesheet" href="/assets/css/componentStyle.css" type="text/css">
<link rel="stylesheet" href="/assets/css/mediaQueries.css" type="text/css">
<meta property="fb:app_id" content="1371961932852223">
{{#unless fileInfo.nsfw}}
{{{addTwitterCard fileInfo.fileType openGraphInfo.source openGraphInfo.embedUrl openGraphInfo.directFileUrl}}}

23
views/login.handlebars Normal file
View file

@ -0,0 +1,23 @@
<div class="wrapper">
{{> topBar}}
<h2>Log In</h2>
<div class="row row--wide">
<div class="column column--6">
<p>Log in to an existing channel:</p>
{{>channelLoginForm}}
</div>
</div>
<h2>Create New</h2>
<div class="row row--wide">
<div class="column column--6">
<p>Create a brand new channel:</p>
{{>channelCreationForm}}
</div>
</div>
{{> footer}}
</div>
<script src="/assets/js/generalFunctions.js"></script>
<script src="/assets/js/validationFunctions.js"></script>
<script src="/assets/js/authFunctions.js"></script>

View file

@ -1,4 +1,4 @@
<div class="panel">
<div class="row">
<div id="asset-placeholder">
<a href="/{{fileInfo.claimId}}/{{fileInfo.name}}.{{fileInfo.fileExt}}">
{{#ifConditional fileInfo.fileType '===' 'video/mp4'}}

View file

@ -3,7 +3,7 @@
<p>{{fileInfo.title}}</>
</div>
<div class="panel links">
<h2 class="subheader">Links</h2>
<h2>Links</h2>
{{!--short direct link to asset--}}
<div class="share-option">
<a href="/{{fileInfo.shortId}}/{{fileInfo.name}}.{{fileInfo.fileExt}}">Permanent Short Link</a> (most convenient)
@ -45,11 +45,11 @@
{{/ifConditional}}
</div>
<div class="panel">
<h2>Description</h2>
<p>{{fileInfo.description}}</>
<h2>Description</h2>
<p>{{fileInfo.description}}</p>
</div>
<div class="panel">
<h2 class="subheader">Metadata</h2>
<h2>Metadata</h2>
<table class="metadata-table" style="table-layout: fixed">
<tr class="metadata-row">
<td class="left-column">Name</td>

View file

@ -0,0 +1,78 @@
<form id="publish-channel-form">
<div class="column column--3">
<label class="label" for="new-channel-name">Name:</label>
</div>
<div class="column column--9">
<div id="input-error-channel-name" class="info-message info-message--failure"></div>
<div class="input-text--primary">
<span>@</span>
<input type="text" name="new-channel-name" id="new-channel-name" class="input-text" placeholder="exampleChannel" value="" oninput="checkChannelName(event.target.value)">
<span id="input-success-channel-name" class="info-message info-message--success"></span>
</div>
</div>
<div class="column column--3">
<label class="label" for="new-channel-password">Password:</label>
</div>
<div class="column column--9">
<div id="input-error-channel-password" class="info-message info-message--failure"></div>
<input type="password" name="new-channel-password" id="new-channel-password" placeholder="" value="" class="input-text input-text--primary">
</div>
<div class="row row--wide">
<button onclick="publishNewChannel(event)">Create Channel</button>
</div>
</form>
<div id="channel-publish-in-progress" hidden="true">
<p>Creating your new channel. This may take a few seconds...</p>
<div id="create-channel-progress-bar"></div>
</div>
<div id="channel-publish-done" hidden="true">
<p>Your channel has been successfully created!</p>
</div>
<script type="text/javascript">
function publishNewChannel (event) {
const channelName = document.getElementById('new-channel-name').value;
const password = document.getElementById('new-channel-password').value;
const channelNameErrorDisplayElement = document.getElementById('input-error-channel-name');
const passwordErrorDisplayElement = document.getElementById('input-error-channel-password');
const chanelCreateForm = document.getElementById('publish-channel-form');
const inProgress = document.getElementById('channel-publish-in-progress');
const done = document.getElementById('channel-publish-done');
// prevent default so this script can handle submission
event.preventDefault();
// validate submission
validateNewChannelSubmission(channelName, password)
.then(() => {
console.log('in progress');
chanelCreateForm.hidden = true;
inProgress.hidden = false;
createProgressBar(document.getElementById('create-channel-progress-bar'), 12);
return sendAuthRequest(channelName, password, '/signup') // post the request
})
.then(() => {
console.log('success');
inProgress.hidden=true;
done.hidden = false;
// refresh window logged in as the channel
window.location.href = '/';
})
.catch(error => {
if (error.name === 'ChannelNameError'){
showError(channelNameErrorDisplayElement, error.message);
} else if (error.name === 'ChannelPasswordError'){
showError(passwordErrorDisplayElement, error.message);
} else {
console.log('failure:', error);
}
})
}
</script>

View file

@ -0,0 +1,45 @@
<form id="channel-login-form">
<div class="column column--3">
<label class="label" for="login-channel-name">Name:</label>
</div>
<div class="column column--9">
<div id="login-error-display-element" class="info-message info-message--failure"></div>
<div class="input-text--primary">
<span>@</span>
<input type="text" name="login-channel-name" id="login-channel-name" class="input-text" placeholder="" value="">
</div>
</div>
<div class="column column--3">
<label class="label" for="login-channel-password" >Password:</label>
</div>
<div class="column column--9">
<input type="password" name="login-channel-password" id="login-channel-password" class="input-text input-text--primary" placeholder="" value="">
</div>
</form>
<div class="row row--wide">
<button onclick="loginToChannel(event)">Login</button>
</div>
<script type="text/javascript">
function loginToChannel (event) {
const channelName = document.getElementById('login-channel-name').value;
const password = document.getElementById('login-channel-password').value;
const loginErrorDisplayElement = document.getElementById('login-error-display-element');
// prevent default
event.preventDefault()
// send request
sendAuthRequest(channelName, password, '/login')
.then(() => {
console.log('login success');
window.location.href = '/';
})
.catch(error => {
showError(loginErrorDisplayElement, error);
console.log('login failure:', error);
})
}
</script>

View file

@ -1,6 +1,6 @@
<div class="panel">
<h2>Documentation
<a class="toggle-link" id="documentation-toggle" href="#" onclick="toggleSection(event)" data-open="false" data-slaveelementid="documentation-detail">[open]</a>
<a class="toggle-link" id="documentation-toggle" href="#" onclick="toggleSection(event)" data-open="false" data-openlabel="[ - ]" data-closedlabel="[ + ]" data-slaveelementid="documentation-detail">[ + ]</a>
</h2>
<div id="documentation-detail" hidden="true">
<code>https://spee.ch/</code>

View file

@ -1,6 +1,6 @@
<div class="panel">
<h2>Examples
<a class="toggle-link" id="examples-toggle" href="#" onclick="toggleSection(event)" data-open="false" data-slaveelementid="examples-detail">[open]</a>
<a class="toggle-link" id="examples-toggle" href="#" onclick="toggleSection(event)" data-open="false" data-openlabel="[ - ]" data-closedlabel="[ + ]" data-slaveelementid="examples-detail">[ + ]</a>
</h2>
<div id="examples-detail" hidden="true">
<div class="example">

View file

@ -1,3 +1,3 @@
<footer class="stop-float">
<footer class="row">
<p> thanks for visiting spee.ch </p>
</footer>

View file

@ -1,3 +1,3 @@
<div class="learn-more stop-float">
<div class="row learn-more">
<p><i>Spee.ch is an open-source project. You should <a href="https://github.com/lbryio/spee.ch/issues">contribute</a> on github, or <a href="https://github.com/lbryio/spee.ch">fork it</a> and make your own!</i></p>
</div>

View file

@ -1,79 +0,0 @@
<div class="panel">
<h2>Publish</h2>
<div class="row" style="overflow:auto; height: 100%;">
<div class="col-left" id="file-selection-area">
<div id="drop-zone" ondrop="drop_handler(event);" ondragover="dragover_handler(event);" ondragend="dragend_handler(event)">
<p>Drag and drop your file here, or choose your file below.</p>
<div class="input-error" id="input-error-file-selection" hidden="true"></div>
<input type="file" id="siofu_input" name="file" accept="video/*,image/*" onchange="previewAndStageFile(event.target.files[0])" enctype="multipart/form-data"/>
</div>
<div id="asset-preview-holder"></div>
</div>
<div class="col-right">
<div id="publish-active-area">
<div class="input-error" id="input-error-claim-name" hidden="true"></div>
<div id="claim-name-input-area">
Spee.ch/<input type="text" id="claim-name-input" placeholder="your-url-here" oninput="checkClaimName(event.target.value)">
<span id="claim-name-available" hidden="true" style="color: green">&#x2714</span>
</div>
<div class="stop-float">
<table>
<tr>
<td><label for="publish-title">Title: </label></td>
<td><input type="text" id="publish-title" class="publish-input"></td>
</tr>
<tr>
<td><label for="publish-description">Description: </label></td>
<td><textarea rows="2" id="publish-description" class="publish-input"> </textarea></td>
</tr>
<tr>
<td><label for="publish-license">License:* </label></td>
<td>
<select type="text" id="publish-license" name="license" value="license">
<option value="Public Domain">Public Domain</option>
<option value="Creative Commons">Creative Commons</option>
</select>
</td>
</tr>
<tr>
<td><label for="publish-nsfw">NSFW*</label></td>
<td><input type="checkbox" id="publish-nsfw"></td>
</tr>
</table>
</div>
<p>
<div class="input-error" id="input-error-publish-submit" hidden="true"></div>
<button id="publish-submit" onclick="publishSelectedImage(event)">Publish</button>
<button onclick="resetPublishArea()">Reset</button>
</p>
<p><i>By clicking 'Publish' I attest that I have read and agree to the <a href="https://lbry.io/termsofservice" target="_blank">LBRY terms of service</a>.</i></p>
</div>
</div>
</div>
</div>
<script type="text/javascript" >
function resetPublishArea (){
// reset file selection area
document.getElementById('file-selection-area').innerHTML = `<div id="drop-zone" ondrop="drop_handler(event);" ondragover="dragover_handler(event);" ondragend="dragend_handler(event)">
<p>Drag and drop your file here, or choose your file below.</p>
<div class="input-error" id="input-error-file-selection" hidden="true"></div>
<input type="file" id="siofu_input" name="file" accept="video/*,image/*" onchange="previewAndStageFile(event.target.files[0])" enctype="multipart/form-data"/>
</div>
<div id="asset-preview-holder"></div>`;
// reset inputs
document.getElementById('claim-name-input').value = '';
document.getElementById('publish-title').value = '';
document.getElementById('publish-description').value = '';
document.getElementById('publish-nsfw').checked = false;
// remove staged files
stagedFiles = null;
// clear any errors
document.getElementById('input-error-file-selection').innerHTML = '';
document.getElementById('input-error-claim-name').innerHTML = '';
document.getElementById('input-error-publish-submit').innerHTML = '';
document.getElementById('claim-name-available').hidden = true;
// remove nsfw check
}
</script>

View file

@ -0,0 +1,66 @@
<div class="row">
<div class="column column--3">
<label class="label" for="channel-name-select">Channel:</label>
</div>
<div class="column column--9">
<div id="input-error-channel-select" class="info-message info-message--failure"></div>
<select type="text" id="channel-name-select" class="select select--primary" value="channel" onchange="toggleChannel(event)">
<optgroup>
{{#if user}}
<option value="{{user.channelName}}" >@{{user.userName}}</option>
{{/if}}
<option value="none" >None</option>
</optgroup>
<optgroup>
<option value="login">Login</option>
<option value="new" >New</option>
</optgroup>
</select>
</div>
</div>
<div id="channel-login-details" class="row" hidden="true">
{{> channelLoginForm}}
</div>
<div id="channel-create-details" class="row" hidden="true">
{{> channelCreationForm}}
</div>
<script src="/assets/js/authFunctions.js"></script>
<script type="text/javascript">
function toggleChannel (event) {
const createChannelTool = document.getElementById('channel-create-details');
const loginToChannelTool = document.getElementById('channel-login-details');
const selectedOption = event.target.selectedOptions[0].value;
const urlChannel = document.getElementById('url-channel');
console.log('toggle event triggered');
if (selectedOption === 'new') {
// show/hide the login and new channel forms
createChannelTool.hidden = false;
loginToChannelTool.hidden = true;
// update URL
urlChannel.innerText = '';
} else if (selectedOption === 'login') {
// show/hide the login and new channel forms
loginToChannelTool.hidden = false;
createChannelTool.hidden = true;
// update URL
urlChannel.innerText = '';
} else {
// hide the login and new channel forms
loginToChannelTool.hidden = true;
createChannelTool.hidden = true;
hideError(document.getElementById('input-error-channel-select'));
// update URL
if (selectedOption === 'none'){
console.log('selected option: none');
urlChannel.innerText = '';
} else {
console.log('selected option:', selectedOption);
// retrieve short url from db
urlChannel.innerText = `{{user.channelName}}:{{user.shortChannelId}}/`;
}
}
}
</script>

View file

@ -0,0 +1,51 @@
<div id="details-detail" hidden="true">
<div class="row row--thin">
<div class="column column--3">
<label for="publish-title" class="label">Title: </label>
</div>
<div class="column column--9">
<input type="text" id="publish-title" class="input-text input-text--primary">
</div>
</div>
<div class="row row--thin">
<div class="column column--3">
<label for="publish-description" class="label">Description: </label>
</div>
<div class="column column--9">
<textarea rows="2" id="publish-description" class="input-textarea"></textarea>
</div>
</div>
<div class="row row--thin">
<div class="column column--3">
<label for="publish-license" class="label">License:* </label>
</div>
<div class="column column--9">
<select type="text" id="publish-license" class="select select--primary">
<option value="Public Domain">Public Domain</option>
<option value="Creative Commons">Creative Commons</option>
</select>
</div>
</div>
<div class="row row--thin">
<div class="column column--3">
<label for="publish-nsfw" class="label">NSFW*</label>
</div>
<div class="column column--9">
<input class="input-checkbox" type="checkbox" id="publish-nsfw">
</div>
</div>
</div>
<div class="row">
<div class="column column--12">
<a class="label" id="details-toggle" href="#" onclick="toggleSection(event)" data-open="false" data-openlabel="[less]" data-closedlabel="[more]" data-slaveelementid="details-detail">[more]</a>
</div>
</div>

View file

@ -0,0 +1,13 @@
<div class="row">
<div class="column column--3">
<label class="label">URL:</label>
</div>
<div class="column column--9">
<div id="input-error-claim-name" class="info-message info-message--failure" hidden="true"></div>
<div class="input-text--primary">
<span class="url-text">Spee.ch/</span><span id="url-channel" class="url-text">{{#if user}}{{user.channelName}}:{{user.shortChannelId}}/{{/if}}</span><input type="text" id="claim-name-input" class="input-text" placeholder="your-url-here" oninput="checkClaimName(event.target.value)">
<span id="input-success-claim-name" class="info-message info-message--success"></span>
</div>
</div>
</div>

View file

@ -0,0 +1,62 @@
<div class="panel">
<h2>Publish</h2>
<div class="row">
<div class="col-left">
<div id="file-selection-area">
<div id="drop-zone" ondrop="drop_handler(event);" ondragover="dragover_handler(event);" ondragend="dragend_handler(event)">
<div class="row">
<p>Drag and drop your file here, or choose your file below.</p>
<div class="info-message info-message--failure" id="input-error-file-selection" hidden="true"></div>
</div>
<div class="row">
<input type="file" id="siofu_input" name="file" accept="video/*,image/*" onchange="previewAndStageFile(event.target.files[0])" enctype="multipart/form-data"/>
</div>
</div>
<div id="asset-preview-holder"></div>
</div>
</div>
<div class="col-right">
<div id="publish-active-area">
{{> publishForm-Channel}}
{{> publishForm-Url}}
{{> publishForm-Details}}
<div class="row">
<div class="input-error" id="input-error-publish-submit" hidden="true"></div>
<button id="publish-submit" onclick="publishSelectedImage(event)">Publish</button>
<button onclick="resetPublishArea()">Reset</button>
</div>
</div>
</div>
</div>
</div>
<script type="text/javascript" >
function resetPublishArea (){
// reset file selection area
document.getElementById('file-selection-area').innerHTML = `<div id="drop-zone" ondrop="drop_handler(event);" ondragover="dragover_handler(event);" ondragend="dragend_handler(event)">
<p>Drag and drop your file here, or choose your file below.</p>
<div class="info-message info-message--failure" id="input-error-file-selection" hidden="true"></div>
<input type="file" id="siofu_input" name="file" accept="video/*,image/*" onchange="previewAndStageFile(event.target.files[0])" enctype="multipart/form-data"/>
</div>
<div id="asset-preview-holder"></div>`;
// reset inputs
document.getElementById('claim-name-input').value = '';
document.getElementById('publish-title').value = '';
document.getElementById('publish-description').value = '';
document.getElementById('publish-nsfw').checked = false;
// remove staged files
stagedFiles = null;
// clear any errors
document.getElementById('input-error-file-selection').innerHTML = '';
document.getElementById('input-error-claim-name').innerHTML = '';
document.getElementById('input-error-publish-submit').innerHTML = '';
document.getElementById('input-success-claim-name').hidden = true;
}
</script>

View file

@ -1,8 +1,32 @@
<div class="top-bar">
<div class="row top-bar">
<a href="https://en.wikipedia.org/wiki/Freedom_of_information" target="_blank"><img id="logo" src="/assets/img/content-freedom-64px.png"/></a>
<h1 id="title"><a href="/">Spee.ch</a></h1><span class="top-bar-left">(beta)</span>
<a href="/popular" class="top-bar-right">popular</a>
<a href="https://github.com/lbryio/spee.ch" target="_blank" class="top-bar-right">source</a>
<a href="/about" class="top-bar-right">help</a>
{{#if user}}
<select type="text" class="select" onchange="toggleLogin(event)">
<option value="none">@{{user.userName}}</option>
<option value="view">view</option>
<option value="logout">logout</option>
</select>
{{else}}
<a href="/login" class="top-bar-right">login</a>
{{/if}}
<div class="top-bar-tagline">Open-source, decentralized image and video hosting.</div>
</div>
<script type="text/javascript">
function toggleLogin (event) {
console.log(event);
const selectedOption = event.target.selectedOptions[0].value;
if (selectedOption === 'logout') {
console.log('login');
window.location.href = '/logout';
} else if (selectedOption === 'view') {
console.log('view channel');
window.location.href = '/{{user.channelName}}:{{user.channelClaimId}}';
}
}
</script>