Authentication #170

Merged
bones7242 merged 43 commits from authentication into master 2017-09-29 02:29:22 +02:00
16 changed files with 282 additions and 153 deletions
Showing only changes of commit f64961446f - Show all commits

View file

@ -130,4 +130,34 @@ module.exports = {
});
});
},
checkChannelAvailability (name) {
return new Promise((resolve, reject) => {
// find any records where the name is used
db.Certificate.findAll({ where: { name } })
.then(result => {
if (result.length >= 1) {
// filter out any results that were not published from a spee.ch wallet address
getWalletList()
.then((walletList) => {
const filteredResult = result.filter((claim) => {
return walletList.includes(claim.address);
});
if (filteredResult.length >= 1) {
resolve(false);
} else {
resolve(true);
}
})
.catch((error) => {
reject(error);
});
} else {
resolve(true);
}
})
.catch(error => {
reject(error);
});
});
},
};

View file

@ -20,7 +20,7 @@ module.exports = new PassportLocalStrategy(
if (!user.validPassword(password, user.password)) {
return done(null, false, {message: 'Incorrect username or password.'});
}
return done(null, user.dataValues);
return done(null, user); // user.dataValues?
})
.catch(error => {
return done(error);

View file

@ -26,7 +26,7 @@ module.exports = new PassportLocalStrategy(
})
.then(user => {
logger.debug('User record was created successfully');
return done(null);
return done(null, user); // user.datavalues?
})
.catch(error => {
logger.debug(error);

View file

@ -54,12 +54,15 @@
width: 90%
}
#claim-name-input {
.input-text--primary {
border: 0px;
background-color: #ffffff;
border-bottom: 1px solid grey;
}
#claim-name-input:focus {
outline: none
.input-text--primary:focus {
outline: none;
border-bottom: 1px solid blue;
}
/* show routes */

View file

@ -28,29 +28,55 @@ function validateFile(file) {
}
}
// validation function that checks to make sure the claim name is not already claimed
function isNameAvailable (name) {
return new Promise(function(resolve, reject) {
// make sure the claim name is still available
var xhttp;
xhttp = new XMLHttpRequest();
xhttp.open('GET', '/api/isClaimAvailable/' + name, true);
xhttp.responseType = 'json';
xhttp.onreadystatechange = function() {
if (this.readyState == 4 ) {
if ( this.status == 200) {
if (this.response == true) {
resolve();
} else {
reject( new NameError("That name has already been claimed by another user. Please choose a different name."));
}
} else {
reject("request to check claim name failed with status:" + this.status);
};
}
};
xhttp.send();
});
function isChannelAvailable (name) {
return new Promise(function(resolve, reject) {
// make sure the claim name is still available
var xhttp;
xhttp = new XMLHttpRequest();
xhttp.open('GET', '/api/isChannelAvailable/' + name, true);
xhttp.responseType = 'json';
xhttp.onreadystatechange = function() {
if (this.readyState == 4 ) {
if ( this.status == 200) {
if (this.response == true) {
resolve();
} else {
reject( new NameError("That name has already been claimed by another user. Please choose a different name."));
}
} else {
reject("request to check claim name failed with status:" + this.status);
};
}
};
xhttp.send();
});
}
// validation function that checks to make sure the claim name is not already claimed
function isNameAvailable (name) {
return new Promise(function(resolve, reject) {
// make sure the claim name is still available
var xhttp;
xhttp = new XMLHttpRequest();
xhttp.open('GET', '/api/isClaimAvailable/' + name, true);
xhttp.responseType = 'json';
xhttp.onreadystatechange = function() {
if (this.readyState == 4 ) {
if ( this.status == 200) {
if (this.response == true) {
resolve();
} else {
reject( new NameError("That name has already been claimed by another user. Please choose a different name."));
}
} else {
reject("request to check claim name failed with status:" + this.status);
};
}
};
xhttp.send();
});
}
// validation function that checks to make sure the claim name is valid
function validateClaimName (name) {
// ensure a name was entered
@ -89,6 +115,31 @@ function checkClaimName(name){
document.getElementById('claim-name-available').hidden = true;
}
}
// validaiton function to check claim name as the input changes
function checkChannelName(event){
console.log(event);
const name = event.target.value;
const target = document.getElementById(event.target.id);
const errorDisplay = target.parentNode.firstChild;
console.log('error display:', errorDisplay)
try {
// check to make sure it is available
isChannelAvailable(name)
.then(function() {
errorDisplay.hidden = false;
})
.catch(function(error) {
errorDisplay.hidden = false;
showError(errorDisplay.getAttribute('id'), error.message);
});
} catch (error) {
console.log(error.message);
document.getElementById(errorDisplay.getAttribute('id')).hidden = true;
}
}
// validation function which checks all aspects of the publish submission
function validateSubmission(stagedFiles, name){
return new Promise(function (resolve, reject) {
@ -118,4 +169,4 @@ function validateSubmission(stagedFiles, name){
reject(error);
});
});
}
}

