diff --git a/README.md b/README.md index 54b75478..c6a7f6d2 100644 --- a/README.md +++ b/README.md @@ -28,28 +28,25 @@ spee.ch is a single-serving site that reads and publishes images and videos to a #### GET * /api/resolve/:name - * a successfull request returns the resolve results for the claim at that name in JSON format + * example: `curl https://spee.ch/api/resolve/doitlive` * /api/claim_list/:name - * a successfull request returns a list of claims at that claim name in JSON format -* /api/isClaimAvailable/:name - * a successfull request returns a boolean: `true` if the name is still available, `false` if the name has already been published to by spee.ch. + * example: `curl https://spee.ch/api/claim_list/doitlive` +* /api/isClaimAvailable/:name (returns `true`/`false` for whether a name is available through spee.ch) + * example: `curl https://spee.ch/api/isClaimAvailable/doitlive` #### POST * /api/publish - * request parameters: - * body (form-data): - * name: string (optional) - * defaults to the file's name, sans extension - * names can only contain the following characters: `A-Z`, `a-z`, `_`, or `-` - * license: string (optional) - * defaults to "No License Provided" - * only "Public Domain" or "Creative Commons" licenses are allowed - * nsfw: string, number, or boolean (optional) - * defaults `true` - * nsfw can be a string ("on"/"off"), number (0 or 1), or boolean (`true`/`false`) - * files: - * the `files` object submitted must use "speech" or "null" as the key for the file's value object - * a successfull request will return the transaction details resulting from your published claim in JSON format + * example: `curl -X POST -F 'name=MyPictureName' -F 'nsfw=false' -F 'file=@/path/to/my/picture.jpeg' https://spee.ch/api/publish` + * Parameters: + * name (string) + * nsfw (boolean) + * file (.mp4, .jpeg, .jpg, .gif, or .png) + * license (string, optional) + * title (string, optional) + * description (string, optional) + * thumbnail (string, optional) (for .mp4 uploads only) + * channelName(string, optional) + * channelPassword (string, optional) ## bugs If you find a bug or experience a problem, please report your issue here on github and find us in the lbry slack! diff --git a/auth/authentication.js b/auth/authentication.js index 6c6cf441..631aee66 100644 --- a/auth/authentication.js +++ b/auth/authentication.js @@ -2,21 +2,23 @@ const db = require('../models'); const logger = require('winston'); module.exports = { - authenticateApiPublish (username, password) { + authenticateChannelCredentials (channelName, userPassword) { return new Promise((resolve, reject) => { - if (username === 'none') { + if (!channelName) { resolve(true); return; } + const userName = channelName.substring(1); + logger.debug(`authenticateChannelCredentials > channelName: ${channelName} username: ${userName} pass: ${userPassword}`); db.User - .findOne({where: {userName: username}}) + .findOne({where: { userName }}) .then(user => { if (!user) { logger.debug('no user found'); resolve(false); return; } - if (!user.validPassword(password, user.password)) { + if (!user.validPassword(userPassword, user.password)) { logger.debug('incorrect password'); resolve(false); return; diff --git a/config/custom-environment-variables.json b/config/custom-environment-variables.json index 2ee10c42..78faebbf 100644 --- a/config/custom-environment-variables.json +++ b/config/custom-environment-variables.json @@ -8,5 +8,8 @@ }, "Logging": { "SlackWebHook": "SLACK_WEB_HOOK" + }, + "Session": { + "SessionKey": "SESSION_KEY" } } \ No newline at end of file diff --git a/config/default.json b/config/default.json index e094bc44..c5b65a20 100644 --- a/config/default.json +++ b/config/default.json @@ -16,5 +16,8 @@ "SlackWebHook": null, "SlackErrorChannel": null, "SlackInfoChannel": null + }, + "Session": { + "SessionKey": null } } \ No newline at end of file diff --git a/controllers/publishController.js b/controllers/publishController.js index 2ef07bdf..84c68750 100644 --- a/controllers/publishController.js +++ b/controllers/publishController.js @@ -7,24 +7,23 @@ module.exports = { publish (publishParams, fileName, fileType) { return new Promise((resolve, reject) => { let publishResults = {}; - // 1. make sure the name is available - publishHelpers.checkClaimNameAvailability(publishParams.name) - // 2. publish the file - .then(result => { - if (result === true) { - return lbryApi.publishClaim(publishParams); - } else { - 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) + // 1. publish the file + return lbryApi.publishClaim(publishParams) + // 2. upsert File record (update is in case the claim has been published before by this daemon) .then(tx => { logger.info(`Successfully published ${fileName}`, tx); publishResults = tx; - return db.Channel.findOne({where: {channelName: publishParams.channel_name}}); + return db.Channel.findOne({where: {channelName: publishParams.channel_name}}); // note: should this be db.User ?? }) - .then(user => { - if (user) { logger.debug('successfully found user in User table') } else { logger.error('user for publish not found in User table') }; + .then(channel => { + let certificateId; + if (channel) { + certificateId = channel.channelClaimId; + logger.debug('successfully found channel in Channel table'); + } else { + certificateId = null; + logger.debug('channel for publish not found in Channel table'); + }; const fileRecord = { name : publishParams.name, claimId : publishResults.claim_id, @@ -39,17 +38,18 @@ module.exports = { nsfw : publishParams.metadata.nsfw, }; 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, + name : publishParams.name, + claimId : publishResults.claim_id, + title : publishParams.metadata.title, + description: publishParams.metadata.description, + address : publishParams.claim_address, + thumbnail : publishParams.metadata.thumbnail, + outpoint : `${publishResults.txid}:${publishResults.nout}`, + height : 0, + contentType: fileType, + nsfw : publishParams.metadata.nsfw, + certificateId, + amount : publishParams.bid, }; const upsertCriteria = { name : publishParams.name, @@ -67,6 +67,7 @@ module.exports = { resolve(publishResults); // resolve the promise with the result from lbryApi.publishClaim; }) .catch(error => { + logger.error('publishController.publish, error', error); publishHelpers.deleteTemporaryFile(publishParams.file_path); // delete the local file reject(error); }); diff --git a/controllers/serveController.js b/controllers/serveController.js index be7de126..16d5ac67 100644 --- a/controllers/serveController.js +++ b/controllers/serveController.js @@ -7,7 +7,9 @@ const { postToStats, sendGoogleAnalytics } = require('../controllers/statsContro const SERVE = 'SERVE'; const SHOW = 'SHOW'; const SHOWLITE = 'SHOWLITE'; -const DEFAULT_THUMBNAIL = 'https://spee.ch/assets/img/content-freedom-large.png'; +const DEFAULT_THUMBNAIL = 'https://spee.ch/assets/img/video_thumb_default.png'; +const NO_CHANNEL = 'NO_CHANNEL'; +const NO_CLAIM = 'NO_CLAIM'; function checkForLocalAssetByClaimId (claimId, name) { return new Promise((resolve, reject) => { @@ -56,7 +58,8 @@ function getAssetByLongClaimId (fullClaimId, name) { // if a result was found, return early with the result if (dataValues) { logger.debug('found a local file for this name and claimId'); - return resolve(dataValues); + resolve(dataValues); + return; } logger.debug('no local file found for this name and claimId'); // 2. if no local claim, resolve and get the claim @@ -78,7 +81,7 @@ function getAssetByLongClaimId (fullClaimId, name) { // insert a record in the File table & Update Claim table return db.File.create(fileRecord); }) - .then(fileRecordResults => { + .then(() => { logger.debug('File record successfully updated'); resolve(fileRecord); }) @@ -105,15 +108,17 @@ function chooseThumbnail (claimInfo, defaultThumbnail) { module.exports = { getAssetByClaim (claimName, claimId) { - logger.debug('getting asset by claim'); + logger.debug(`getAssetByClaim(${claimName}, ${claimId})`); return new Promise((resolve, reject) => { - // 1. get the long claim id - db - .getLongClaimId(claimName, claimId) - // 2. get the claim Id - .then(longClaimId => { - logger.debug('long claim id = ', longClaimId); - resolve(getAssetByLongClaimId(longClaimId, claimName)); + db.getLongClaimId(claimName, claimId) // 1. get the long claim id + .then(result => { // 2. get the asset using the long claim id + logger.debug('getLongClaimId result:', result); + if (result === NO_CLAIM) { + logger.debug('resolving NO_CLAIM'); + resolve(NO_CLAIM); + return; + } + resolve(getAssetByLongClaimId(result, claimName)); }) .catch(error => { reject(error); @@ -123,17 +128,21 @@ module.exports = { getAssetByChannel (channelName, channelId, claimName) { logger.debug('getting asset by channel'); return new Promise((resolve, reject) => { - // 1. get the long channel id - db - .getLongChannelId(channelName, channelId) - // 2. get the claim Id - .then(longChannelId => { - return db.getClaimIdByLongChannelId(longChannelId, claimName); + db.getLongChannelId(channelName, channelId) // 1. get the long channel id + .then(result => { // 2. get the long claim Id + if (result === NO_CHANNEL) { + resolve(NO_CHANNEL); + return; + } + return db.getClaimIdByLongChannelId(result, claimName); }) - // 3. get the asset by this claim id and name - .then(claimId => { - logger.debug('asset claim id = ', claimId); - resolve(getAssetByLongClaimId(claimId, claimName)); + .then(result => { // 3. get the asset using the long claim id + logger.debug('asset claim id =', result); + if (result === NO_CHANNEL || result === NO_CLAIM) { + resolve(result); + return; + } + resolve(getAssetByLongClaimId(result, claimName)); }) .catch(error => { reject(error); @@ -144,35 +153,41 @@ module.exports = { return new Promise((resolve, reject) => { let longChannelId; let shortChannelId; - // 1. get the long channel Id - db - .getLongChannelId(channelName, channelId) - // 2. get all claims for that channel - .then(result => { + db.getLongChannelId(channelName, channelId) // 1. get the long channel Id + .then(result => { // 2. get all claims for that channel + if (result === NO_CHANNEL) { + return NO_CHANNEL; + } longChannelId = result; return db.getShortChannelIdFromLongChannelId(longChannelId, channelName); }) - // 3. get all Claim records for this channel - .then(result => { + .then(result => { // 3. get all Claim records for this channel + if (result === NO_CHANNEL) { + return NO_CHANNEL; + } shortChannelId = result; return db.getAllChannelClaims(longChannelId); }) - // 4. add extra data not available from Claim table - .then(allChannelClaims => { - if (allChannelClaims) { - allChannelClaims.forEach(element => { + .then(result => { // 4. add extra data not available from Claim table + if (result === NO_CHANNEL) { + resolve(NO_CHANNEL); + return; + } + if (result) { + result.forEach(element => { const fileExtenstion = element.contentType.substring(element.contentType.lastIndexOf('/') + 1); element['showUrlLong'] = `/${channelName}:${longChannelId}/${element.name}`; element['directUrlLong'] = `/${channelName}:${longChannelId}/${element.name}.${fileExtenstion}`; + element['showUrlShort'] = `/${channelName}:${shortChannelId}/${element.name}`; element['directUrlShort'] = `/${channelName}:${shortChannelId}/${element.name}.${fileExtenstion}`; element['thumbnail'] = chooseThumbnail(element, DEFAULT_THUMBNAIL); }); } - return resolve({ + resolve({ channelName, longChannelId, shortChannelId, - claims: allChannelClaims, + claims: result, }); }) .catch(error => { @@ -206,9 +221,12 @@ module.exports = { return db.resolveClaim(fileInfo.name, fileInfo.claimId); }) .then(resolveResult => { + logger.debug('resolve result >>', resolveResult); fileInfo['thumbnail'] = chooseThumbnail(resolveResult, DEFAULT_THUMBNAIL); fileInfo['title'] = resolveResult.title; fileInfo['description'] = resolveResult.description; + if (resolveResult.certificateId) { fileInfo['certificateId'] = resolveResult.certificateId }; + if (resolveResult.channelName) { fileInfo['channelName'] = resolveResult.channelName }; showFile(fileInfo, res); return fileInfo; }) diff --git a/controllers/statsController.js b/controllers/statsController.js index 3ee05e77..b728ede6 100644 --- a/controllers/statsController.js +++ b/controllers/statsController.js @@ -69,79 +69,6 @@ module.exports = { } }); }, - getStatsSummary (startDate) { - logger.debug('retrieving request records'); - return new Promise((resolve, reject) => { - // get the raw Requests data - db.Request - .findAll({ - where: { - createdAt: { - gt: startDate, - }, - }, - }) - .then(data => { - let resultHashTable = {}; - let totalServe = 0; - let totalPublish = 0; - let totalShow = 0; - let totalCount = 0; - let totalSuccess = 0; - let totalFailure = 0; - let percentSuccess; - // summarise the data - for (let i = 0; i < data.length; i++) { - let key = data[i].action + data[i].url; - totalCount += 1; - switch (data[i].action) { - case 'SERVE': - totalServe += 1; - break; - case 'PUBLISH': - totalPublish += 1; - break; - case 'SHOW': - totalShow += 1; - break; - default: break; - } - if (resultHashTable[key]) { - resultHashTable[key]['count'] += 1; - if (data[i].result === 'success') { - resultHashTable[key]['success'] += 1; - totalSuccess += 1; - } else { - resultHashTable[key]['failure'] += 1; - totalFailure += 1; - } - } else { - resultHashTable[key] = { - action : data[i].action, - url : data[i].url, - count : 1, - success: 0, - failure: 0, - }; - if (data[i].result === 'success') { - resultHashTable[key]['success'] += 1; - totalSuccess += 1; - } else { - resultHashTable[key]['failure'] += 1; - totalFailure += 1; - } - } - } - percentSuccess = Math.round(totalSuccess / totalCount * 100); - // return results - resolve({ records: resultHashTable, totals: { totalServe, totalPublish, totalShow, totalCount, totalSuccess, totalFailure }, percentSuccess }); - }) - .catch(error => { - logger.error('sequelize error >>', error); - reject(error); - }); - }); - }, getTrendingClaims (startDate) { logger.debug('retrieving trending requests'); return new Promise((resolve, reject) => { @@ -155,7 +82,7 @@ module.exports = { element['directUrlLong'] = `/${element.claimId}/${element.name}.${fileExtenstion}`; element['directUrlShort'] = `/${element.claimId}/${element.name}.${fileExtenstion}`; element['contentType'] = element.fileType; - element['thumbnail'] = 'https://spee.ch/assets/img/content-freedom-large.png'; + element['thumbnail'] = 'https://spee.ch/assets/img/video_thumb_default.png'; }); } resolve(results); diff --git a/helpers/authHelpers.js b/helpers/authHelpers.js new file mode 100644 index 00000000..92066cbd --- /dev/null +++ b/helpers/authHelpers.js @@ -0,0 +1,44 @@ +const db = require('../models'); // require our models for syncing +const logger = require('winston'); + +module.exports = { + populateLocalsDotUser (req, res, next) { + if (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(); + }, + serializeSpeechUser (user, done) { + done(null, user.id); + }, + deserializeSpeechUser (id, done) { + 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; + // return done(null, userInfo); + done(null, userInfo); + return null; + }) + .catch(error => { + logger.error(error); + done(error, null); + }); + }, +}; diff --git a/helpers/errorHandlers.js b/helpers/errorHandlers.js index d1ac23cc..b127cd90 100644 --- a/helpers/errorHandlers.js +++ b/helpers/errorHandlers.js @@ -1,20 +1,9 @@ const logger = require('winston'); const { postToStats } = require('../controllers/statsController.js'); -function useObjectPropertiesIfNoKeys (err) { - if (Object.keys(err).length === 0) { - let newErrorObject = {}; - Object.getOwnPropertyNames(err).forEach((key) => { - newErrorObject[key] = err[key]; - }); - return newErrorObject; - } - return err; -} - module.exports = { handleRequestError (action, originalUrl, ip, error, res) { - logger.error('Request Error:', useObjectPropertiesIfNoKeys(error)); + logger.error(`Request Error: ${originalUrl}`, module.exports.useObjectPropertiesIfNoKeys(error)); postToStats(action, originalUrl, ip, null, null, error); if (error.response) { res.status(error.response.status).send(error.response.data.error.message); @@ -27,13 +16,31 @@ module.exports = { } }, handlePublishError (error) { - logger.error('Publish Error:', useObjectPropertiesIfNoKeys(error)); + logger.error('Publish Error:', module.exports.useObjectPropertiesIfNoKeys(error)); if (error.code === 'ECONNREFUSED') { return 'Connection refused. The daemon may not be running.'; - } else if (error.response.data.error) { - return error.response.data.error.message; + } else if (error.response) { + if (error.response.data) { + if (error.response.data.message) { + return error.response.data.message; + } else if (error.response.data.error) { + return error.response.data.error.message; + } + return error.response.data; + } + return error.response; } else { return error; } }, + useObjectPropertiesIfNoKeys (err) { + if (Object.keys(err).length === 0) { + let newErrorObject = {}; + Object.getOwnPropertyNames(err).forEach((key) => { + newErrorObject[key] = err[key]; + }); + return newErrorObject; + } + return err; + }, }; diff --git a/helpers/handlebarsHelpers.js b/helpers/handlebarsHelpers.js index 771cdfae..b4fb3c42 100644 --- a/helpers/handlebarsHelpers.js +++ b/helpers/handlebarsHelpers.js @@ -51,7 +51,7 @@ module.exports = { } }, addTwitterCard (mimeType, source, embedUrl, directFileUrl) { - let basicTwitterTags = ``; + let basicTwitterTags = ``; if (mimeType === 'video/mp4') { return new Handlebars.SafeString( `${basicTwitterTags} diff --git a/helpers/lbryApi.js b/helpers/lbryApi.js index 4c758690..e342a0eb 100644 --- a/helpers/lbryApi.js +++ b/helpers/lbryApi.js @@ -2,14 +2,14 @@ const axios = require('axios'); const logger = require('winston'); function handleResponse ({ data }, resolve, reject) { - logger.debug('handling lbry api response'); + 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); + // logger.debug('data.result', data.result); resolve(data.result); return; } @@ -118,9 +118,11 @@ module.exports = { }, }) .then(response => { + logger.verbose('createChannel response:', response); handleResponse(response, resolve, reject); }) .catch(error => { + logger.error('createChannel error:', error); reject(error); }); }); diff --git a/helpers/publishHelpers.js b/helpers/publishHelpers.js index 2db37d46..1ba54ddc 100644 --- a/helpers/publishHelpers.js +++ b/helpers/publishHelpers.js @@ -1,77 +1,129 @@ const logger = require('winston'); -const config = require('config'); const fs = require('fs'); const db = require('../models'); +const config = require('config'); module.exports = { - validateFile (file, name, license, nsfw) { + validateApiPublishRequest (body, files) { + if (!body) { + throw new Error('no body found in request'); + } + if (!body.name) { + throw new Error('no name field found in request'); + } + if (!body.nsfw) { + throw new Error('no nsfw field found in request'); + } + if (!files) { + throw new Error('no files found in request'); + } + if (!files.file) { + throw new Error('no file with key of [file] found in request'); + } + }, + validatePublishSubmission (file, claimName, nsfw) { + try { + module.exports.validateFile(file); + module.exports.validateClaimName(claimName); + module.exports.validateNSFW(nsfw); + } catch (error) { + throw error; + } + }, + validateFile (file) { if (!file) { - throw new Error('No file was submitted or the key used was incorrect. Files posted through this route must use a key of "speech" or null'); + logger.debug('publish > file validation > no file found'); + throw new Error('no file provided'); + } + // check the file name + if (/'/.test(file.name)) { + logger.debug('publish > file validation > file name had apostrophe in it'); + throw new Error('apostrophes are not allowed in the file name'); } // check file type and size switch (file.type) { case 'image/jpeg': + case 'image/jpg': case 'image/png': + if (file.size > 10000000) { + logger.debug('publish > file validation > .jpeg/.jpg/.png was too big'); + throw new Error('Sorry, images are limited to 10 megabytes.'); + } + break; case 'image/gif': if (file.size > 50000000) { - throw new Error('Your image exceeds the 50 megabyte limit.'); + logger.debug('publish > file validation > .gif was too big'); + throw new Error('Sorry, .gifs are limited to 50 megabytes.'); } break; case 'video/mp4': if (file.size > 50000000) { - throw new Error('Your video exceeds the 50 megabyte limit.'); + logger.debug('publish > file validation > .mp4 was too big'); + throw new Error('Sorry, videos are limited to 50 megabytes.'); } break; default: - throw new Error('The ' + file.Type + ' content type is not supported. Only, .jpeg, .png, .gif, and .mp4 files are currently supported.'); + logger.debug('publish > file validation > unrecognized file type'); + throw new Error('The ' + file.type + ' content type is not supported. Only, .jpeg, .png, .gif, and .mp4 files are currently supported.'); } - // validate claim name - const invalidCharacters = /[^A-Za-z0-9,-]/.exec(name); + return file; + }, + validateClaimName (claimName) { + const invalidCharacters = /[^A-Za-z0-9,-]/.exec(claimName); if (invalidCharacters) { - throw new Error('The url name you provided is not allowed. Only the following characters are allowed: A-Z, a-z, 0-9, and "-"'); + throw new Error('The claim name you provided is not allowed. Only the following characters are allowed: A-Z, a-z, 0-9, and "-"'); } - // validate license + }, + validateLicense (license) { 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'); + throw new Error('Only posts with a "Public Domain" or "Creative Commons" license are eligible for publishing through spee.ch'); } + }, + cleanseNSFW (nsfw) { switch (nsfw) { case true: - case false: - case 'true': - case 'false': case 'on': + case 'true': + case 1: + case '1': + return true; + case false: + case 'false': case 'off': case 0: case '0': - case 1: - case '1': - break; + return false; default: - throw new Error('NSFW value was not accepted. NSFW must be set to either true, false, "on", or "off"'); + return null; } }, - 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 (typeof nsfw === 'string') { - if (nsfw.toLowerCase === 'false' || nsfw.toLowerCase === 'off' || nsfw === '0') { - nsfw = false; + cleanseChannelName (channelName) { + if (channelName) { + if (channelName.indexOf('@') !== 0) { + channelName = `@${channelName}`; } - } else if (nsfw === 0) { - nsfw = false; - } else { - nsfw = true; } - // provide defaults for title & description - if (title === null || title === '') { + return channelName; + }, + validateNSFW (nsfw) { + if (nsfw === true || nsfw === false) { + return; + } + throw new Error('NSFW must be set to either true or false'); + }, + createPublishParams (filePath, name, title, description, license, nsfw, thumbnail, channelName) { + logger.debug(`Creating Publish Parameters`); + // provide defaults for title + if (title === null || title.trim() === '') { title = name; } + // provide default for description if (description === null || description.trim() === '') { - description = `${name} published via spee.ch`; + description = ''; + } + // provide default for license + if (license === null || license.trim() === '') { + license = ' '; // default to empty string } // create the publish params const publishParams = { @@ -86,21 +138,24 @@ module.exports = { license, nsfw, }, - claim_address: claimAddress, + claim_address: config.get('WalletConfig.LbryClaimAddress'), }; - // add channel if applicable - if (channel !== 'none') { - publishParams['channel_name'] = channel; - } else { - publishParams['channel_name'] = defaultChannel; + // add thumbnail to channel if video + if (thumbnail !== null) { + publishParams['metadata']['thumbnail'] = thumbnail; + } + // add channel to params, if applicable + if (channelName) { + publishParams['channel_name'] = channelName; } - - logger.debug('publishParams:', publishParams); return publishParams; }, deleteTemporaryFile (filePath) { fs.unlink(filePath, err => { - if (err) throw err; + if (err) { + logger.error(`error deleting temporary file ${filePath}`); + throw err; + } logger.debug(`successfully deleted ${filePath}`); }); }, diff --git a/helpers/serveHelpers.js b/helpers/serveHelpers.js index d5dbdaf1..6e7c610a 100644 --- a/helpers/serveHelpers.js +++ b/helpers/serveHelpers.js @@ -4,8 +4,8 @@ function createOpenGraphInfo ({ fileType, claimId, name, fileName, fileExt }) { return { embedUrl : `https://spee.ch/embed/${claimId}/${name}`, showUrl : `https://spee.ch/${claimId}/${name}`, - source : `https://spee.ch/${claimId}/${name}${fileExt}`, - directFileUrl: `https://spee.ch/media/${fileName}`, + source : `https://spee.ch/${claimId}/${name}.${fileExt}`, + directFileUrl: `https://spee.ch/${claimId}/${name}.${fileExt}`, }; } @@ -40,6 +40,6 @@ module.exports = { }, showFileLite (fileInfo, res) { const openGraphInfo = createOpenGraphInfo(fileInfo); - res.status(200).render('showLite', { layout: 'show', fileInfo, openGraphInfo }); + res.status(200).render('showLite', { layout: 'showlite', fileInfo, openGraphInfo }); }, }; diff --git a/migrations/AddChannelNameToClaim.js b/migrations/AddChannelNameToClaim.js new file mode 100644 index 00000000..b9ac1da8 --- /dev/null +++ b/migrations/AddChannelNameToClaim.js @@ -0,0 +1,22 @@ +module.exports = { + up: (queryInterface, Sequelize) => { + // logic for transforming into the new state + const p1 = queryInterface.addColumn( + 'Claim', + 'channelName', + { + type : Sequelize.STRING, + allowNull: true, + } + ); + return Promise.all([p1]); + }, + down: (queryInterface, Sequelize) => { + // logic for reverting the changes + const p1 = queryInterface.removeColumn( + 'Claim', + 'channelName' + ); + return Promise.all([p1]); + }, +}; diff --git a/migrations/UpdateAssociationColumns.js b/migrations/UpdateAssociationColumns.js deleted file mode 100644 index 22c6def9..00000000 --- a/migrations/UpdateAssociationColumns.js +++ /dev/null @@ -1,79 +0,0 @@ -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]); - }, -}; diff --git a/migrations/UpdateUserAndChannel.js b/migrations/UpdateUserAndChannel.js deleted file mode 100644 index 32c22097..00000000 --- a/migrations/UpdateUserAndChannel.js +++ /dev/null @@ -1,46 +0,0 @@ -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]); - }, -}; diff --git a/migrations/UpdateUserPasswords5.js b/migrations/UpdateUserPasswords5.js new file mode 100644 index 00000000..f911c86e --- /dev/null +++ b/migrations/UpdateUserPasswords5.js @@ -0,0 +1,46 @@ +const db = require('../models'); +const bcrypt = require('bcrypt'); +const logger = require('winston'); + +module.exports = { + up: (queryInterface, Sequelize) => { + // get all the users + return db.User + .findAll() + .then((users) => { + // create an array of promises, with each promise bcrypting a password and updating the record + const promises = users.map((record) => { + // bcrypt + // generate a salt string to use for hashing + return new Promise((resolve, reject) => { + bcrypt.genSalt((saltError, salt) => { + if (saltError) { + logger.error('salt error', saltError); + reject(saltError); + return; + } + // generate a hashed version of the user's password + bcrypt.hash(record.password, salt, (hashError, hash) => { + // if there is an error with the hash generation return the error + if (hashError) { + logger.error('hash error', hashError); + reject(hashError); + return; + } + // replace the password string with the hash password value + resolve(queryInterface.sequelize.query(`UPDATE User SET User.password = "${hash}" WHERE User.id = ${record.id}`)); + }); + }); + }); + }); + // return the array of promises + return Promise.all(promises); + }) + .catch(error => { + logger.error('error prepping promises array', error); + }); + }, + down: (queryInterface, Sequelize) => { + // logic for reverting the changes + }, +}; diff --git a/models/claim.js b/models/claim.js index ab35576b..804bb284 100644 --- a/models/claim.js +++ b/models/claim.js @@ -134,6 +134,11 @@ module.exports = (sequelize, { STRING, BOOLEAN, INTEGER, TEXT, ARRAY, DECIMAL, D type : STRING, default: null, }, + channelName: { + type : STRING, + allowNull: true, + default : null, + }, }, { freezeTableName: true, diff --git a/models/index.js b/models/index.js index b330fbd4..6fccf574 100644 --- a/models/index.js +++ b/models/index.js @@ -6,6 +6,9 @@ const config = require('config'); const db = {}; const logger = require('winston'); +const NO_CHANNEL = 'NO_CHANNEL'; +const NO_CLAIM = 'NO_CLAIM'; + const database = config.get('Database.Database'); const username = config.get('Database.Username'); const password = config.get('Database.Password'); @@ -52,7 +55,7 @@ function getLongClaimIdFromShortClaimId (name, shortId) { .then(result => { switch (result.length) { case 0: - throw new Error('That is an invalid Short Claim Id'); + return resolve(NO_CLAIM); default: // note results must be sorted return resolve(result[0].claimId); } @@ -68,9 +71,10 @@ function getTopFreeClaimIdByClaimName (name) { db .sequelize.query(`SELECT claimId FROM Claim WHERE name = '${name}' ORDER BY effectiveAmount DESC, height ASC LIMIT 1`, { type: db.sequelize.QueryTypes.SELECT }) .then(result => { + logger.debug('getTopFreeClaimIdByClaimName result:', result); switch (result.length) { case 0: - return resolve(null); + return resolve(NO_CLAIM); default: return resolve(result[0].claimId); } @@ -88,7 +92,7 @@ function getLongChannelIdFromShortChannelId (channelName, channelId) { .then(result => { switch (result.length) { case 0: - throw new Error('That is an invalid Short Channel Id'); + return resolve(NO_CHANNEL); default: // note results must be sorted return resolve(result[0].claimId); } @@ -100,13 +104,14 @@ function getLongChannelIdFromShortChannelId (channelName, channelId) { } function getLongChannelIdFromChannelName (channelName) { + logger.debug(`getLongChannelIdFromChannelName(${channelName})`); return new Promise((resolve, reject) => { db - .sequelize.query(`SELECT claimId, amount, height FROM Certificate WHERE name = '${channelName}' ORDER BY amount DESC, height ASC LIMIT 1;`, { type: db.sequelize.QueryTypes.SELECT }) + .sequelize.query(`SELECT claimId, amount, height FROM Certificate WHERE name = '${channelName}' ORDER BY effectiveAmount DESC, height ASC LIMIT 1;`, { type: db.sequelize.QueryTypes.SELECT }) .then(result => { switch (result.length) { case 0: - throw new Error('That is an invalid Channel Name'); + return resolve(NO_CHANNEL); default: return resolve(result[0].claimId); } @@ -230,7 +235,7 @@ db['getAllFreeClaims'] = (name) => { db['resolveClaim'] = (name, claimId) => { return new Promise((resolve, reject) => { db - .sequelize.query(`SELECT name, claimId, outpoint, height, address, title, description, thumbnail FROM Claim WHERE name = '${name}' AND claimId = '${claimId}'`, { type: db.sequelize.QueryTypes.SELECT }) + .sequelize.query(`SELECT name, claimId, outpoint, height, address, title, description, thumbnail, certificateId, channelName FROM Claim WHERE name = '${name}' AND claimId = '${claimId}'`, { type: db.sequelize.QueryTypes.SELECT }) .then(result => { switch (result.length) { case 0: @@ -255,7 +260,7 @@ db['getClaimIdByLongChannelId'] = (channelId, claimName) => { .then(result => { switch (result.length) { case 0: - throw new Error('There is no such claim for that channel'); + return resolve(NO_CLAIM); default: return resolve(result[0].claimId); } @@ -270,7 +275,7 @@ db['getAllChannelClaims'] = (channelId) => { return new Promise((resolve, reject) => { logger.debug(`finding all claims in channel "${channelId}"`); db - .sequelize.query(`SELECT name, claimId, outpoint, height, address, contentType, title, description, license, thumbnail FROM Claim WHERE certificateId = '${channelId}' ORDeR BY height DESC;`, { type: db.sequelize.QueryTypes.SELECT }) + .sequelize.query(`SELECT name, claimId, outpoint, height, address, contentType, title, description, license, thumbnail FROM Claim WHERE certificateId = '${channelId}' ORDER BY height DESC;`, { type: db.sequelize.QueryTypes.SELECT }) .then(result => { switch (result.length) { case 0: @@ -286,22 +291,24 @@ db['getAllChannelClaims'] = (channelId) => { }; db['getLongClaimId'] = (claimName, claimId) => { - if (claimId && (claimId.length === 40)) { + logger.debug(`getLongClaimId(${claimName}, ${claimId})`); + if (claimId && (claimId.length === 40)) { // if a full claim id is provided return new Promise((resolve, reject) => resolve(claimId)); } else if (claimId && claimId.length < 40) { - return getLongClaimIdFromShortClaimId(claimName, claimId); // need to create this function - } else { // if no claim id provided - return getTopFreeClaimIdByClaimName(claimName); + return getLongClaimIdFromShortClaimId(claimName, claimId); // if a short claim id is provided + } else { + return getTopFreeClaimIdByClaimName(claimName); // if no claim id is provided } }; db['getLongChannelId'] = (channelName, channelId) => { - if (channelId && (channelId.length === 40)) { // full channel id + logger.debug(`getLongChannelId (${channelName}, ${channelId})`); + if (channelId && (channelId.length === 40)) { // if a full channel id is provided return new Promise((resolve, reject) => resolve(channelId)); - } else if (channelId && channelId.length < 40) { // short channel id + } else if (channelId && channelId.length < 40) { // if a short channel id is provided return getLongChannelIdFromShortChannelId(channelName, channelId); } else { - return getLongChannelIdFromChannelName(channelName); + return getLongChannelIdFromChannelName(channelName); // if no channel id provided } }; diff --git a/models/user.js b/models/user.js index bce93d91..ddad3524 100644 --- a/models/user.js +++ b/models/user.js @@ -1,3 +1,7 @@ +'use strict'; +const bcrypt = require('bcrypt'); +const logger = require('winston'); + module.exports = (sequelize, { STRING }) => { const User = sequelize.define( 'User', @@ -20,10 +24,37 @@ module.exports = (sequelize, { STRING }) => { User.hasOne(db.Channel); }; - User.prototype.validPassword = (givenpassword, thispassword) => { - console.log(`${givenpassword} === ${thispassword}`); - return (givenpassword === thispassword); + User.prototype.comparePassword = function (password, callback) { + logger.debug(`User.prototype.comparePassword ${password} ${this.password}`); + bcrypt.compare(password, this.password, callback); }; + // pre-save hook method to hash the user's password before the user's info is saved to the db. + User.hook('beforeCreate', (user, options) => { + logger.debug('...beforeCreate hook...'); + return new Promise((resolve, reject) => { + // generate a salt string to use for hashing + bcrypt.genSalt((saltError, salt) => { + if (saltError) { + logger.error('salt error', saltError); + reject(saltError); + return; + } + // generate a hashed version of the user's password + bcrypt.hash(user.password, salt, (hashError, hash) => { + // if there is an error with the hash generation return the error + if (hashError) { + logger.error('hash error', hashError); + reject(hashError); + return; + } + // replace the password string with the hash password value + user.password = hash; + resolve(); + }); + }); + }); + }); + return User; }; diff --git a/package.json b/package.json index e1aa4a85..f9acff82 100644 --- a/package.json +++ b/package.json @@ -27,12 +27,15 @@ "homepage": "https://github.com/lbryio/spee.ch#readme", "dependencies": { "axios": "^0.16.1", + "bcrypt": "^1.0.3", "body-parser": "^1.17.1", "config": "^1.26.1", "connect-multiparty": "^2.0.0", + "cookie-session": "^2.0.0-beta.3", "express": "^4.15.2", "express-handlebars": "^3.0.0", "express-session": "^1.15.5", + "form-data": "^2.3.1", "helmet": "^3.8.1", "mysql2": "^1.3.5", "nodemon": "^1.11.0", diff --git a/passport/local-login.js b/passport/local-login.js index 5ccb822c..f83dcc0e 100644 --- a/passport/local-login.js +++ b/passport/local-login.js @@ -1,3 +1,4 @@ + const PassportLocalStrategy = require('passport-local').Strategy; const db = require('../models'); const logger = require('winston'); @@ -11,6 +12,7 @@ module.exports = new PassportLocalStrategy( }, (req, username, password, done) => { logger.debug(`verifying loggin attempt ${username} ${password}`); + let userInfo = {}; return db.User .findOne({where: {userName: username}}) .then(user => { @@ -18,13 +20,35 @@ module.exports = new PassportLocalStrategy( 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); + logger.debug('...comparing password...'); + return user.comparePassword(password, (passwordErr, isMatch) => { + if (passwordErr) { + logger.error('passwordErr:', passwordErr); + return done(passwordErr); + } + + if (!isMatch) { + logger.debug('incorrect password'); + return done(null, false, {message: 'Incorrect username or password.'}); + } + logger.debug('...password was a match...'); + userInfo['id'] = user.id; + userInfo['userName'] = user.userName; + // get the User's channel info + 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; + return done(null, userInfo); + }) + .catch(error => { + throw error; + }); }); }) .catch(error => { diff --git a/passport/local-signup.js b/passport/local-signup.js index 23273180..177f388c 100644 --- a/passport/local-signup.js +++ b/passport/local-signup.js @@ -11,8 +11,8 @@ module.exports = new PassportLocalStrategy( 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; + logger.verbose(`new channel signup request. user: ${username} pass: ${password} .`); + let userInfo = {}; // server-side validaton of inputs (username, password) // create the channel and retrieve the metadata @@ -23,34 +23,42 @@ module.exports = new PassportLocalStrategy( userName: username, password: password, }; - logger.debug('userData >', userData); + logger.verbose('userData >', userData); // create user record const channelData = { channelName : `@${username}`, channelClaimId: tx.claim_id, }; - logger.debug('channelData >', channelData); + logger.verbose('channelData >', channelData); // create certificate record const certificateData = { claimId: tx.claim_id, name : `@${username}`, // address, }; - logger.debug('certificateData >', certificateData); + logger.verbose('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.verbose('user and certificate successfully created'); logger.debug('user result >', newUser.dataValues); - logger.debug('user result >', newChannel.dataValues); - logger.debug('certificate result >', newCertificate.dataValues); + userInfo['id'] = newUser.id; + userInfo['userName'] = newUser.userName; + logger.verbose('channel result >', newChannel.dataValues); + userInfo['channelName'] = newChannel.channelName; + userInfo['channelClaimId'] = newChannel.channelClaimId; + logger.verbose('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); + }) + .then(() => { + logger.verbose('user and certificate successfully associated'); + return db.getShortChannelIdFromLongChannelId(userInfo.channelClaimId, userInfo.channelName); + }) + .then(shortChannelId => { + userInfo['shortChannelId'] = shortChannelId; + return done(null, userInfo); }) .catch(error => { logger.error('signup error', error); diff --git a/public/assets/css/BEM.css b/public/assets/css/BEM.css deleted file mode 100644 index dfa8262e..00000000 --- a/public/assets/css/BEM.css +++ /dev/null @@ -1,276 +0,0 @@ - -/* 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; -} \ No newline at end of file diff --git a/public/assets/css/componentStyle.css b/public/assets/css/componentStyle.css deleted file mode 100644 index 9c043422..00000000 --- a/public/assets/css/componentStyle.css +++ /dev/null @@ -1,185 +0,0 @@ - -/* top bar */ -#logo, #title { - float: left; -} - -#logo { - height: 1.5em; -} - -#title { - margin: 2px 5px 2px 5px; -} - -.top-bar-left { - float: left; -} - -.top-bar-tagline { - font-style: italic; - color: grey; -} - -.top-bar-right { - margin-left: 0.5em; -} - -/* publish */ -#drop-zone { - border: 1px dashed lightgrey; - padding: 1em; - height: 13em; - background: #F5F0EF; -} - -#asset-preview-holder { - width: 100%; - margin-bottom: 1em; -} - -/* show routes */ -.show-asset { - width: 100%; - margin-bottom: 1em; - margin-top: 1em; -} - -.show-asset-lite { - margin: 0px; -} - -.panel.links { - font-size: small; -} - -input.link { - width: 80%; -} - -button.copy-button { - padding: 4px; - float: right; -} - -.share-option { - margin-bottom: 1em; -} - -.metadata-table { - font-size: small; - border-collapse: collapse; - margin-bottom: 1em; -} - -.metadata-row { - border-bottom: 1px solid lightgrey; - margin: 2px; -} - -.left-column { - width: 30%; - font-weight: bold; - vertical-align: top; -} - -/* trending claims */ -.grid-item { - width: 23%; - margin: 0px 1% 20px 1%; -} - -/* learn more */ -.learn-more { - text-align: center; - border-top: 1px solid lightgrey; -} - -/* examples */ -.example { - clear: both; - width: 100%; - margin-bottom: 15px; - overflow: auto; -} - -.example-image, .example-code { - float: left; - margin: 2%; -} - -.example-image { - width: 21%; -} - -.example-code { - float: right; - padding: 4%; - width: 62%; - background-color: lightgrey; - font-family: monospace; - color: #666; - word-wrap: break-word; -} - -/* contribute */ -#github-logo { - float: right; - height: 1em; -} - -/* content lists */ -.content-list-card { - margin-top: 2px; - padding-top: 2px; - border-top: 1px lightgrey solid; - overflow: auto; - position: relative; -} - -.content-list-card-link { - position:absolute; - width:100%; - height:100%; - top:0; - left: 0; - z-index: 1; -} - -.content-list-asset { - width: 20%; - float: left; - margin: 5px 30px 5px 0px; -} - -.content-list-title { - color: black; - font-weight: bold; -} - -.content-list-details { - word-wrap: break-word; -} - -.content-list-details > ul { - position: relative; - z-index: 2; - list-style: none; - list-style-type: none; -} - -.content-list-card:hover { - background-color: #F5F0EF; -} - -/* statistics */ -.totals-row { - border-top: 1px solid grey; - border-bottom: 1px solid grey; - font-weight: bold; -} -.stats-table-url { - word-wrap: break-word; -} - - diff --git a/public/assets/css/general.css b/public/assets/css/general.css new file mode 100644 index 00000000..2074e0d6 --- /dev/null +++ b/public/assets/css/general.css @@ -0,0 +1,578 @@ +@font-face { + font-family: 'Lekton'; + src: url('../font/Lekton/Lekton-Regular.ttf'); +} + +@font-face { + font-family: 'Lekton'; + src: url('../font/Lekton/Lekton-Bold.ttf'); + font-weight: bold; + font-style: normal; +} + +@font-face { + font-family: 'Lekton'; + src: url('../font/Lekton/Lekton-Italic.ttf'); + font-weight: normal; + font-style: italic; +} + +html, body { + margin: 0; + padding: 0; + height: 100%; +} + +body, .flex-container { + display: -webkit-flex; + display: flex; +} + +body, .flex-container--column { + -webkit-flex-direction: column; + flex-direction: column; +} + +.flex-container--row { + -webkit-flex-direction: row; + flex-direction: row; + justify-content: space-between; +} + +.flex-container--wrap { + -webkit-flex-wrap: wrap; + flex-wrap: wrap; +} + +.flex-container--align-center { + align-items: center; +} + +.flex-container--justify-center { + justify-content: center; +} + +.flex-container--justify-space-between { + justify-content: space-between; +} + +.flex-container--left-bottom { + justify-content: flex-start; + align-items: baseline; +} + +.hidden { + display: none; +} + +/* TEXT */ + +body, button, input, textarea, label, select, option { + font-family: 'Lekton', monospace; + font-size: large; +} + +h3, p { + + font-size: x-large; +} + + +.text--large { + font-size: 2rem; +} + +.pull-quote { + font-size: 3rem; + margin-top: 1rem; +} + +.fine-print { + font-size: small; +} + +.blue { + color: #4156C5; +} + +.blue--underlined { + color: #4156C5; + text-decoration: underline; +} + +/* TOOL TIPS */ +/* Tooltip container */ +.tooltip { + position: relative; +} +/* Tooltip text */ +.tooltip > .tooltip-text { + visibility: hidden; + width: 15em; + background-color: #9b9b9b; + color: #fff; + text-align: center; + padding: 0.5em; + /* Position the tooltip text */ + position: absolute; + z-index: 1; + bottom: 110%; + left: 50%; + margin-left: -8em; /* Use half of the width (120/2 = 60), to center the tooltip */ +} +/* Show the tooltip text when you mouse over the tooltip container */ +.tooltip:hover > .tooltip-text { + visibility: visible; +} +/* arrow at bottom of tooltip text */ +.tooltip > .tooltip-text::after { + content: " "; + position: absolute; + top: 100%; + left: 50%; + margin-left: -5px; + border-width: 5px; + border-style: solid; + border-color: #9b9b9b transparent transparent transparent; +} + +/* LINKS */ + +a, a:visited { + text-decoration: none; +} + +.link--primary, .link--primary:visited { + color: #4156C5; +} + +.link--nav { + color: black; + border-bottom: 2px solid white; +} + +.link--nav:hover { + color: #4156C5; +} + +.link--nav-active { + color: #4156C5; + border-bottom: 2px solid #4156C5; +} + +/* COLUMNS AND ROWS */ + +.row { + clear: both; + margin: 0px; +} + +.row--padded { + padding: 3rem; +} + +.row--margined { + margin: 3rem; +} + +.row--wide { + padding-right: 0px; + padding-left: 0px; +} + +.row--short { + padding-top: 0px; + padding-bottom: 0px; +} + +.row--tall { + flex: 1 0 auto; +} + +.row--no-top { + padding-top: 0px; +} + +.row--no-bottom { + padding-bottom: 0px; +} + +.row--no-right { + padding-right: 0px; +} + +.column { + display: inline-block; + padding: 0px; + margin: 0px; +} + +.column--1 { + width: 10%; +} + +.column--2 { + width: 20%; +} + +.column--3 { + width: 30%; +} + +.column--4 { + width: 40%; +} + +.column--5 { + width: 50%; +} + +.column--6 { + width: 60%; +} + +.column--7 { + width: 70%; +} + +.column--8 { + width: 80%; +} + +.column--9 { + width: 90%; +} + +.column--10 { + width: 100%; +} + +/* ALIGNMENT */ +.align-content-left { + text-align: left; +} + +.align-content-center { + text-align: center; +} + +.align-content-right { + text-align: right; +} + +.align-content-top { + vertical-align: top; +} + +.align-content-right { + vertical-align: bottom; +} + + +/* ERROR MESSAGES */ + +.info-message--success, .info-message--failure { + + font-size: medium; + margin: 0px; + padding: 0.3em; +} + +.info-message--success { + color: green; +} + +.info-message--failure { + color: red; +} + +.info-message-placeholder { + +} + +/* INPUT FIELDS */ + +/* blocks */ +input:-webkit-autofill { + -webkit-box-shadow: 0 0 0px 1000px white inset; +} + +.label, .input-text, .select, .textarea, .text--large { + margin: 0px; + padding: 0.3em; + outline: none; + border: 0px; + background-color: white; + display: inline-block; +} + +.input-disabled { + border: 1px solid black; + padding: 0.5em; + margin: 0px; + color: black; + background-color: white; +} + +option { + font-family: monospace; +} + + +.input-checkbox { + border: 1px solid black; + background: white; +} + +.input-file { + width: 0.1px; + height: 0.1px; + opacity: 0; + overflow: hidden; + position: absolute; + z-index: -1; +} + +.input-radio, .label--pointer { + cursor: pointer; +} + +#claim-name-input { + +} + +#input-success-claim-name { + +} + +.span--relative { + position: relative; +} + +.span--absolute { + position: absolute; + bottom: 0px; + right: 0px; +} + +/* modifiers */ +.select--arrow { + -moz-appearance:none; + -webkit-appearance: none; + background: url('../icon/Shape.svg') no-repeat right; + cursor: pointer; + padding-right: 1.5em; +} + +.input-text--primary, .select--primary { + border-bottom: 1px solid #9b9b9b; +} + +.input-text--primary:focus, .select--primary:focus { + border-bottom: 1px solid #9b9b9b; +} + +.textarea--primary { + border-bottom: 1px solid #9b9b9b; +} + +.textarea--primary:focus { + border-bottom: 1px solid #9b9b9b; +} + +.input-text--full-width, .textarea--full-width { + width: calc(100% - 0.6em); +} + +.input-disabled--full-width { + width: calc(100% - 1em - 2px); +} + +.url-text--primary, .url-text--secondary { + margin:0px; + padding:0px; +} + +.url-text--primary { + color: black; +} + +.url-text--secondary { + color: #9b9b9b; +} + +/* BUTTONS */ + +button { + cursor: pointer; +} + +.button--primary { + border: 1px solid black; + padding: 0.5em; + margin: 0.5em 0.3em 0.5em 0.3em; + color: black; + background-color: white; +} + +.button--primary:hover { + border: 1px solid #4156C5; + color: white; + background-color: #4156C5; +} + +.button--primary:active{ + border: 1px solid #4156C5; + color: white; + background-color: white; +} + +.button--large{ + margin: 0px; + width: calc(100% - 2px); + padding: 2rem; + font-size: x-large; +} + +.button--cancel{ + border: 0px; + background-color: white; + color: #9b9b9b; +} + +/* TABLES */ + +table { + width: 100%; + text-align: left; +} + +/* NAV BAR */ + +.nav-bar { + border-bottom: 0.5px solid #cacaca; +} + +.nav-bar--left { + align-self: center; +} + +.nav-bar-tagline { + font-size: small; +} + +.nav-bar-link { + padding: 1.5rem; + display: inline-block; +} + +/* PUBLISH FORM */ + +.dropzone { + border: 2px dashed #9b9b9b; + text-align: center; + position: relative; +} + +.dropzone:hover, .dropzone--drag-over { + border: 2px dashed #4156C5; + cursor: pointer; +} + +#primary-dropzone-instructions, #dropbzone-dragover { + z-index: -1; +} + +.position-absolute { + position: absolute; + top: 0px; + left: 0px; + height: 100%; + width: 100%; +} + +#asset-preview-holder { + position: relative; +} + +#asset-preview { + display: block; + padding: 0.5rem; + width: calc(100% - 1rem); +} + +/* Show page */ + +.video-show, .gifv-show, .image-show { + display: block; + width: 100%; +} + +#video-player { + background-color: black; + cursor: pointer; +} + +.show-asset-light { + max-width: 50%; +} + +/* item lists */ + +.content-list-item-asset { + width: 90%; +} + +/* progress bar */ + +.progress-bar--inactive { + color: lightgrey; +} + +.progress-bar--active { + color: #4156C5; +} + +/* other */ + +.wrap-words { + word-wrap: break-word; +} + +#new-release-banner { + font-size: small; + background-color: #4156C5; + color: white; + text-align: center; +} + +/* ---- grid items ---- */ + +.grid-item { + width: calc(33% - 2rem); + padding: 0px; + margin: 1rem; + float: left; + border: 0.5px solid white; +} + +.grid-item-image { + width: 100%; +} + +.grid-item-details { + position: absolute; + top: 0px; + left: 0px; + height: 100%; + width: 100%; + cursor: pointer; +} + +.grid-item-details-text { + font-size: medium; + margin: 0px; + text-align: center; + padding: 1em 0px 1em 0px; + width: 100%; +} \ No newline at end of file diff --git a/public/assets/css/mediaQueries.css b/public/assets/css/mediaQueries.css index 6a36df91..890b1585 100644 --- a/public/assets/css/mediaQueries.css +++ b/public/assets/css/mediaQueries.css @@ -1,59 +1,96 @@ -@media (max-width: 1250px) { - .wrapper { - margin-left: 10%; - width:80%; - } -} +@media (max-width: 1050px) { -@media (max-width: 1000px) { - .wrapper { - margin-left: 10%; - width:80%; + .nav-bar--center { + display: none; } - .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; + .column--med-10 { width: 100%; } + } @media (max-width: 750px ) { - .col-left, .col-right { - float: none; - margin: 0px; - padding: 0px; + + body, button, input, textarea, label, select, option, p, h3 { + font-size: medium; + } + + .pull-quote { + font-size: 1.5rem; + } + + .column--sml-10 { width: 100%; } - .col-right { - padding-top: 20px; + .nav-bar-logo { + height: 1rem; } - .all-claims-asset { - width:30%; + .link--nav, .link--nav-active { + padding: 1rem 0.5rem 1rem 0.5rem; } - .all-claims-details { + .select--arrow { + padding-right: 1.5em; + } + + .show-asset-light { + max-width: 100%; + } + +} + +@media (max-width: 500px) { + + .nav-bar-logo { + height: 1rem; + } + + .row--padded { + padding: 1rem; + } + + .row--short { + padding-top: 0px; + padding-bottom: 0px; + } + + .row--margined { + margin: 1rem; + } + + body, button, input, textarea, label, select, option, p, h3, .fine-print { font-size: small; } - .show-asset-lite { - width: 100%; + .pull-quote, .text--large, .button--large { + font-size: medium; } - .top-bar-tagline { - clear: both; - text-align: left; - width: 100%; + .grid-item { + width: calc(100% - 2em); + float: none; + padding: 1em; + margin: 0px; } + + .info-message--success, .info-message--failure { + font-size: small; + } + + } + +@media (max-width: 360px) { + + body, button, input, textarea, label, select, option, p, h3, .fine-print { + font-size: x-small; + } + + .pull-quote, .text--large, .button--large { + font-size: small; + } + +} \ No newline at end of file diff --git a/public/assets/font/Lekton/Lekton-Bold.ttf b/public/assets/font/Lekton/Lekton-Bold.ttf new file mode 100644 index 00000000..b46b56b2 Binary files /dev/null and b/public/assets/font/Lekton/Lekton-Bold.ttf differ diff --git a/public/assets/font/Lekton/Lekton-Italic.ttf b/public/assets/font/Lekton/Lekton-Italic.ttf new file mode 100644 index 00000000..a23c4ce4 Binary files /dev/null and b/public/assets/font/Lekton/Lekton-Italic.ttf differ diff --git a/public/assets/font/Lekton/Lekton-Regular.ttf b/public/assets/font/Lekton/Lekton-Regular.ttf new file mode 100644 index 00000000..e971ecaa Binary files /dev/null and b/public/assets/font/Lekton/Lekton-Regular.ttf differ diff --git a/public/assets/font/Lekton/OFL.txt b/public/assets/font/Lekton/OFL.txt new file mode 100644 index 00000000..8f6fe441 --- /dev/null +++ b/public/assets/font/Lekton/OFL.txt @@ -0,0 +1,93 @@ +Copyright (c) 2008-2010, Isia Urbino (http://www.isiaurbino.net) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/public/assets/icon/Fill 5114 + Fill 5115 + Fill 5116.svg b/public/assets/icon/Fill 5114 + Fill 5115 + Fill 5116.svg new file mode 100644 index 00000000..a7eda5c2 --- /dev/null +++ b/public/assets/icon/Fill 5114 + Fill 5115 + Fill 5116.svg @@ -0,0 +1,15 @@ + + + + Fill 5114 + Fill 5115 + Fill 5116 + Created with Sketch. + + + + + + + + + + \ No newline at end of file diff --git a/public/assets/icon/Shape.svg b/public/assets/icon/Shape.svg new file mode 100644 index 00000000..fc73332e --- /dev/null +++ b/public/assets/icon/Shape.svg @@ -0,0 +1,16 @@ + + + + Shape + Created with Sketch. + + + + + + + + + + + \ No newline at end of file diff --git a/public/assets/icon/upload.svg b/public/assets/icon/upload.svg new file mode 100644 index 00000000..8b5637c1 --- /dev/null +++ b/public/assets/icon/upload.svg @@ -0,0 +1,22 @@ + + + + upload + Created with Sketch. + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/assets/img/Speech_Logo_Main@OG-02.jpg b/public/assets/img/Speech_Logo_Main@OG-02.jpg new file mode 100644 index 00000000..49eb43d8 Binary files /dev/null and b/public/assets/img/Speech_Logo_Main@OG-02.jpg differ diff --git a/public/assets/img/black_video_play.jpg b/public/assets/img/black_video_play.jpg new file mode 100644 index 00000000..17cfdd8a Binary files /dev/null and b/public/assets/img/black_video_play.jpg differ diff --git a/public/assets/img/content-freedom-large.png b/public/assets/img/content-freedom-large.png index a352c3c8..318746da 100644 Binary files a/public/assets/img/content-freedom-large.png and b/public/assets/img/content-freedom-large.png differ diff --git a/public/assets/img/down_triangle.png b/public/assets/img/down_triangle.png new file mode 100644 index 00000000..c5f5544a Binary files /dev/null and b/public/assets/img/down_triangle.png differ diff --git a/public/assets/img/logo.gif b/public/assets/img/logo.gif new file mode 100644 index 00000000..259a18e0 Binary files /dev/null and b/public/assets/img/logo.gif differ diff --git a/public/assets/img/upload_arrow.png b/public/assets/img/upload_arrow.png new file mode 100644 index 00000000..4036932f Binary files /dev/null and b/public/assets/img/upload_arrow.png differ diff --git a/public/assets/img/video_thumb_default.png b/public/assets/img/video_thumb_default.png new file mode 100644 index 00000000..318746da Binary files /dev/null and b/public/assets/img/video_thumb_default.png differ diff --git a/public/assets/js/constants.js b/public/assets/js/constants.js new file mode 100644 index 00000000..b37ed213 --- /dev/null +++ b/public/assets/js/constants.js @@ -0,0 +1 @@ +const EMAIL_FORMAT = 'ERROR_EMAIL_FORMAT'; diff --git a/public/assets/js/createChannelFunctions.js b/public/assets/js/createChannelFunctions.js new file mode 100644 index 00000000..faef7189 --- /dev/null +++ b/public/assets/js/createChannelFunctions.js @@ -0,0 +1,56 @@ +function showChannelCreateInProgressDisplay () { + const publishChannelForm = document.getElementById('publish-channel-form'); + publishChannelForm.hidden = true; + const inProgress = document.getElementById('channel-publish-in-progress'); + inProgress.hidden = false; + createProgressBar(document.getElementById('create-channel-progress-bar'), 12); +} + +function showChannelCreateDoneDisplay() { + const inProgress = document.getElementById('channel-publish-in-progress'); + inProgress.hidden=true; + const done = document.getElementById('channel-publish-done'); + done.hidden = false; +} + +function showChannelCreationError(msg) { + const inProgress = document.getElementById('channel-publish-in-progress'); + inProgress.innerText = msg; +} + +function publishNewChannel (event) { + const userName = document.getElementById('new-channel-name').value; + const password = document.getElementById('new-channel-password').value; + // prevent default so this script can handle submission + event.preventDefault(); + // validate submission + validateNewChannelSubmission(userName, password) + .then(() => { + showChannelCreateInProgressDisplay(); + return sendAuthRequest(userName, password, '/signup') // post the request + }) + .then(result => { + showChannelCreateDoneDisplay(); + // refresh window logged in as the channel + setUserCookies(result.channelName, result.channelClaimId, result.shortChannelId); // set cookies + }) + .then(() => { + if (window.location.pathname === '/') { + // remove old channel and replace with new one & select it + replaceChannelOptionInPublishChannelSelect(); + // remove old channel and replace with new one & select it + replaceChannelOptionInNavBarChannelSelect(); + } else { + window.location = '/'; + } + }) + .catch(error => { + if (error.name === 'ChannelNameError' || error.name === 'ChannelPasswordError'){ + const channelNameErrorDisplayElement = document.getElementById('input-error-channel-name'); + showError(channelNameErrorDisplayElement, error.message); + } else { + console.log('signup failure:', error); + showChannelCreationError('Unfortunately, Spee.ch encountered an error while creating your channel. Please let us know in slack!'); + } + }) +} \ No newline at end of file diff --git a/public/assets/js/dropzoneFunctions.js b/public/assets/js/dropzoneFunctions.js new file mode 100644 index 00000000..567537e1 --- /dev/null +++ b/public/assets/js/dropzoneFunctions.js @@ -0,0 +1,56 @@ +function triggerFileChooser(fileInputId, event) { + document.getElementById(fileInputId).click(); +} + +function drop_handler(event) { + event.preventDefault(); + // if dropped items aren't files, reject them + var dt = event.dataTransfer; + if (dt.items) { + if (dt.items[0].kind == 'file') { + var droppedFile = dt.items[0].getAsFile(); + previewAndStageFile(droppedFile); + } + } +} + +function dragover_handler(event) { + event.preventDefault(); +} + +function dragend_handler(event) { + var dt = event.dataTransfer; + if (dt.items) { + for (var i = 0; i < dt.items.length; i++) { + dt.items.remove(i); + } + } else { + event.dataTransfer.clearData(); + } +} + +function dragenter_handler(event) { + var thisDropzone = document.getElementById(event.target.id); + thisDropzone.setAttribute('class', 'dropzone dropzone--drag-over row row--margined row--padded row--tall flex-container flex-container--column flex-container--justify-center'); + thisDropzone.firstElementChild.setAttribute('class', 'hidden'); + thisDropzone.lastElementChild.setAttribute('class', ''); + +} + +function dragexit_handler(event) { + var thisDropzone = document.getElementById(event.target.id); + thisDropzone.setAttribute('class', 'dropzone row row--tall row--margined row--padded flex-container flex-container--column flex-container--justify-center'); + thisDropzone.firstElementChild.setAttribute('class', ''); + thisDropzone.lastElementChild.setAttribute('class', 'hidden'); +} + +function preview_onmouseenter_handler () { + document.getElementById('asset-preview-dropzone-instructions').setAttribute('class', 'flex-container flex-container--column flex-container--justify-center position-absolute'); + document.getElementById('asset-preview').style.opacity = 0.2; +} + +function preview_onmouseleave_handler () { + document.getElementById('asset-preview-dropzone-instructions').setAttribute('class', 'hidden'); + document.getElementById('asset-preview').style.opacity = 1; +} + diff --git a/public/assets/js/generalFunctions.js b/public/assets/js/generalFunctions.js index d19f35ec..f6cd1714 100644 --- a/public/assets/js/generalFunctions.js +++ b/public/assets/js/generalFunctions.js @@ -1,15 +1,14 @@ 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 if (xhttp.status == 401) { + reject('Wrong username or password'); } else { reject('request failed with status:' + xhttp.status); }; @@ -20,7 +19,6 @@ function getRequest (url) { } 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); @@ -28,10 +26,10 @@ function postRequest (url, params) { 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 if (xhttp.status == 401) { + reject( new AuthenticationError('Wrong username or password')); } else { reject('request failed with status:' + xhttp.status); }; @@ -62,22 +60,99 @@ function toggleSection(event){ } } -function createProgressBar(element, size){ - var x = 1; +function createProgressBar(element, size){ + var x = 0; var adder = 1; - function addOne(){ - var bars = '

