added channel select to publish tool
This commit is contained in:
parent
0e9147d9d6
commit
f64961446f
16 changed files with 282 additions and 153 deletions
|
@ -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);
|
||||
});
|
||||
});
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 */
|
||||
|
|
|
@ -27,6 +27,31 @@ function validateFile(file) {
|
|||
throw new Error(file.type + ' is not a supported file type. Only, .jpeg, .png, .gif, and .mp4 files are currently supported.')
|
||||
}
|
||||
}
|
||||
// validation function that checks to make sure the claim name is not already claimed
|
||||
function 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) {
|
||||
|
@ -51,6 +76,7 @@ function isNameAvailable (name) {
|
|||
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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) => {
|
||||
if (req.user) {
|
||||
res.status(200).redirect(`/@${req.user.channelName}`);
|
||||
} else {
|
||||
res.status(200).render('login');
|
||||
});
|
||||
app.get('/signup', (req, res) => {
|
||||
res.status(200).render('signup');
|
||||
}
|
||||
});
|
||||
// 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');
|
||||
});
|
||||
};
|
||||
|
|
24
speech.js
24
speech.js
|
@ -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
|
||||
|
|
|
@ -1,23 +0,0 @@
|
|||
<div class="wrapper">
|
||||
{{> topBar}}
|
||||
<div>
|
||||
<h3>All Claims</h3>
|
||||
<p>These are all the free, public assets at that claim. You can publish more at <a href="/">spee.ch</a>.</p>
|
||||
{{#each claims}}
|
||||
<div class="all-claims-item">
|
||||
<img class="all-claims-asset" src="/{{this.claimId}}/{{this.name}}.test" />
|
||||
<div class="all-claims-details">
|
||||
<ul style="list-style-type:none">
|
||||
<li>claim: {{this.name}}</li>
|
||||
<li>claim_id: {{this.claim_id}}</li>
|
||||
<li>link: <a href="/{{this.claimId}}/{{this.name}}">spee.ch/{{this.name}}/{{this.claimId}}</a></li>
|
||||
<li>author: {{this.value.stream.metadata.author}}</li>
|
||||
<li>description: {{this.value.stream.metadata.description}}</li>
|
||||
<li>license: {{this.value.stream.metadata.license}}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{{/each}}
|
||||
</div>
|
||||
{{> footer}}
|
||||
</div>
|
|
@ -1,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>
|
||||
|
|
38
views/partials/publishChannel.handlebars
Normal file
38
views/partials/publishChannel.handlebars
Normal 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>
|
28
views/partials/publishDetails.handlebars
Normal file
28
views/partials/publishDetails.handlebars
Normal 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>
|
|
@ -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)">
|
||||
<div class="stop-float">
|
||||
<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">✔</span>
|
||||
</p>
|
||||
</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>
|
||||
{{> 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>
|
||||
</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>
|
||||
|
|
|
@ -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>
|
|
@ -1,11 +0,0 @@
|
|||
<div class="wrapper">
|
||||
{{> topBar}}
|
||||
<div class="full">
|
||||
{{#if isAuthenticated}}
|
||||
{{> profile }}
|
||||
{{else}}
|
||||
{{> loginForm}}
|
||||
{{/if}}
|
||||
</div>
|
||||
{{> footer}}
|
||||
</div>
|
|
@ -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>
|
Loading…
Reference in a new issue