View file

@ -3,7 +3,7 @@ const multipart = require('connect-multiparty');
const multipartMiddleware = multipart();
const { publish } = require('../controllers/publishController.js');
const { getClaimList, resolveUri } = require('../helpers/lbryApi.js');
const { createPublishParams, validateFile, checkNameAvailability } = require('../helpers/publishHelpers.js');
const { createPublishParams, validateFile, checkNameAvailability, checkChannelAvailability } = require('../helpers/publishHelpers.js');
const errorHandlers = require('../helpers/errorHandlers.js');
const { postToStats, sendGoogleAnalytics } = require('../controllers/statsController.js');
@ -38,6 +38,22 @@ module.exports = (app, hostedContentPath) => {
res.status(500).json(error);
});
});
// route to check whether spee.ch has published to a channel
app.get('/api/isChannelAvailable/:name', ({ ip, originalUrl, params }, res) => {
// send response
checkChannelAvailability(params.name)
.then(result => {
if (result === true) {
res.status(200).json(true);
} else {
logger.debug(`Rejecting publish request because ${params.name} has already been published via spee.ch`);
res.status(200).json(false);
}
})
.catch(error => {
res.status(500).json(error);
});
});
// route to run a resolve request on the daemon
app.get('/api/resolve/:uri', ({ headers, ip, originalUrl, params }, res) => {
// google analytics

View file

@ -1,30 +1,35 @@
const errorHandlers = require('../helpers/errorHandlers.js');
const db = require('../models');
const { postToStats, getStatsSummary, getTrendingClaims, getRecentClaims } = require('../controllers/statsController.js');
const passport = require('passport');
// const { deAuthenticate } = require('../auth/authentication.js');
const logger = require('winston');
module.exports = (app) => {
// route for auth
app.post('/signup', passport.authenticate('local-signup'), (req, res) => {
console.log('redirecting to user channel');
// If this function gets called, authentication was successful.
logger.debug('redirecting to user channel');
// If this function gets called, signup was successful.
// `req.user` contains the authenticated user.
res.redirect('/@' + req.user.channelName);
});
app.post('/login', passport.authenticate('local-login'), (req, res) => {
console.log('redirecting to user channel');
// If this function gets called, authentication was successful.
logger.debug('redirecting to user channel');
// If this function gets called, login was successful.
// `req.user` contains the authenticated user.
res.redirect('/@' + req.user.channelName);
});
// route to log out
app.get('/logout', (req, res) => {
req.logout();
res.redirect('/login');
});
// route to display login page
app.get('/login', (req, res) => {
res.status(200).render('login');
});
app.get('/signup', (req, res) => {
res.status(200).render('signup');
if (req.user) {
res.status(200).redirect(`/@${req.user.channelName}`);
} else {
res.status(200).render('login');
}
});
// route to display login page
// app.get('/users/:name', isAuthenticated, (req, res) => {
@ -50,7 +55,9 @@ module.exports = (app) => {
getTrendingClaims(dateTime)
.then(result => {
// logger.debug(result);
res.status(200).render('trending', { trendingAssets: result });
res.status(200).render('trending', {
trendingAssets: result,
});
})
.catch(error => {
errorHandlers.handleRequestError(error, res);
@ -61,21 +68,26 @@ module.exports = (app) => {
getRecentClaims()
.then(result => {
// logger.debug(result);
res.status(200).render('new', { newClaims: result });
res.status(200).render('new', {
newClaims: result,
});
})
.catch(error => {
errorHandlers.handleRequestError(error, res);
});
});
// route to show statistics for spee.ch
app.get('/stats', ({ ip, originalUrl }, res) => {
app.get('/stats', ({ ip, originalUrl, user }, res) => {
// get and render the content
const startDate = new Date();
startDate.setDate(startDate.getDate() - 1);
getStatsSummary(startDate)
.then(result => {
postToStats('show', originalUrl, ip, null, null, 'success');
res.status(200).render('statistics', result);
res.status(200).render('statistics', {
user,
result,
});
})
.catch(error => {
errorHandlers.handleRequestError(error, res);
@ -91,20 +103,8 @@ module.exports = (app) => {
res.status(200).render('embed', { layout: 'embed', claimId, name });
});
// route to display all free public claims at a given name
app.get('/:name/all', ({ ip, originalUrl, params }, res) => {
app.get('/:name/all', (req, res) => {
// get and render the content
db
.getAllFreeClaims(params.name)
.then(orderedFreeClaims => {
if (!orderedFreeClaims) {
res.status(307).render('noClaims');
return;
}
postToStats('show', originalUrl, ip, null, null, 'success');
res.status(200).render('allClaims', { claims: orderedFreeClaims });
})
.catch(error => {
errorHandlers.handleRequestError('show', originalUrl, ip, error, res);
});
res.status(410).send('/:name/all is no longer supported');
});
};

View file

@ -22,35 +22,35 @@ require('./config/slackLoggerConfig.js')(logger);
// trust the proxy to get ip address for us
app.enable('trust proxy');
// add middleware
app.use(helmet()); // set HTTP headers to protect against well-known web vulnerabilties
app.use(express.static(`${__dirname}/public`)); // 'express.static' to serve static files from public directory
app.use(bodyParser.json()); // 'body parser' for parsing application/json
app.use(bodyParser.urlencoded({ extended: true })); // 'body parser' for parsing application/x-www-form-urlencoded
app.use(siofu.router); // 'socketio-file-upload' router for uploading with socket.io
app.use((req, res, next) => { // custom logging middleware to log all incomming http requests
app.use((req, res, next) => { // custom logging middleware to log all incoming http requests
logger.verbose(`Request on ${req.originalUrl} from ${req.ip}`);
next();
});
// initialize passport
app.use(session({ secret: 'cats' }));
app.use(passport.initialize());
app.use(passport.session());
passport.serializeUser(function (user, done) {
passport.serializeUser((user, done) => {
done(null, user.id);
});
passport.deserializeUser(function (id, done) {
passport.deserializeUser((id, done) => {
db.User.findOne({ where: { id } })
.then(user => {
done(null, user.dataValues);
done(null, user); // user.dataValues?
})
.catch(error => {
logger.error('sequelize error', error);
done(error, null);
});
});
// Load passport strategies
const localSignupStrategy = require('./passport/local-signup.js');
const localLoginStrategy = require('./passport/local-login.js');
passport.use('local-signup', localSignupStrategy);
@ -65,6 +65,16 @@ const hbs = expressHandlebars.create({
app.engine('handlebars', hbs.engine);
app.set('view engine', 'handlebars');
// middleware to pass user info back to client, if user is logged in
app.use((req, res, next) => {
if (req.user) {
res.locals.user = {
id : req.user.id,
channelName: req.user.channelName,
};
}
next();
});
// start the server
db.sequelize
.sync() // sync sequelize

View file

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

View file

@ -1,20 +1,39 @@
<div class="wrapper">
{{> topBar}}
<div class="full">
<h2>Log In</h2>
<p>Log in to an existing channel</p>
<form id="login-form" action="/login" method="post">
<div>
<label>Username:</label>
<input type="text" name="username"/>
@ <input type="text" name="username" class="input-text--primary"/>
</div>
<div>
<label>Password:</label>
<input type="password" name="password"/>
<input type="password" name="password" class="input-text--primary"/>
</div>
<div>
<input type="submit" value="Log In"/>
</div>
</form>
<h2>Create New</h2>
<p>Create a brand new channel</p>
<form action="/signup" method="post">
<div>
<label>Channel name:</label>
@ <input type="text" name="username" class="input-text--primary"/>
</div>
<div>
<label>Password:</label>
<input type="password" name="password" class="input-text--primary"/>
</div>
<div>
<input type="submit" value="Create"/>
</div>
</form>
</div>
{{> footer}}
</div>

View file

@ -0,0 +1,38 @@
<div id="claim-channel-input-area">
<label for="publish-channel">Channel:</label></td>
<select type="text" id="publish-channel" name="channel" value="channel" onclick="check(event)">
{{#if user}}
<option value="@{{user.channelName}}" >@{{user.channelName}}</option>
{{/if}}
<option value="@speech" >Anonymous</option>
<option value="new" >New</option>
</select>
<div id="channel-create-details" hidden="true"><p id="test" style="color: red;"></p>
<label for="channelName">Channel Name: </label>
@<input type="text" id="channel-name-input" class="input-text--primary" oninput="checkChannelName(event)">
<br/>
<label for="password" >Password: </label>
<input type="text" id="password" class="input-text--primary">
<br/>
<button >create</button>
</div>
</div>
<script type="text/javascript">
// toggle channel creation tool
const createChannelTool = document.getElementById('channel-create-details');
function check(event) {
const selectedOption = event.target.selectedOptions[0].value;
console.log(selectedOption);
if (selectedOption === 'new') {
createChannelTool.hidden = false;
} else {
createChannelTool.hidden = true;
}
}
function createChannel() {
}
</script>

View file

@ -0,0 +1,28 @@
<p>Details
<a class="toggle-link" id="details-toggle" href="#" onclick="toggleSection(event)" data-open="false" data-slaveelementid="details-detail">[open]</a>
</p>
<div id="details-detail" hidden="true">
<table>
<tr>
<td><label for="publish-title">Title: </label></td>
<td><input type="text" id="publish-title" class="input-text--primary"></td>
</tr>
<tr>
<td><label for="publish-description">Description: </label></td>
<td><textarea rows="2" id="publish-description"> </textarea></td>
</tr>
<tr>
<td><label for="publish-license">License:* </label></td>
<td>
<select type="text" id="publish-license" name="license" value="license">
<option value="Public Domain">Public Domain</option>
<option value="Creative Commons">Creative Commons</option>
</select>
</td>
</tr>
<tr>
<td><label for="publish-nsfw">NSFW*</label></td>
<td><input type="checkbox" id="publish-nsfw"></td>
</tr>
</table>
</div>

View file

@ -12,41 +12,24 @@
<div class="col-right">
<div id="publish-active-area">
<div class="input-error" id="input-error-claim-name" hidden="true"></div>
<div id="claim-name-input-area">
Spee.ch/<input type="text" id="claim-name-input" placeholder="your-url-here" oninput="checkClaimName(event.target.value)">
<span id="claim-name-available" hidden="true" style="color: green">&#x2714</span>
</div>
<div class="stop-float">
<table>
<tr>
<td><label for="publish-title">Title: </label></td>
<td><input type="text" id="publish-title" class="publish-input"></td>
</tr>
<tr>
<td><label for="publish-description">Description: </label></td>
<td><textarea rows="2" id="publish-description" class="publish-input"> </textarea></td>
</tr>
<tr>
<td><label for="publish-license">License:* </label></td>
<td>
<select type="text" id="publish-license" name="license" value="license">
<option value="Public Domain">Public Domain</option>
<option value="Creative Commons">Creative Commons</option>
</select>
</td>
</tr>
<tr>
<td><label for="publish-nsfw">NSFW*</label></td>
<td><input type="checkbox" id="publish-nsfw"></td>
</tr>
</table>
<p>
Spee.ch/
<input type="text" id="claim-name-input" class="input-text--primary" placeholder="your-url-here" oninput="checkClaimName(event.target.value)">
<span id="claim-name-available" hidden="true" style="color: green">&#x2714</span>
</p>
</div>
<div class="stop-float">
{{> publishChannel}}
</div>
<div class="stop-float">
{{> publishDetails}}
</div>
<p>
<div class="input-error" id="input-error-publish-submit" hidden="true"></div>
<button id="publish-submit" onclick="publishSelectedImage(event)">Publish</button>
<button onclick="resetPublishArea()">Reset</button>
<div class="input-error" id="input-error-publish-submit" hidden="true"></div>
<button id="publish-submit" onclick="publishSelectedImage(event)">Publish</button>
<button onclick="resetPublishArea()">Reset</button>
</p>
<p><i>By clicking 'Publish' I attest that I have read and agree to the <a href="https://lbry.io/termsofservice" target="_blank">LBRY terms of service</a>.</i></p>
</div>
</div>
</div>
@ -73,7 +56,5 @@
document.getElementById('input-error-claim-name').innerHTML = '';
document.getElementById('input-error-publish-submit').innerHTML = '';
document.getElementById('claim-name-available').hidden = true;
// remove nsfw check
}
</script>

View file

@ -4,5 +4,12 @@
<a href="/popular" class="top-bar-right">popular</a>
<a href="https://github.com/lbryio/spee.ch" target="_blank" class="top-bar-right">source</a>
<a href="/about" class="top-bar-right">help</a>
{{#if user}}
<a href="/@{{user.channelName}}" class="top-bar-right">@{{user.channelName}}</a>
<a href="/logout" class="top-bar-right">logout</a>
{{else}}
<a href="/login" class="top-bar-right">login</a>
{{/if}}
<div class="top-bar-tagline">Open-source, decentralized image and video hosting.</div>
</div>

View file

@ -1,11 +0,0 @@
<div class="wrapper">
{{> topBar}}
<div class="full">
{{#if isAuthenticated}}
{{> profile }}
{{else}}
{{> loginForm}}
{{/if}}
</div>
{{> footer}}
</div>

View file

@ -1,20 +0,0 @@
<div class="wrapper">
{{> topBar}}
<div class="full">
<h2>Sign up</h2>
<form action="/signup" method="post">
<div>
<label>Username:</label>
<input type="text" name="username"/>
</div>
<div>
<label>Password:</label>
<input type="password" name="password"/>
</div>
<div>
<input type="submit" value="Log In"/>
</div>
</form>
</div>
{{> footer}}
</div>