Authentication #170
1
.gitignore
vendored
|
@ -1,2 +1,3 @@
|
|||
node_modules
|
||||
.idea
|
||||
config/config.json
|
33
auth/authentication.js
Normal 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();
|
||||
});
|
||||
});
|
||||
},
|
||||
};
|
|
@ -1,16 +1,20 @@
|
|||
👍 :+1:
Great design here! Love that this makes it easy to use different channels on staging/dev and production as well as easy to change if forked. Great design here! Love that this makes it easy to use different channels on staging/dev and production as well as easy to change if forked.
👍 :+1:
|
||||
{
|
||||
"WalletConfig": {
|
||||
"LbryClaimAddress": "none"
|
||||
Great design here! Love that this makes it easy to use different channels on staging/dev and production as well as easy to change if forked. Great design here! Love that this makes it easy to use different channels on staging/dev and production as well as easy to change if forked.
👍 :+1:
|
||||
"LbryClaimAddress": null,
|
||||
Great design here! Love that this makes it easy to use different channels on staging/dev and production as well as easy to change if forked. Great design here! Love that this makes it easy to use different channels on staging/dev and production as well as easy to change if forked.
👍 :+1:
|
||||
"DefaultChannel": null
|
||||
Great design here! Love that this makes it easy to use different channels on staging/dev and production as well as easy to change if forked. Great design here! Love that this makes it easy to use different channels on staging/dev and production as well as easy to change if forked.
👍 :+1:
|
||||
},
|
||||
"AnalyticsConfig":{
|
||||
"GoogleId": "none"
|
||||
Great design here! Love that this makes it easy to use different channels on staging/dev and production as well as easy to change if forked. Great design here! Love that this makes it easy to use different channels on staging/dev and production as well as easy to change if forked.
👍 :+1:
|
||||
"GoogleId": null
|
||||
Great design here! Love that this makes it easy to use different channels on staging/dev and production as well as easy to change if forked. Great design here! Love that this makes it easy to use different channels on staging/dev and production as well as easy to change if forked.
👍 :+1:
|
||||
},
|
||||
"Database": {
|
||||
"Database": "lbry",
|
||||
"Username": "none",
|
||||
Great design here! Love that this makes it easy to use different channels on staging/dev and production as well as easy to change if forked. Great design here! Love that this makes it easy to use different channels on staging/dev and production as well as easy to change if forked.
👍 :+1:
|
||||
"Password": "none"
|
||||
Great design here! Love that this makes it easy to use different channels on staging/dev and production as well as easy to change if forked. Great design here! Love that this makes it easy to use different channels on staging/dev and production as well as easy to change if forked.
👍 :+1:
|
||||
"Username": null,
|
||||
Great design here! Love that this makes it easy to use different channels on staging/dev and production as well as easy to change if forked. Great design here! Love that this makes it easy to use different channels on staging/dev and production as well as easy to change if forked.
👍 :+1:
Is there a reason this file uses Is there a reason this file uses `"none"` as a string instead of `null` or something falsier?
I did it so that when I called the variable later, if l logged the variable and got 'none' I knew the variable had been created but wasn't updated with the proper value for the environment. Not very clean/efficient. I fixed this now. They are all set to I did it so that when I called the variable later, if l logged the variable and got 'none' I knew the variable had been created but wasn't updated with the proper value for the environment. Not very clean/efficient. I fixed this now. They are all set to `null` and a script runs through them when the server loads and prints their values so I can check them up front. (https://github.com/lbryio/spee.ch/pull/170/commits/250ead165bd51c4b9812d514c724d320f526d90c)
|
||||
"Password": null
|
||||
Great design here! Love that this makes it easy to use different channels on staging/dev and production as well as easy to change if forked. Great design here! Love that this makes it easy to use different channels on staging/dev and production as well as easy to change if forked.
👍 :+1:
|
||||
},
|
||||
"Logging": {
|
||||
"LogLevel": "none"
|
||||
Great design here! Love that this makes it easy to use different channels on staging/dev and production as well as easy to change if forked. Great design here! Love that this makes it easy to use different channels on staging/dev and production as well as easy to change if forked.
👍 :+1:
|
||||
"LogLevel": null,
|
||||
Great design here! Love that this makes it easy to use different channels on staging/dev and production as well as easy to change if forked. Great design here! Love that this makes it easy to use different channels on staging/dev and production as well as easy to change if forked.
👍 :+1:
|
||||
"SlackWebHook": null,
|
||||
Great design here! Love that this makes it easy to use different channels on staging/dev and production as well as easy to change if forked. Great design here! Love that this makes it easy to use different channels on staging/dev and production as well as easy to change if forked.
👍 :+1:
|
||||
"SlackErrorChannel": null,
|
||||
Great design here! Love that this makes it easy to use different channels on staging/dev and production as well as easy to change if forked. Great design here! Love that this makes it easy to use different channels on staging/dev and production as well as easy to change if forked.
👍 :+1:
|
||||
"SlackInfoChannel": null
|
||||
Great design here! Love that this makes it easy to use different channels on staging/dev and production as well as easy to change if forked. Great design here! Love that this makes it easy to use different channels on staging/dev and production as well as easy to change if forked.
👍 :+1:
|
||||
}
|
||||
}
|
|
@ -1,13 +1,10 @@
|
|||
{
|
||||
"WalletConfig": {
|
||||
"LbryClaimAddress": "none"
|
||||
"DefaultChannel": "@speechDev"
|
||||
},
|
||||
"AnalyticsConfig":{
|
||||
"GoogleId": "UA-100747990-1"
|
||||
},
|
||||
"Database": {
|
||||
"MySqlConnectionUri": "none"
|
||||
},
|
||||
"Logging": {
|
||||
"LogLevel": "silly",
|
||||
"SlackErrorChannel": "#staging_speech-errors",
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -1,13 +1,10 @@
|
|||
{
|
||||
"WalletConfig": {
|
||||
"LbryClaimAddress": "none"
|
||||
"DefaultChannel": "@speech"
|
||||
},
|
||||
"AnalyticsConfig":{
|
||||
"GoogleId": "UA-60403362-3"
|
||||
},
|
||||
"Database": {
|
||||
"MySqlConnectionUri": "none"
|
||||
},
|
||||
"Logging": {
|
||||
"LogLevel": "verbose",
|
||||
"SlackErrorChannel": "#speech-errors",
|
||||
|
|
|
@ -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.');
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment `[file, claim] = result`
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment
|
||||
};
|
||||
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 => {
|
||||
|
|
|
@ -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
|
@ -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]}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
96
helpers/handlebarsHelpers.js
Normal 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),
|
||||
Why do these have to be in a helper instead of just in a normal template? (I know nothing about Handlebars -- you can just say it has to be this way if that's the case :) ) Why do these have to be in a helper instead of just in a normal template?
(I know nothing about Handlebars -- you can just say it has to be this way if that's the case :) )
They have to be in a helper so that they can be generated based on the proper google analytics key from the environmental vars They have to be in a helper so that they can be generated based on the proper google analytics key from the environmental vars
|
||||
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);
|
||||
}
|
||||
},
|
||||
};
|
|
@ -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);
|
||||
Should be a const somewhere Should be a const somewhere
|
||||
})
|
||||
.catch(error => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
`description.trim() === ''`
👍 :+1:
|
||||
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);
|
||||
});
|
||||
});
|
||||
},
|
||||
};
|
||||
|
|
79
migrations/UpdateAssociationColumns.js
Normal 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]);
|
||||
},
|
||||
};
|
46
migrations/UpdateUserAndChannel.js
Normal 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]);
|
||||
},
|
||||
};
|
|
@ -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
|
@ -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;
|
||||
};
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -52,6 +52,7 @@ module.exports = (sequelize, { STRING, BOOLEAN, INTEGER }) => {
|
|||
|
||||
File.associate = db => {
|
||||
File.hasMany(db.Request);
|
||||
File.hasOne(db.Claim);
|
||||
};
|
||||
|
||||
return 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
|
@ -0,0 +1,29 @@
|
|||
module.exports = (sequelize, { STRING }) => {
|
||||
const User = sequelize.define(
|
||||
'User',
|
||||
Is this a "User" or a "Channel"? I understand these have a 1-1 correspondence right now, but these are likely not the same thing as spee.ch continues to grow/evolve. I think it is likely that this should be two tables, a User table and a Channel table, with Channel having a foreign key to User. In addition to being more semantic, this would facilitate growth into a design where one user can access multiple channels from a single session/login without a tricky later refactor. Is this a "User" or a "Channel"? I understand these have a 1-1 correspondence right now, but these are likely not the same thing as spee.ch continues to grow/evolve.
I think it is likely that this should be two tables, a User table and a Channel table, with Channel having a foreign key to User.
In addition to being more semantic, this would facilitate growth into a design where one user can access multiple channels from a single session/login without a tricky later refactor.
per our conversation today, I am updating the db to include both User and Channel tables per our conversation today, I am updating the db to include both User and Channel tables
|
||||
{
|
||||
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;
|
||||
};
|
|
@ -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
|
@ -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
|
@ -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
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -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%;
|
||||
}
|
||||
|
||||
}
|
59
public/assets/css/mediaQueries.css
Normal 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%;
|
||||
}
|
||||
}
|
0
public/assets/css/reset.css
Normal file
4
public/assets/js/authFunctions.js
Normal file
|
@ -0,0 +1,4 @@
|
|||
Is this the only place we're making XMLHttpRequests in the app? If so, I suppose this is okay, if not, we might want to standardize this a bit. Is this the only place we're making XMLHttpRequests in the app? If so, I suppose this is okay, if not, we might want to standardize this a bit.
Is this the only place we're making XMLHttpRequests in the app? If so, I suppose this is okay, if not, we might want to standardize this a bit. Is this the only place we're making XMLHttpRequests in the app? If so, I suppose this is okay, if not, we might want to standardize this a bit.
|
||||
function sendAuthRequest (channelName, password, url) {
|
||||
Is this the only place we're making XMLHttpRequests in the app? If so, I suppose this is okay, if not, we might want to standardize this a bit. Is this the only place we're making XMLHttpRequests in the app? If so, I suppose this is okay, if not, we might want to standardize this a bit.
|
||||
const params = `username=${channelName}&password=${password}`;
|
||||
Is this the only place we're making XMLHttpRequests in the app? If so, I suppose this is okay, if not, we might want to standardize this a bit. Is this the only place we're making XMLHttpRequests in the app? If so, I suppose this is okay, if not, we might want to standardize this a bit.
|
||||
return postRequest(url, params);
|
||||
Is this the only place we're making XMLHttpRequests in the app? If so, I suppose this is okay, if not, we might want to standardize this a bit. Is this the only place we're making XMLHttpRequests in the app? If so, I suppose this is okay, if not, we might want to standardize this a bit.
|
||||
}
|
||||
Is this the only place we're making XMLHttpRequests in the app? If so, I suppose this is okay, if not, we might want to standardize this a bit. Is this the only place we're making XMLHttpRequests in the app? If so, I suppose this is okay, if not, we might want to standardize this a bit.
|
|
@ -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;
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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 = "✔";
|
||||
}
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
}
|
|
@ -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
|
@ -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);
|
||||
});
|
||||
};
|
|
@ -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');
|
||||
});
|
||||
};
|
||||
|
|
|
@ -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 => {
|
||||
|
|
|
@ -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
|
@ -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);
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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
|
@ -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>
|
|
@ -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'}}
|
||||
|
|
|
@ -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>
|
||||
|
|
78
views/partials/channelCreationForm.handlebars
Normal 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>
|
45
views/partials/channelLoginForm.handlebars
Normal 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>
|
|
@ -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>
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
<footer class="stop-float">
|
||||
<footer class="row">
|
||||
<p> thanks for visiting spee.ch </p>
|
||||
</footer>
|
|
@ -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>
|
|
@ -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">✔</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>
|
66
views/partials/publishForm-Channel.handlebars
Normal 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>
|
51
views/partials/publishForm-Details.handlebars
Normal 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>
|
||||
|
||||
|
||||
|
||||
|
13
views/partials/publishForm-Url.handlebars
Normal 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>
|
62
views/partials/publishForm.handlebars
Normal 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>
|
|
@ -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>
|
Great design here! Love that this makes it easy to use different channels on staging/dev and production as well as easy to change if forked.