|'; - for (var i = 0; i < x; i++){ bars += ' | '; } - bars += '

'; - element.innerHTML = bars; - if (x === size){ - adder = -1; - } else if ( x === 0){ - adder = 1; - } - x += adder; + // create the bar holder & place it + var barHolder = document.createElement('p'); + for (var i = 0; i < size; i++) { + const bar = document.createElement('span'); + bar.innerText = '| '; + bar.setAttribute('class', 'progress-bar progress-bar--inactive'); + barHolder.appendChild(bar); + } + element.appendChild(barHolder); + // get the bars + const bars = document.getElementsByClassName('progress-bar'); + // function to update the bars' classes + function updateOneBar(){ + // update the appropriate bar + if (x > -1 && x < size){ + if (adder === 1){ + bars[x].setAttribute('class', 'progress-bar progress-bar--active'); + } else { + bars[x].setAttribute('class', 'progress-bar progress-bar--inactive'); + } + } + // set x + if (x === size){ + adder = -1; + } else if ( x === -1){ + adder = 1; + } + // update the adder + x += adder; + }; - setInterval(addOne, 300); + // start updater + setInterval(updateOneBar, 300); +} + +function setCookie(key, value) { + document.cookie = `${key}=${value}`; +} + +function getCookie(cname) { + const name = cname + "="; + const decodedCookie = decodeURIComponent(document.cookie); + const ca = decodedCookie.split(';'); + for(let i = 0; i { + // send request + return sendAuthRequest(userName, password, '/login') + }) + .then(result => { + // update session cookie with new channel name and id's + setUserCookies(result.channelName, result.channelClaimId, result.shortChannelId); // replace the current cookies + }) + .then(() => { + // update channel selection + if (window.location.pathname === '/') { + // remove old channel and replace with new one & select it + replaceChannelOptionInPublishChannelSelect(); + // remove old channel and replace with new one & select it + replaceChannelOptionInNavBarChannelSelect(); + } else { + window.location = '/'; + } + + }) + .catch(error => { + const loginErrorDisplayElement = document.getElementById('login-error-display-element'); + if (error.name){ + showError(loginErrorDisplayElement, error.message); + } else { + showError(loginErrorDisplayElement, 'There was an error logging into your channel'); + } + }) +} diff --git a/public/assets/js/navBarFunctions.js b/public/assets/js/navBarFunctions.js new file mode 100644 index 00000000..5df9cfc4 --- /dev/null +++ b/public/assets/js/navBarFunctions.js @@ -0,0 +1,15 @@ +function toggleNavBarSelection (value) { + const selectedOption = value; + if (selectedOption === 'LOGOUT') { + // remove session cookies + clearUserCookies(); + // send logout request to server + window.location.href = '/logout'; + } else if (selectedOption === 'VIEW') { + // get channel info + const channelName = getCookie('channel_name'); + const channelClaimId = getCookie('channel_claim_id'); + // redirect to channel page + window.location.href = `/${channelName}:${channelClaimId}`; + } +} \ No newline at end of file diff --git a/public/assets/js/publishFileFunctions.js b/public/assets/js/publishFileFunctions.js index c535d1a6..42222e32 100644 --- a/public/assets/js/publishFileFunctions.js +++ b/public/assets/js/publishFileFunctions.js @@ -1,62 +1,48 @@ -/* 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(); - } -} - /* publish functions */ -// update the publish status -function updatePublishStatus(msg){ - document.getElementById('publish-status').innerHTML = msg; +function cancelPublish () { + window.location.href = '/'; } -// When a file is selected for publish, validate that file and +// When a file is selected for publish, validate that file and // stage it so it will be ready when the publish button is clicked. -function previewAndStageFile(selectedFile){ - var previewHolder = document.getElementById('asset-preview-holder'); - var dropzone = document.getElementById('drop-zone'); - var previewReader = new FileReader(); - var nameInput = document.getElementById('claim-name-input'); +function previewAndStageFile(selectedFile){ + const publishForm = document.getElementById('publish-form'); + const assetPreview = document.getElementById('asset-preview-target'); + const primaryDropzone = document.getElementById('primary-dropzone'); + const previewReader = new FileReader(); + const nameInput = document.getElementById('claim-name-input'); + const fileSelectionInputError = document.getElementById('input-error-file-selection'); + const thumbnailSelectionTool = document.getElementById('publish-thumbnail'); + const thumbnailSelectionInput = document.getElementById('claim-thumbnail-input'); // validate the file's name, type, and size try { validateFile(selectedFile); } catch (error) { - showError('input-error-file-selection', error.message); + showError(fileSelectionInputError, error.message); return; } // set the image preview, if an image was provided - if (selectedFile.type !== 'video/mp4') { + if (selectedFile.type !== 'video/mp4') { + if (selectedFile.type === 'image/gif') { + assetPreview.innerHTML = `

loading preview...

` + } previewReader.readAsDataURL(selectedFile); previewReader.onloadend = function () { - dropzone.style.display = 'none'; - previewHolder.style.display = 'block'; - previewHolder.innerHTML = 'image preview'; + assetPreview.innerHTML = 'image preview'; }; + // clear & hide the thumbnail selection input + thumbnailSelectionInput.value = ''; + thumbnailSelectionTool.hidden = true; + } else { + assetPreview.innerHTML = ``; + // clear & show the thumbnail selection input + thumbnailSelectionInput.value = ''; + thumbnailSelectionTool.hidden = false; } + // hide the drop zone + primaryDropzone.setAttribute('class', 'hidden'); + publishForm.setAttribute('class', 'row') // set the name input value to the image name if none is set yet if (nameInput.value === "") { var filename = selectedFile.name.substring(0, selectedFile.name.indexOf('.')) @@ -67,27 +53,46 @@ function previewAndStageFile(selectedFile){ stagedFiles = [selectedFile]; } -// Validate the publish submission and then trigger publishing. -function publishSelectedImage(event) { - var claimName = document.getElementById('claim-name-input').value; - var channelName = document.getElementById('channel-name-select').value; +// Validate the publish submission and then trigger upload +function publishStagedFile(event) { // prevent default so this script can handle submission event.preventDefault(); + // declare variables + const claimName = document.getElementById('claim-name-input').value; + let channelName = document.getElementById('channel-name-select').value; + const fileSelectionInputError = document.getElementById('input-error-file-selection'); + const claimNameError = document.getElementById('input-error-claim-name'); + const channelSelectError = document.getElementById('input-error-channel-select'); + const publishSubmitError = document.getElementById('input-error-publish-submit'); + let anonymousOrInChannel; + // replace channelName with 'anonymous' if appropriate + const radios = document.getElementsByName('anonymous-or-channel'); + for (let i = 0; i < radios.length; i++) { + if (radios[i].checked) { + // do whatever you want with the checked radio + anonymousOrInChannel = radios[i].value; + // only one radio can be logically checked, don't check the rest + break; + } + } + if (anonymousOrInChannel === 'anonymous') { + channelName = null; + }; // validate, submit, and handle response validateFilePublishSubmission(stagedFiles, claimName, channelName) .then(() => { - uploader.submitFiles(stagedFiles); + uploader.submitFiles(stagedFiles); }) .catch(error => { if (error.name === 'FileError') { - showError(document.getElementById('input-error-file-selection'), error.message); + showError(fileSelectionInputError, error.message); } else if (error.name === 'NameError') { - showError(document.getElementById('input-error-claim-name'), error.message); + showError(claimNameError, error.message); } else if (error.name === 'ChannelNameError'){ console.log(error); - showError(document.getElementById('input-error-channel-select'), error.message); + showError(channelSelectError, error.message); } else { - showError(document.getElementById('input-error-publish-submit'), error.message); + showError(publishSubmitError, error.message); } return; }) diff --git a/public/assets/js/showFunctions.js b/public/assets/js/showFunctions.js new file mode 100644 index 00000000..94047cd3 --- /dev/null +++ b/public/assets/js/showFunctions.js @@ -0,0 +1,23 @@ +function playOrPause(video){ + if (video.paused == true) { + video.play(); + } + else{ + video.pause(); + } +} + +// if a video player is present, set the listeners +const video = document.getElementById('video-player'); +if (video) { + // add event listener for click + video.addEventListener('click', ()=> { + playOrPause(video); + }); + // add event listener for space bar + document.body.onkeyup = (event) => { + if (event.keyCode == 32) { + playOrPause(video); + } + }; +} \ No newline at end of file diff --git a/public/assets/js/validationFunctions.js b/public/assets/js/validationFunctions.js index 7d4f2e00..e8204776 100644 --- a/public/assets/js/validationFunctions.js +++ b/public/assets/js/validationFunctions.js @@ -1,47 +1,56 @@ - - // validation function which checks the proposed file's type, size, and name function validateFile(file) { - if (!file) { - throw new Error('no file provided'); - } - if (/'/.test(file.name)) { - throw new Error('apostrophes are not allowed in the file name'); - } - // validate size and type - switch (file.type) { - case 'image/jpeg': - case 'image/jpg': - case 'image/png': - case 'image/gif': - if (file.size > 50000000){ - throw new Error('Sorry, images are limited to 50 megabytes.'); - } - break; - case 'video/mp4': - if (file.size > 50000000){ - throw new Error('Sorry, videos are limited to 50 megabytes.'); - } - break; - default: - throw new Error(file.type + ' is not a supported file type. Only, .jpeg, .png, .gif, and .mp4 files are currently supported.') - } + if (!file) { + console.log('no file found'); + throw new Error('no file provided'); + } + if (/'/.test(file.name)) { + console.log('file name had apostrophe in it'); + throw new Error('apostrophes are not allowed in the file name'); + } + // validate size and type + switch (file.type) { + case 'image/jpeg': + case 'image/jpg': + case 'image/png': + if (file.size > 10000000){ + console.log('file was too big'); + throw new Error('Sorry, images are limited to 10 megabytes.'); + } + break; + case 'image/gif': + if (file.size > 50000000){ + console.log('file was too big'); + throw new Error('Sorry, .gifs are limited to 50 megabytes.'); + } + break; + case 'video/mp4': + if (file.size > 50000000){ + console.log('file was too big'); + throw new Error('Sorry, videos are limited to 50 megabytes.'); + } + break; + default: + console.log('file type is not supported'); + 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 valid function validateClaimName (name) { - // ensure a name was entered - if (name.length < 1) { - 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 url.'); - } + // ensure a name was entered + if (name.length < 1) { + 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'); + } } function validateChannelName (name) { - name = name.substring(name.indexOf('@') + 1); + 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"); @@ -49,7 +58,7 @@ function validateChannelName (name) { // 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.'); + throw new ChannelNameError('"' + invalidCharacters + '" characters are not allowed'); } } @@ -60,9 +69,9 @@ function validatePassword (password) { } 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; + 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; } // validation functions to check claim & channel name eligibility as the inputs change @@ -99,14 +108,13 @@ function checkAvailability(name, successDisplayElement, errorDisplayElement, val // check to make sure it is available isNameAvailable(name, apiUrl) .then(result => { - console.log('result:', result) - if (result === true) { + if (result === true) { hideError(errorDisplayElement); showSuccess(successDisplayElement) - } else { + } else { hideSuccess(successDisplayElement); showError(errorDisplayElement, errorMessage); - } + } }) .catch(error => { hideSuccess(successDisplayElement); @@ -119,58 +127,68 @@ function checkAvailability(name, successDisplayElement, errorDisplayElement, val } 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/'); + const successDisplayElement = document.getElementById('input-success-claim-name'); + const errorDisplayElement = document.getElementById('input-error-claim-name'); + checkAvailability(name, successDisplayElement, errorDisplayElement, validateClaimName, isNameAvailable, 'Sorry, that ending is already taken', '/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/'); + checkAvailability(name, successDisplayElement, errorDisplayElement, validateChannelName, isNameAvailable, 'Sorry, that name is already taken', '/api/isChannelAvailable/'); } // validation function which checks all aspects of the publish submission function validateFilePublishSubmission(stagedFiles, claimName, channelName){ - return new Promise(function (resolve, reject) { - // 1. make sure only 1 file was selected - if (!stagedFiles) { - return reject(new FileError("Please select a file")); - } else if (stagedFiles.length > 1) { - return reject(new FileError("Only one file is allowed at a time")); - } - // 2. validate the file's name, type, and size - try { - validateFile(stagedFiles[0]); - } catch (error) { - return reject(error); - } - // 3. validate that a channel was chosen - if (channelName === 'new' || channelName === 'login') { - return reject(new ChannelNameError("Please select a valid channel")); + return new Promise(function (resolve, reject) { + // 1. make sure 1 file was staged + if (!stagedFiles) { + reject(new FileError("Please select a file")); + return; + } else if (stagedFiles.length > 1) { + reject(new FileError("Only one file is allowed at a time")); + return; + } + // 2. validate the file's name, type, and size + try { + validateFile(stagedFiles[0]); + } catch (error) { + reject(error); + return; + } + // 3. validate that a channel was chosen + if (channelName === 'new' || channelName === 'login') { + reject(new ChannelNameError("Please log in to a channel")); + return; }; - // 4. validate the claim name - try { - validateClaimName(claimName); - } catch (error) { - return reject(error); - } - // if all validation passes, check availability of the name - isNameAvailable(claimName, '/api/isClaimAvailable/') - .then(() => { - resolve(); - }) - .catch(error => { - reject(error); - }); - }); + // 4. validate the claim name + try { + validateClaimName(claimName); + } catch (error) { + reject(error); + return; + } + // if all validation passes, check availability of the name (note: do we need to re-validate channel name vs. credentials as well?) + return isNameAvailable(claimName, '/api/isClaimAvailable/') + .then(result => { + if (result) { + resolve(); + } else { + reject(new NameError('Sorry, that ending is already taken')); + } + }) + .catch(error => { + reject(error); + }); + }); } -// validation function which checks all aspects of the publish submission -function validateNewChannelSubmission(channelName, password){ +// validation function which checks all aspects of a new channel submission +function validateNewChannelSubmission(userName, password){ + const channelName = `@${userName}`; return new Promise(function (resolve, reject) { - // 1. validate name + // 1. validate name try { validateChannelName(channelName); } catch (error) { @@ -184,13 +202,35 @@ function validateNewChannelSubmission(channelName, password){ } // 3. if all validation passes, check availability of the name isNameAvailable(channelName, '/api/isChannelAvailable/') // validate the availability - .then(() => { - console.log('channel is avaliable'); - resolve(); + .then(result => { + if (result) { + resolve(); + } else { + reject(new ChannelNameError('Sorry, that name is already taken')); + } }) .catch( error => { - console.log('error: channel is not avaliable'); + console.log('error evaluating channel name availability', error); reject(error); }); }); +} +// validation function which checks all aspects of a new channel login +function validateNewChannelLogin(userName, password){ + const channelName = `@${userName}`; + 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); + } + resolve(); + }); } \ No newline at end of file diff --git a/public/assets/vendors/imagesloaded/imagesloaded.pkgd.min.js b/public/assets/vendors/imagesloaded/imagesloaded.pkgd.min.js new file mode 100644 index 00000000..0854d63e --- /dev/null +++ b/public/assets/vendors/imagesloaded/imagesloaded.pkgd.min.js @@ -0,0 +1,7 @@ +/*! + * imagesLoaded PACKAGED v4.1.3 + * JavaScript is all like "You images are done yet or what?" + * MIT License + */ + +!function(e,t){"function"==typeof define&&define.amd?define("ev-emitter/ev-emitter",t):"object"==typeof module&&module.exports?module.exports=t():e.EvEmitter=t()}("undefined"!=typeof window?window:this,function(){function e(){}var t=e.prototype;return t.on=function(e,t){if(e&&t){var i=this._events=this._events||{},n=i[e]=i[e]||[];return-1==n.indexOf(t)&&n.push(t),this}},t.once=function(e,t){if(e&&t){this.on(e,t);var i=this._onceEvents=this._onceEvents||{},n=i[e]=i[e]||{};return n[t]=!0,this}},t.off=function(e,t){var i=this._events&&this._events[e];if(i&&i.length){var n=i.indexOf(t);return-1!=n&&i.splice(n,1),this}},t.emitEvent=function(e,t){var i=this._events&&this._events[e];if(i&&i.length){var n=0,o=i[n];t=t||[];for(var r=this._onceEvents&&this._onceEvents[e];o;){var s=r&&r[o];s&&(this.off(e,o),delete r[o]),o.apply(this,t),n+=s?0:1,o=i[n]}return this}},t.allOff=t.removeAllListeners=function(){delete this._events,delete this._onceEvents},e}),function(e,t){"use strict";"function"==typeof define&&define.amd?define(["ev-emitter/ev-emitter"],function(i){return t(e,i)}):"object"==typeof module&&module.exports?module.exports=t(e,require("ev-emitter")):e.imagesLoaded=t(e,e.EvEmitter)}("undefined"!=typeof window?window:this,function(e,t){function i(e,t){for(var i in t)e[i]=t[i];return e}function n(e){var t=[];if(Array.isArray(e))t=e;else if("number"==typeof e.length)for(var i=0;ie;e++){var i=h[e];t[i]=0}return t}function n(t){var e=getComputedStyle(t);return e||a("Style returned "+e+". Are you running this code in a hidden iframe on Firefox? See http://bit.ly/getsizebug1"),e}function o(){if(!d){d=!0;var e=document.createElement("div");e.style.width="200px",e.style.padding="1px 2px 3px 4px",e.style.borderStyle="solid",e.style.borderWidth="1px 2px 3px 4px",e.style.boxSizing="border-box";var i=document.body||document.documentElement;i.appendChild(e);var o=n(e);r.isBoxSizeOuter=s=200==t(o.width),i.removeChild(e)}}function r(e){if(o(),"string"==typeof e&&(e=document.querySelector(e)),e&&"object"==typeof e&&e.nodeType){var r=n(e);if("none"==r.display)return i();var a={};a.width=e.offsetWidth,a.height=e.offsetHeight;for(var d=a.isBorderBox="border-box"==r.boxSizing,l=0;u>l;l++){var c=h[l],f=r[c],m=parseFloat(f);a[c]=isNaN(m)?0:m}var p=a.paddingLeft+a.paddingRight,g=a.paddingTop+a.paddingBottom,y=a.marginLeft+a.marginRight,v=a.marginTop+a.marginBottom,_=a.borderLeftWidth+a.borderRightWidth,z=a.borderTopWidth+a.borderBottomWidth,E=d&&s,b=t(r.width);b!==!1&&(a.width=b+(E?0:p+_));var x=t(r.height);return x!==!1&&(a.height=x+(E?0:g+z)),a.innerWidth=a.width-(p+_),a.innerHeight=a.height-(g+z),a.outerWidth=a.width+y,a.outerHeight=a.height+v,a}}var s,a="undefined"==typeof console?e:function(t){console.error(t)},h=["paddingLeft","paddingRight","paddingTop","paddingBottom","marginLeft","marginRight","marginTop","marginBottom","borderLeftWidth","borderRightWidth","borderTopWidth","borderBottomWidth"],u=h.length,d=!1;return r}),function(t,e){"use strict";"function"==typeof define&&define.amd?define("desandro-matches-selector/matches-selector",e):"object"==typeof module&&module.exports?module.exports=e():t.matchesSelector=e()}(window,function(){"use strict";var t=function(){var t=window.Element.prototype;if(t.matches)return"matches";if(t.matchesSelector)return"matchesSelector";for(var e=["webkit","moz","ms","o"],i=0;is?"round":"floor";r=Math[a](r),this.cols=Math.max(r,1)},n.getContainerWidth=function(){var t=this._getOption("fitWidth"),i=t?this.element.parentNode:this.element,n=e(i);this.containerWidth=n&&n.innerWidth},n._getItemLayoutPosition=function(t){t.getSize();var e=t.size.outerWidth%this.columnWidth,i=e&&1>e?"round":"ceil",n=Math[i](t.size.outerWidth/this.columnWidth);n=Math.min(n,this.cols);for(var o=this.options.horizontalOrder?"_getHorizontalColPosition":"_getTopColPosition",r=this[o](n,t),s={x:this.columnWidth*r.col,y:r.y},a=r.y+t.size.outerHeight,h=n+r.col,u=r.col;h>u;u++)this.colYs[u]=a;return s},n._getTopColPosition=function(t){var e=this._getTopColGroup(t),i=Math.min.apply(Math,e);return{col:e.indexOf(i),y:i}},n._getTopColGroup=function(t){if(2>t)return this.colYs;for(var e=[],i=this.cols+1-t,n=0;i>n;n++)e[n]=this._getColGroupY(n,t);return e},n._getColGroupY=function(t,e){if(2>e)return this.colYs[t];var i=this.colYs.slice(t,t+e);return Math.max.apply(Math,i)},n._getHorizontalColPosition=function(t,e){var i=this.horizontalColIndex%this.cols,n=t>1&&i+t>this.cols;i=n?0:i;var o=e.size.outerWidth&&e.size.outerHeight;return this.horizontalColIndex=o?i+t:this.horizontalColIndex,{col:i,y:this._getColGroupY(i,t)}},n._manageStamp=function(t){var i=e(t),n=this._getElementOffset(t),o=this._getOption("originLeft"),r=o?n.left:n.right,s=r+i.outerWidth,a=Math.floor(r/this.columnWidth);a=Math.max(0,a);var h=Math.floor(s/this.columnWidth);h-=s%this.columnWidth?0:1,h=Math.min(this.cols-1,h);for(var u=this._getOption("originTop"),d=(u?n.top:n.bottom)+i.outerHeight,l=a;h>=l;l++)this.colYs[l]=Math.max(d,this.colYs[l])},n._getContainerSize=function(){this.maxY=Math.max.apply(Math,this.colYs);var t={height:this.maxY};return this._getOption("fitWidth")&&(t.width=this._getContainerFitWidth()),t},n._getContainerFitWidth=function(){for(var t=0,e=this.cols;--e&&0===this.colYs[e];)t++;return(this.cols-t)*this.columnWidth-this.gutter},n.needsResizeLayout=function(){var t=this.containerWidth;return this.getContainerWidth(),t!=this.containerWidth},i}); \ No newline at end of file diff --git a/public/robots.txt b/public/robots.txt new file mode 100644 index 00000000..e69de29b diff --git a/routes/api-routes.js b/routes/api-routes.js index e88d4668..aabd348a 100644 --- a/routes/api-routes.js +++ b/routes/api-routes.js @@ -4,10 +4,10 @@ const multipartMiddleware = multipart(); const db = require('../models'); const { publish } = require('../controllers/publishController.js'); const { getClaimList, resolveUri } = require('../helpers/lbryApi.js'); -const { createPublishParams, validateFile, checkClaimNameAvailability, checkChannelAvailability } = require('../helpers/publishHelpers.js'); +const { createPublishParams, validateApiPublishRequest, validatePublishSubmission, cleanseNSFW, cleanseChannelName, 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'); +const { authenticateChannelCredentials } = require('../auth/authentication.js'); module.exports = (app) => { // route to run a claim_list request on the daemon @@ -25,7 +25,7 @@ module.exports = (app) => { }); }); // route to check whether spee.ch has published to a claim - app.get('/api/isClaimAvailable/:name', ({ ip, originalUrl, params }, res) => { + app.get('/api/isClaimAvailable/:name', ({ params }, res) => { // send response checkClaimNameAvailability(params.name) .then(result => { @@ -71,52 +71,79 @@ module.exports = (app) => { }); }); // route to run a publish request on the daemon - app.post('/api/publish', multipartMiddleware, ({ body, files, headers, ip, originalUrl }, res) => { - // google analytics - sendGoogleAnalytics('PUBLISH', headers, ip, originalUrl); - // 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 || null; - const channelName = body.channelName || 'none'; - const channelPassword = body.channelPassword || null; - logger.debug(`name: ${name}, license: ${license}, nsfw: ${nsfw}`); + app.post('/api/publish', multipartMiddleware, (req, res) => { + logger.debug('req:', req); + // validate that mandatory parts of the request are present + const body = req.body; + const files = req.files; try { - validateFile(file, name, license, nsfw); + validateApiPublishRequest(body, files); } catch (error) { - postToStats('publish', originalUrl, ip, null, null, error.message); - logger.debug('rejected >>', error.message); - res.status(400).send(error.message); + logger.debug('publish request rejected, insufficient request parameters'); + res.status(400).json({success: false, message: error.message}); return; } + // validate file, name, license, and nsfw + const file = files.file; const fileName = file.name; const filePath = file.path; const fileType = file.type; - // channel authorization - authenticateApiPublish(channelName, channelPassword) + const name = body.name; + let nsfw = body.nsfw; + nsfw = cleanseNSFW(nsfw); // cleanse nsfw + try { + validatePublishSubmission(file, name, nsfw); + } catch (error) { + logger.debug('publish request rejected'); + res.status(400).json({success: false, message: error.message}); + return; + } + logger.debug(`name: ${name}, nsfw: ${nsfw}`); + // optional inputs + const license = body.license || null; + const title = body.title || null; + const description = body.description || null; + const thumbnail = body.thumbnail || null; + let channelName = body.channelName || null; + channelName = cleanseChannelName(channelName); + const channelPassword = body.channelPassword || null; + logger.debug(`license: ${license} title: "${title}" description: "${description}" channelName: "${channelName}" channelPassword: "${channelPassword}"`); + // check channel authorization + authenticateChannelCredentials(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'); + throw new Error('Authentication failed, you do not have access to that channel'); } - return createPublishParams(name, filePath, title, description, license, nsfw, channelName); + // make sure the claim name is available + return checkClaimNameAvailability(name); + }) + .then(result => { + if (!result) { + throw new Error('That name is already in use by spee.ch.'); + } + // create publish parameters object + return createPublishParams(filePath, name, title, description, license, nsfw, thumbnail, channelName); }) - // create publish parameters object .then(publishParams => { + logger.debug('publishParams:', publishParams); + // publish the asset return publish(publishParams, fileName, fileType); }) - // publish the asset .then(result => { - postToStats('publish', originalUrl, ip, null, null, 'success'); - res.status(200).json(result); + res.status(200).json({ + success: true, + message: { + url : `spee.ch/${result.claim_id}/${name}`, + lbryTx: result, + }, + }); }) .catch(error => { logger.error('publish api error', error); + res.status(400).json({success: false, message: error.message}); }); }); + // route to get a short claim id from long claim Id app.get('/api/shortClaimId/:longId/:name', ({ originalUrl, ip, params }, res) => { // serve content @@ -134,7 +161,7 @@ module.exports = (app) => { // serve content db.getShortChannelIdFromLongChannelId(params.longId, params.name) .then(shortId => { - console.log('sending back short channel id', shortId); + logger.debug('sending back short channel id', shortId); res.status(200).json(shortId); }) .catch(error => { diff --git a/routes/auth-routes.js b/routes/auth-routes.js index 39c0ef0f..c1650009 100644 --- a/routes/auth-routes.js +++ b/routes/auth-routes.js @@ -4,12 +4,23 @@ 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); + logger.verbose(`successful signup for ${req.user.channelName}`); + res.status(200).json({ + success : true, + channelName : req.user.channelName, + channelClaimId: req.user.channelClaimId, + shortChannelId: req.user.shortChannelId, + }); }); // route for log in app.post('/login', passport.authenticate('local-login'), (req, res) => { + logger.debug('req.user:', req.user); logger.debug('successful login'); - res.status(200).json(true); + res.status(200).json({ + success : true, + channelName : req.user.channelName, + channelClaimId: req.user.channelClaimId, + shortChannelId: req.user.shortChannelId, + }); }); }; diff --git a/routes/page-routes.js b/routes/page-routes.js index 855198bf..6b0368e9 100644 --- a/routes/page-routes.js +++ b/routes/page-routes.js @@ -1,5 +1,5 @@ const errorHandlers = require('../helpers/errorHandlers.js'); -const { postToStats, getStatsSummary, getTrendingClaims, getRecentClaims } = require('../controllers/statsController.js'); +const { getTrendingClaims, getRecentClaims } = require('../controllers/statsController.js'); module.exports = (app) => { // route to log out @@ -31,12 +31,12 @@ module.exports = (app) => { getTrendingClaims(dateTime) .then(result => { // logger.debug(result); - res.status(200).render('trending', { + res.status(200).render('popular', { trendingAssets: result, }); }) .catch(error => { - errorHandlers.handleRequestError(error, res); + errorHandlers.handleRequestError(null, null, null, error, res); }); }); // route to display a list of the trending images @@ -49,32 +49,13 @@ module.exports = (app) => { }); }) .catch(error => { - errorHandlers.handleRequestError(error, res); - }); - }); - // route to show statistics for spee.ch - 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', { - user, - result, - }); - }) - .catch(error => { - errorHandlers.handleRequestError(error, res); + errorHandlers.handleRequestError(null, null, null, error, res); }); }); // route to send embedable video player (for twitter) app.get('/embed/:claimId/:name', ({ params }, res) => { const claimId = params.claimId; const name = params.name; - console.log('claimId ==', claimId); - console.log('name ==', name); // get and render the content res.status(200).render('embed', { layout: 'embed', claimId, name }); }); diff --git a/routes/serve-routes.js b/routes/serve-routes.js index 188a03c1..2e2ea652 100644 --- a/routes/serve-routes.js +++ b/routes/serve-routes.js @@ -7,7 +7,11 @@ const SHOW = 'SHOW'; const SHOWLITE = 'SHOWLITE'; const CHANNEL = 'CHANNEL'; const CLAIM = 'CLAIM'; -const CHANNELID_INDICATOR = ':'; +const CLAIM_ID_CHAR = ':'; +const CHANNEL_CHAR = '@'; +const CLAIMS_PER_PAGE = 10; +const NO_CHANNEL = 'NO_CHANNEL'; +const NO_CLAIM = 'NO_CLAIM'; function isValidClaimId (claimId) { return ((claimId.length === 40) && !/[^A-Za-z0-9]/g.test(claimId)); @@ -32,12 +36,58 @@ function getAsset (claimType, channelName, channelId, name, claimId) { } } +function getPage (query) { + if (query.p) { + return parseInt(query.p); + } + return 1; +} + +function extractPageFromClaims (claims, pageNumber) { + logger.debug('claims is array?', Array.isArray(claims)); + logger.debug(`pageNumber ${pageNumber} is number?`, Number.isInteger(pageNumber)); + const claimStartIndex = (pageNumber - 1) * CLAIMS_PER_PAGE; + const claimEndIndex = claimStartIndex + 10; + const pageOfClaims = claims.slice(claimStartIndex, claimEndIndex); + logger.debug('page of claims:', pageOfClaims); + return pageOfClaims; +} + +function determineTotalPages (totalClaims) { + if (totalClaims === 0) { + return 0; + } + if (totalClaims < CLAIMS_PER_PAGE) { + return 1; + } + const fullPages = Math.floor(totalClaims / CLAIMS_PER_PAGE); + const remainder = totalClaims % CLAIMS_PER_PAGE; + if (remainder === 0) { + return fullPages; + } + return fullPages + 1; +} + +function determinePreviousPage (currentPage) { + if (currentPage === 1) { + return null; + } + return currentPage - 1; +} + +function determineNextPage (totalPages, currentPage) { + if (currentPage === totalPages) { + return null; + } + return currentPage + 1; +} + module.exports = (app) => { // route to serve a specific asset app.get('/:identifier/:name', ({ headers, ip, originalUrl, params }, res) => { let identifier = params.identifier; let name = params.name; - let claimType; + let claimOrChannel; let channelName = null; let claimId = null; let channelId = null; @@ -74,8 +124,8 @@ module.exports = (app) => { // parse identifier for whether it is a channel, short url, or claim_id if (identifier.charAt(0) === '@') { channelName = identifier; - claimType = CHANNEL; - const channelIdIndex = channelName.indexOf(CHANNELID_INDICATOR); + claimOrChannel = CHANNEL; + const channelIdIndex = channelName.indexOf(CLAIM_ID_CHAR); if (channelIdIndex !== -1) { channelId = channelName.substring(channelIdIndex + 1); channelName = channelName.substring(0, channelIdIndex); @@ -84,17 +134,21 @@ module.exports = (app) => { } else { claimId = identifier; logger.debug('claim id =', claimId); - claimType = CLAIM; + claimOrChannel = CLAIM; } // 1. retrieve the asset and information - getAsset(claimType, channelName, channelId, name, claimId) + getAsset(claimOrChannel, channelName, channelId, name, claimId) // 2. serve or show - .then(fileInfo => { - if (!fileInfo) { - res.status(200).render('noClaims'); - } else { - return serveOrShowAsset(fileInfo, fileExtension, method, headers, originalUrl, ip, res); + .then(result => { + logger.debug('getAsset result:', result); + if (result === NO_CLAIM) { + res.status(200).render('noClaim'); + return; + } else if (result === NO_CHANNEL) { + res.status(200).render('noChannel'); + return; } + return serveOrShowAsset(result, fileExtension, method, headers, originalUrl, ip, res); }) // 3. update the file .then(fileInfoForUpdate => { @@ -105,16 +159,18 @@ module.exports = (app) => { }); }); // route to serve the winning asset at a claim - app.get('/:name', ({ headers, ip, originalUrl, params }, res) => { + app.get('/:name', ({ headers, ip, originalUrl, params, query }, res) => { // parse name param let name = params.name; let method; let fileExtension; let channelName = null; let channelId = null; - if (name.charAt(0) === '@') { + // (a) handle channel requests + if (name.charAt(0) === CHANNEL_CHAR) { channelName = name; - const channelIdIndex = channelName.indexOf(CHANNELID_INDICATOR); + const paginationPage = getPage(query); + const channelIdIndex = channelName.indexOf(CLAIM_ID_CHAR); if (channelIdIndex !== -1) { channelId = channelName.substring(channelIdIndex + 1); channelName = channelName.substring(0, channelIdIndex); @@ -125,16 +181,39 @@ module.exports = (app) => { getChannelContents(channelName, channelId) // 2. respond to the request .then(result => { - logger.debug('result'); - if (!result.claims) { + if (result === NO_CHANNEL) { // no channel found res.status(200).render('noChannel'); - } else { - res.status(200).render('channel', result); + } else if (!result.claims) { // channel found, but no claims + res.status(200).render('channel', { + channelName : result.channelName, + longChannelId : result.longChannelId, + shortChannelId: result.shortChannelId, + claims : [], + previousPage : 0, + currentPage : 0, + nextPage : 0, + totalPages : 0, + totalResults : 0, + }); + } else { // channel found, with claims + const totalPages = determineTotalPages(result.claims.length); + res.status(200).render('channel', { + channelName : result.channelName, + longChannelId : result.longChannelId, + shortChannelId: result.shortChannelId, + claims : extractPageFromClaims(result.claims, paginationPage), + previousPage : determinePreviousPage(paginationPage), + currentPage : paginationPage, + nextPage : determineNextPage(totalPages, paginationPage), + totalPages : totalPages, + totalResults : result.claims.length, + }); } }) .catch(error => { handleRequestError('serve', originalUrl, ip, error, res); }); + // (b) handle stream requests } else { if (name.indexOf('.') !== -1) { method = SERVE; @@ -155,11 +234,12 @@ module.exports = (app) => { // 1. retrieve the asset and information getAsset(CLAIM, null, null, name, null) // 2. respond to the request - .then(fileInfo => { - if (!fileInfo) { - res.status(200).render('noClaims'); + .then(result => { + logger.debug('getAsset result', result); + if (result === NO_CLAIM) { + res.status(200).render('noClaim'); } else { - return serveOrShowAsset(fileInfo, fileExtension, method, headers, originalUrl, ip, res); + return serveOrShowAsset(result, fileExtension, method, headers, originalUrl, ip, res); } }) // 3. update the database diff --git a/routes/sockets-routes.js b/routes/sockets-routes.js index c7dadd54..88bffef2 100644 --- a/routes/sockets-routes.js +++ b/routes/sockets-routes.js @@ -1,7 +1,7 @@ const logger = require('winston'); -const publishController = require('../controllers/publishController.js'); -const publishHelpers = require('../helpers/publishHelpers.js'); -const errorHandlers = require('../helpers/errorHandlers.js'); +const { publish } = require('../controllers/publishController.js'); +const { createPublishParams } = require('../helpers/publishHelpers.js'); +const { useObjectPropertiesIfNoKeys } = require('../helpers/errorHandlers.js'); const { postToStats } = require('../controllers/statsController.js'); module.exports = (app, siofu, hostedContentPath) => { @@ -34,31 +34,38 @@ module.exports = (app, siofu, hostedContentPath) => { uploader.on('saved', ({ file }) => { 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. - */ - + socket.emit('publish-update', '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. + // */ + let thumbnail; + if (file.meta.thumbnail) { + thumbnail = file.meta.thumbnail; + } else { + thumbnail = null; + } + let channelName; + if (file.meta.channel) { + channelName = file.meta.channel; + } else { + channelName = null; + } // 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, file.meta.channel); - logger.debug(publishParams); + const publishParams = createPublishParams(file.pathName, file.meta.name, file.meta.title, file.meta.description, file.meta.license, file.meta.nsfw, thumbnail, channelName); + logger.debug('publish parameters:', publishParams); // publish the file - publishController.publish(publishParams, file.name, file.meta.type) + publish(publishParams, file.name, file.meta.type) .then(result => { - postToStats('PUBLISH', '/', null, null, null, 'success'); socket.emit('publish-complete', { name: publishParams.name, result }); }) .catch(error => { - error = errorHandlers.handlePublishError(error); - postToStats('PUBLISH', '/', null, null, null, error); - socket.emit('publish-failure', error); + logger.error('Publish Error:', useObjectPropertiesIfNoKeys(error)); + socket.emit('publish-failure', error.message); }); } else { - 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'); + logger.error(`An error occurred in uploading the client's file`); // to-do: remove the file, if not done automatically } }); diff --git a/speech.js b/speech.js index 082c64a6..b6e00a7b 100644 --- a/speech.js +++ b/speech.js @@ -5,6 +5,7 @@ const siofu = require('socketio-file-upload'); const expressHandlebars = require('express-handlebars'); const Handlebars = require('handlebars'); const handlebarsHelpers = require('./helpers/handlebarsHelpers.js'); +const { populateLocalsDotUser, serializeSpeechUser, deserializeSpeechUser } = require('./helpers/authHelpers.js'); const config = require('config'); const logger = require('winston'); const { getDownloadDirectory } = require('./helpers/lbryApi'); @@ -13,7 +14,7 @@ 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'); +const cookieSession = require('cookie-session'); // configure logging const logLevel = config.get('Logging.LogLevel'); @@ -34,40 +35,20 @@ app.use(bodyParser.urlencoded({ extended: true })); // 'body parser' for parsing app.use(siofu.router); // 'socketio-file-upload' router for uploading with socket.io 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); + logger.debug('req.body:', req.body); next(); }); // initialize passport -app.use(session({ secret: 'cats' })); +app.use(cookieSession({ + name : 'session', + keys : [config.get('Session.SessionKey')], + maxAge: 24 * 60 * 60 * 1000, // 24 hours +})); 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); - }); -}); +passport.serializeUser(serializeSpeechUser); // takes the user id from the db and serializes it +passport.deserializeUser(deserializeSpeechUser); // this deserializes id then populates req.user with info const localSignupStrategy = require('./passport/local-signup.js'); const localLoginStrategy = require('./passport/local-login.js'); passport.use('local-signup', localSignupStrategy); @@ -83,19 +64,7 @@ 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(); -}); +app.use(populateLocalsDotUser); // start the server db.sequelize diff --git a/views/about.handlebars b/views/about.handlebars index 9600c2f0..db20bad9 100644 --- a/views/about.handlebars +++ b/views/about.handlebars @@ -1,17 +1,20 @@ -
- {{> topBar}} -
-

About Spee.ch

-

Spee.ch is a single-serving site that reads and publishes images to and from the LBRY blockchain.

-

Spee.ch is an image hosting service, but with the added benefit that it stores your images on a decentralized network of computers -- the LBRY network. This means that your images are stored in multiple locations without a single point of failure.

- {{> examples}} - {{> documentation}} - {{> bugs}} -
- - {{> footer}} -
+
+
+
+

Spee.ch is an open-source project. Please contribute to the existing site, or fork it and make your own.

+

TWITTER

+

GITHUB

+

DISCORD CHANNEL

+

DOCUMENTATION

+
+
+
+

Spee.ch is a media-hosting site that reads and publishes content from the LBRY blockchain.

+

Spee.ch is a hosting service, but with the added benefit that it stores your content on a decentralized network of computers -- the LBRY network. This means that your images are stored in multiple locations without a single point of failure.

+

Contribute

+

If you have an idea for your own spee.ch-like site on top of LBRY, fork our github repo and go to town!

+

If you want to improve spee.ch, join our discord channel or solve one of our github issues.

+
+
- \ No newline at end of file +
diff --git a/views/channel.handlebars b/views/channel.handlebars index 9434af0c..0f81e08f 100644 --- a/views/channel.handlebars +++ b/views/channel.handlebars @@ -1,11 +1,47 @@ -
- {{> topBar}} -
-

{{this.channelName}}:{{this.longChannelId}}

-

Below is all the free content in this channel.

- {{#each this.claims}} - {{> contentListItem}} - {{/each}} -
- {{> footer}} +
+
+

Below are the contents for {{this.channelName}}:{{this.longChannelId}}

+
+ {{#each this.claims}} + {{> gridItem}} + {{/each}} +
+
+
+ First [1] +
+ {{#if this.previousPage}} + Previous + {{else}} + Previous + {{/if}} + | + {{#if this.nextPage}} + Next + {{else}} + Next + {{/if}} +
+
+
+ + + + \ No newline at end of file diff --git a/views/fourOhFour.handlebars b/views/fourOhFour.handlebars index cc91ea9f..eeb979ac 100644 --- a/views/fourOhFour.handlebars +++ b/views/fourOhFour.handlebars @@ -1,7 +1,4 @@ -
- {{> topBar}} -
-

404: Not Found

-

That page does not exist. Return home.

-
-
\ No newline at end of file +
+

404: Not Found

+

That page does not exist. Return home.

+
diff --git a/views/index.handlebars b/views/index.handlebars index 1e1bc2e1..432a33f4 100644 --- a/views/index.handlebars +++ b/views/index.handlebars @@ -1,69 +1,154 @@ - - -
-
-
- {{> topBar}} - {{> publishForm}} - {{> learnMore}} - {{> footer}} +
+
+ +
+
+
+ +

Drag & drop image or video here to publish

+

OR

+

CHOOSE FILE

+
+ +
+ +
+ - - diff --git a/views/layouts/main.handlebars b/views/layouts/main.handlebars index ea4184b4..790219f7 100644 --- a/views/layouts/main.handlebars +++ b/views/layouts/main.handlebars @@ -6,21 +6,31 @@ Spee.ch - - + - + + + {{ googleAnalytics }} - {{{ body }}} + + + + + + + + + {{> navBar}} + {{{ body }}} \ No newline at end of file diff --git a/views/layouts/show.handlebars b/views/layouts/show.handlebars index d0130e4b..694a4eb8 100644 --- a/views/layouts/show.handlebars +++ b/views/layouts/show.handlebars @@ -6,22 +6,27 @@ Spee.ch - - + {{#unless fileInfo.nsfw}} {{{addTwitterCard fileInfo.fileType openGraphInfo.source openGraphInfo.embedUrl openGraphInfo.directFileUrl}}} {{{addOpenGraph fileInfo.title fileInfo.fileType openGraphInfo.showUrl openGraphInfo.source fileInfo.description fileInfo.thumbnail}}} {{/unless}} + + {{ googleAnalytics }} - {{{ body }}} + + + + + + {{> navBar}} + {{{ body }}} + - \ No newline at end of file diff --git a/views/layouts/showlite.handlebars b/views/layouts/showlite.handlebars new file mode 100644 index 00000000..6471042e --- /dev/null +++ b/views/layouts/showlite.handlebars @@ -0,0 +1,24 @@ + + + + + + + Spee.ch + + + + + {{#unless fileInfo.nsfw}} + {{{addTwitterCard fileInfo.fileType openGraphInfo.source openGraphInfo.embedUrl openGraphInfo.directFileUrl}}} + {{{addOpenGraph fileInfo.title fileInfo.fileType openGraphInfo.showUrl openGraphInfo.source fileInfo.description fileInfo.thumbnail}}} + {{/unless}} + + {{ googleAnalytics }} + + + {{{ body }}} + + + + diff --git a/views/login.handlebars b/views/login.handlebars index 7309d518..8e6e77bb 100644 --- a/views/login.handlebars +++ b/views/login.handlebars @@ -1,23 +1,15 @@ -
- {{> topBar}} -

Log In

-
- -
-

Log in to an existing channel:

- {{>channelLoginForm}} +
+
+
+

Channels allow you to publish and group content under an identity. You can create a channel for yourself, or share one with like-minded friends. You can create 1 channel, or 100, so whether you're documenting important events, or making a public repository for cat gifs (password: '1234'), try creating a channel for it!

-
-

Create New

-
-
-

Create a brand new channel:

+
+
+

Log in to an existing channel:

+ {{>channelLoginForm}} +

Create a brand new channel:

{{>channelCreationForm}}
- {{> footer}}
- - - \ No newline at end of file diff --git a/views/new.handlebars b/views/new.handlebars deleted file mode 100644 index 8b6cbbfb..00000000 --- a/views/new.handlebars +++ /dev/null @@ -1,26 +0,0 @@ -
- {{> topBar}} -
-

New on Spee.ch

-

The 25 most recent publishes on spee.ch

- {{#each newClaims}} - - {{/each}} -
- {{> footer}} -
\ No newline at end of file diff --git a/views/noChannel.handlebars b/views/noChannel.handlebars index 585c0298..19476180 100644 --- a/views/noChannel.handlebars +++ b/views/noChannel.handlebars @@ -1,8 +1,5 @@ -
- {{> topBar}} -
-

No Claims

-

There are no free assets on this channel.

-

If you think this message is an error, contact us in the LBRY slack!

-
-
\ No newline at end of file +
+

No Channel

+

There are no published channels matching your url

+

If you think this message is an error, contact us in the LBRY Discord!

+
diff --git a/views/noClaim.handlebars b/views/noClaim.handlebars new file mode 100644 index 00000000..6cc37965 --- /dev/null +++ b/views/noClaim.handlebars @@ -0,0 +1,5 @@ +
+

No Claims

+

There are no free assets at that claim. You should publish one at spee.ch.

+

NOTE: it is possible your claim was published, but it is still being processed by the blockchain

+
diff --git a/views/noClaims.handlebars b/views/noClaims.handlebars deleted file mode 100644 index 9e918fa8..00000000 --- a/views/noClaims.handlebars +++ /dev/null @@ -1,8 +0,0 @@ -
- {{> topBar}} -
-

No Claims

-

There are no free assets at that claim. You should publish one at spee.ch.

-

NOTE: it is possible your claim was published, but it is still being processed by the blockchain

-
-
\ No newline at end of file diff --git a/views/partials/asset.handlebars b/views/partials/asset.handlebars index a963b07d..ad16bb9f 100644 --- a/views/partials/asset.handlebars +++ b/views/partials/asset.handlebars @@ -1,23 +1,29 @@ - \ No newline at end of file +{{#ifConditional fileInfo.fileType '===' 'video/mp4'}} + {{#ifConditional fileInfo.fileExt '===' 'gifv'}} + + {{else}} + + + + {{/ifConditional}} +{{else}} + + + +{{/ifConditional}} \ No newline at end of file diff --git a/views/partials/assetInfo.handlebars b/views/partials/assetInfo.handlebars index b485bc69..a79912ea 100644 --- a/views/partials/assetInfo.handlebars +++ b/views/partials/assetInfo.handlebars @@ -1,76 +1,104 @@ -
-

Title

-

{{fileInfo.title}} +{{#if fileInfo.channelName}} +

+
+ Channel: +
-