Merge branch 'master' into next

This commit is contained in:
Wyatt Johnson
2018-01-05 11:10:32 -07:00
24 changed files with 381 additions and 330 deletions
+1 -1
View File
@@ -28,5 +28,5 @@ plugins/*
!plugins/talk-plugin-deep-reply-count
!plugins/talk-plugin-subscriber
!plugins/talk-plugin-flag-details
public
node_modules
+1 -1
View File
@@ -2,7 +2,7 @@
"exec": "npm-run-all --parallel generate-introspection start:development",
"verbose": true,
"ignore": ["test/*", "client/*", "dist/*", "plugins/*/client"],
"ext": "js,json,graphql",
"ext": "js,json,graphql,yml",
"watch": [
".",
"bin/cli",
+3 -5
View File
@@ -207,13 +207,11 @@ const CONFIG = {
// SMTP Server configuration
//------------------------------------------------------------------------------
SMTP_FROM_ADDRESS: process.env.TALK_SMTP_FROM_ADDRESS,
SMTP_HOST: process.env.TALK_SMTP_HOST,
SMTP_PASSWORD: process.env.TALK_SMTP_PASSWORD,
SMTP_PORT: process.env.TALK_SMTP_PORT
? parseInt(process.env.TALK_SMTP_PORT)
: undefined,
SMTP_USERNAME: process.env.TALK_SMTP_USERNAME,
SMTP_PORT: process.env.TALK_SMTP_PORT,
SMTP_PASSWORD: process.env.TALK_SMTP_PASSWORD,
SMTP_FROM_ADDRESS: process.env.TALK_SMTP_FROM_ADDRESS,
//------------------------------------------------------------------------------
// Flagging Config
+12 -3
View File
@@ -69,9 +69,17 @@ const ErrMissingUsername = new APIError('A username is required to create a user
status: 400
});
// ErrMissingToken is returned in the event that the password reset is requested
// ErrEmailVerificationToken is returned in the event that the password reset is requested
// without a token.
const ErrMissingToken = new APIError('token is required', {
const ErrEmailVerificationToken = new APIError('token is required', {
translation_key: 'EMAIL_VERIFICATION_TOKEN_INVALID',
status: 400
});
// ErrPasswordResetToken is returned in the event that the password reset is requested
// without a token.
const ErrPasswordResetToken = new APIError('token is required', {
translation_key: 'PASSWORD_RESET_TOKEN_INVALID',
status: 400
});
@@ -225,7 +233,8 @@ module.exports = {
ErrMaxRateLimit,
ErrMissingEmail,
ErrMissingPassword,
ErrMissingToken,
ErrEmailVerificationToken,
ErrPasswordResetToken,
ErrMissingUsername,
ErrNotAuthorized,
ErrNotFound,
+11
View File
@@ -19,6 +19,15 @@ en:
email_message_ban: "Dear {0},\n\nSomeone with access to your account has violated our community guidelines. As a result, your account has been banned. You will no longer be able to comment, like or report comments. if you think this has been done in error, please contact our community team."
bio_offensive: "This bio is offensive"
cancel: "Cancel"
confirm_email:
click_to_confirm: "Click below to confirm your email address"
confirm: "Confirm"
password_reset:
set_new_password: "Change Your Password"
new_password: "New Password"
new_password_help: "Password must be at least 8 characters"
confirm_new_password: "Confirm New Password"
change_password: "Change Password"
characters_remaining: "characters remaining"
comment:
anon: "Anonymous"
@@ -189,6 +198,8 @@ en:
embedlink:
copy: "Copy to Clipboard"
error:
EMAIL_VERIFICATION_TOKEN_INVALID: "Email verification token is invalid."
PASSWORD_RESET_TOKEN_INVALID: "Your password reset link is invalid."
COMMENT_TOO_SHORT: "Comments should be more than one character, please revise your comment and try again."
NOT_AUTHORIZED: "You are not authorized to perform this action."
NO_SPECIAL_CHARACTERS: "Usernames can contain letters numbers and _ only"
+17 -5
View File
@@ -1,3 +1,5 @@
const SettingsService = require('../services/settings');
const {
BASE_URL,
BASE_PATH,
@@ -24,8 +26,8 @@ const TEMPLATE_LOCALS = {
},
};
// attachLocals will attach the locals to the response only.
const attachLocals = (locals) => {
// attachStaticLocals will attach the locals to the response only.
const attachStaticLocals = (locals) => {
for (const key in TEMPLATE_LOCALS) {
const value = TEMPLATE_LOCALS[key];
@@ -33,13 +35,23 @@ const attachLocals = (locals) => {
}
};
module.exports = (req, res, next) => {
module.exports = async (req, res, next) => {
try {
// Attach the custom css url.
const {customCssUrl} = await SettingsService.retrieve('customCssUrl');
res.locals.customCssUrl = customCssUrl;
} catch (err) {
console.warn(err);
}
// Always attach the locals.
attachLocals(res.locals);
attachStaticLocals(res.locals);
// Forward the request.
next();
};
module.exports.attachLocals = attachLocals;
module.exports.attachStaticLocals = attachStaticLocals;
module.exports.TEMPLATE_LOCALS = TEMPLATE_LOCALS;
+71
View File
@@ -0,0 +1,71 @@
body, #root {
width: 100%;
height: 100%;
margin: 0;
background: #fff;
}
.container {
max-width: 300px;
margin: 50px auto;
}
#root form {
display: none;
padding: 15px;
}
.legend {
text-align: center;
width: 100%;
font-weight: bold;
}
label {
display: block;
margin-top: 10px;
margin-bottom: 3px;
padding-right: 30px;
}
small {
color: #888;
}
input {
border-radius: 4px;
margin-top: 3px;
border: 1px solid lightgrey;
font-size: 16px;
width: 100%;
padding: 14px;
height: 100%;
display: inline-block;
}
button[type="submit"] {
border-radius: 4px;
border: none;
display: block;
background-color: #333;
color: white;
text-align: center;
width: 100%;
padding: 10px;
margin-top: 10px;
cursor: pointer;
}
.error-console {
display: none;
margin-top: 10px;
border-radius: 4px;
background-color: pink;
color: red;
border: 1px solid red;
padding: 10px;
}
.error-console.active {
display: block;
}
+8
View File
@@ -0,0 +1,8 @@
function showError(error) {
try {
let err = JSON.parse(error);
$('.error-console').text(err.message).addClass('active');
} catch (err) {
$('.error-console').text(error).addClass('active');
}
}
-6
View File
@@ -1,17 +1,11 @@
const express = require('express');
const router = express.Router();
// Get /email-confirmation expects a signed JWT in the hash
router.get('/confirm-email', (req, res) => {
res.render('admin/confirm-email');
});
// Get /password-reset expects a signed token (JWT) in the hash.
// Links to this endpoint are generated by /views/password-reset-email.ejs.
router.get('/password-reset', (req, res) => {
// TODO: store the redirect uri in the token or something fancy.
// admins and regular users should probably be redirected to different places.
res.render('admin/password-reset');
});
+46 -35
View File
@@ -13,31 +13,56 @@ router.get('/', authorization.needed(), (req, res, next) => {
res.json(req.user);
});
/**
* verifyTokenOnCheck will verify that the request contains a token, and if
* being checked, will return the check status to the user.
*
* @param {Function} verifier the function used to verify the token, will throw on error
* @param {Object} error the error object to send back in the event an error is found
*/
const tokenCheck = (verifier, error) => async (req, res, next) => {
const {token = null, check = false} = req.body;
if (check) {
// This request is checking to see if the token is valid.
try {
// Verify the token.
await verifier(token);
} catch (err) {
// Log out the error, slurp it and send out the predefined error to the
// error handler.
console.error(err);
return next(error);
}
res.status(204).end();
// Don't continue to pass it onto the next middleware, as we've only been
// asked to verify the token.
return;
}
next();
};
// POST /email/confirm takes the password confirmation token available as a
// payload parameter and if it verifies, it updates the confirmed_at date on the
// local profile.
router.post('/email/verify', async (req, res, next) => {
const {
token
} = req.body;
if (!token) {
return next(errors.ErrMissingToken);
}
router.post('/email/verify', tokenCheck(UsersService.verifyEmailConfirmationToken, errors.ErrEmailVerificationToken), async (req, res, next) => {
const {token} = req.body;
try {
let {referer} = await UsersService.verifyEmailConfirmation(token);
res.json({redirectUri: referer});
} catch (e) {
return next(e);
return res.json({redirectUri: referer});
} catch (err) {
console.error(err);
return next(errors.ErrEmailVerificationToken);
}
});
/**
* this endpoint takes an email (username) and checks if it belongs to a User account
* if it does, create a JWT and send an email
*/
router.post('/password/reset', async (req, res, next) => {
const {email, loc} = req.body;
@@ -48,7 +73,7 @@ router.post('/password/reset', async (req, res, next) => {
try {
let token = await UsersService.createPasswordResetToken(email, loc);
if (token) {
await mailer.sendSimple({
await mailer.send({
template: 'password-reset',
locals: {
token,
@@ -64,34 +89,20 @@ router.post('/password/reset', async (req, res, next) => {
}
});
/**
* expects 2 fields in the body of the request
* 1) the token that was in the url of the email link {String}
* 2) the new password {String}
*/
router.put('/password/reset', async (req, res, next) => {
const {check} = req.query;
router.put('/password/reset', tokenCheck(UsersService.verifyPasswordResetToken, errors.ErrPasswordResetToken), async (req, res, next) => {
const {token, password} = req.body;
if (!token) {
return next(errors.ErrMissingToken);
}
if (check !== 'true' && (!password || password.length < 8)) {
if (!password || password.length < 8) {
return next(errors.ErrPasswordTooShort);
}
try {
let [user, loc] = await UsersService.verifyPasswordResetToken(token);
if (check === 'true') {
res.status(204).end();
return;
}
let [user, redirect] = await UsersService.verifyPasswordResetToken(token);
// Change the users' password.
await UsersService.changePassword(user.id, password);
res.json({redirect: loc});
res.json({redirect});
} catch (e) {
console.error(e);
return next(errors.ErrNotAuthorized);
+2 -10
View File
@@ -1,16 +1,8 @@
const express = require('express');
const router = express.Router();
const SettingsService = require('../../services/settings');
router.use('/:embed', async (req, res, next) => {
switch (req.params.embed) {
case 'stream': {
const {customCssUrl} = await SettingsService.retrieve('customCssUrl');
return res.render('embed/stream', {customCssUrl});
}
}
return next();
router.use('/stream', (req, res) => {
res.render('embed/stream');
});
module.exports = router;
+2 -2
View File
@@ -174,7 +174,7 @@ router.use('/api', (err, req, res, next) => {
if (err instanceof errors.APIError) {
res.status(err.status).json({
message: err.message,
message: res.locals.t(`error.${err.translation_key}`),
error: err
});
} else {
@@ -190,7 +190,7 @@ router.use('/', (err, req, res, next) => {
if (err instanceof errors.APIError) {
res.status(err.status);
res.render('error', {
message: err.message,
message: res.locals.t(`error.${err.translation_key}`),
error: process.env.NODE_ENV === 'development' ? err : {}
});
} else {
+1 -1
View File
@@ -4,6 +4,6 @@
<%= t('email.confirm.to_confirm') %>
<%= BASE_URL %>confirm/endpoint#<%= token %>
<%= BASE_URL %>admin/confirm-email#<%= token %>
<%= t('email.confirm.if_you_did_not') %>
+95 -91
View File
@@ -4,7 +4,7 @@ const kue = require('./kue');
const path = require('path');
const fs = require('fs');
const _ = require('lodash');
const {attachLocals} = require('../middleware/staticTemplate');
const {TEMPLATE_LOCALS} = require('../middleware/staticTemplate');
const i18n = require('./i18n');
@@ -54,102 +54,106 @@ templates.render = (name, format = 'txt', context) => new Promise((resolve, reje
return resolve(view(context));
});
}); // ends templates.render
});
const options = {
host: SMTP_HOST,
auth: {
user: SMTP_USERNAME,
pass: SMTP_PASSWORD
}
};
const mailer = {};
if (SMTP_PORT) {
try {
options.port = parseInt(SMTP_PORT);
} catch (e) {
throw new Error('TALK_SMTP_PORT is not an integer');
// enabled is true when the required configuration is available. When testing
// is enabled, we will be simulating that emails are being sent, because in a
// production system, emails should and would be sent.
mailer.enabled = Boolean(
SMTP_HOST &&
SMTP_USERNAME &&
SMTP_PORT &&
SMTP_PASSWORD &&
SMTP_FROM_ADDRESS
) || process.env.NODE_ENV === 'test';
if (mailer.enabled) {
const options = {
host: SMTP_HOST,
auth: {
user: SMTP_USERNAME,
pass: SMTP_PASSWORD
}
};
if (SMTP_PORT) {
try {
options.port = parseInt(SMTP_PORT);
} catch (e) {
throw new Error('TALK_SMTP_PORT is not an integer');
}
} else {
options.port = 25;
}
} else {
options.port = 25;
mailer.transport = nodemailer.createTransport(options);
}
const defaultTransporter = nodemailer.createTransport(options);
/**
* Create the new Task kue.
*/
mailer.task = new kue.Task({
name: 'mailer'
});
const mailer = module.exports = {
/**
* Create the new Task kue.
*/
task: new kue.Task({
name: 'mailer'
}),
sendSimple({template, locals, to, subject}) {
if (!to) {
return Promise.reject('sendSimple requires a comma-separated list of "to" addresses');
}
if (!subject) {
return Promise.reject('sendSimple requires a subject for the email');
}
// Prefix the subject with `[Talk]`.
subject = `${EMAIL_SUBJECT_PREFIX} ${subject}`;
attachLocals(locals);
// Attach the translation function.
locals.t = i18n.t;
return Promise.all([
// Render the HTML version of the email.
templates.render(template, 'html', locals),
// Render the TEXT version of the email.
templates.render(template, 'txt', locals)
])
.then(([html, text]) => {
// Create the job.
return mailer.task.create({
title: 'Mail',
message: {
to,
subject,
text,
html
}
});
});
},
/**
* Start the queue processor for the mailer job.
*/
process() {
debug(`Now processing ${mailer.task.name} jobs`);
return mailer.task.process(({id, data}, done) => {
debug(`Starting to send mail for Job[${id}]`);
// Set the `from` field.
data.message.from = SMTP_FROM_ADDRESS;
// Actually send the email.
defaultTransporter.sendMail(data.message, (err) => {
if (err) {
debug(`Failed to send mail for Job[${id}]:`, err);
return done(err);
}
debug(`Finished sending mail for Job[${id}]`);
return done();
});
});
/**
* send will create a new message and send it.
*/
mailer.send = async (options) => {
if (!mailer.enabled) {
throw new Error('email is not enabled because required configuration is not available');
}
// Create the new locals object and attach the static locals and the i18n
// framework.
const locals = _.merge({}, options.locals, TEMPLATE_LOCALS, {t: i18n.t});
// Render the templates.
const [
html,
text,
] = await Promise.all(['html', 'txt'].map((fmt) => {
return templates.render(options.template, fmt, locals);
}));
// Create the job.
return mailer.task.create({
title: 'Mail',
message: {
to: options.to,
subject: `${EMAIL_SUBJECT_PREFIX} ${options.subject}`,
text,
html
}
});
};
/**
* Start the queue processor for the mailer job.
*/
mailer.process = () => {
debug(`Now processing ${mailer.task.name} jobs`);
return mailer.task.process(({id, data}, done) => {
debug(`Starting to send mail for Job[${id}]`);
// Set the `from` field.
data.message.from = SMTP_FROM_ADDRESS;
// Actually send the email.
mailer.transport.sendMail(data.message, (err) => {
if (err) {
debug(`Failed to send mail for Job[${id}]:`, err);
return done(err);
}
debug(`Finished sending mail for Job[${id}]`);
return done();
});
});
};
module.exports = mailer;
+50 -6
View File
@@ -452,7 +452,7 @@ class UsersService {
redirectURI
);
return MailerService.sendSimple({
return MailerService.send({
template: 'email-confirm',
locals: {
token,
@@ -478,7 +478,7 @@ class UsersService {
to,
});
return MailerService.sendSimple(options);
return MailerService.send(options);
}
static async changePassword(id, password) {
@@ -741,10 +741,16 @@ class UsersService {
}
/**
* Verifies a jwt and returns the associated user.
* Verifies a jwt and returns the associated user. Throws an error when the
* token isn't valid.
*
* @param {String} token the JSON Web Token to verify
*/
static async verifyPasswordResetToken(token) {
if (!token) {
throw new Error('cannot verify an empty token');
}
const {userId, loc, version} = await UsersService.verifyToken(token, {
subject: PASSWORD_RESET_JWT_SUBJECT,
});
@@ -851,6 +857,46 @@ class UsersService {
);
}
/**
* verifyEmailConfirmationToken checks the validity of a given token without
* actually confirming the user's email address.
*
* @param {String} token the token to verify
*/
static async verifyEmailConfirmationToken(token) {
if (!token) {
throw new Error('cannot verify an empty token');
}
const decoded = await UsersService.verifyToken(token, {
subject: EMAIL_CONFIRM_JWT_SUBJECT
});
const user = await UserModel.findOne({
id: decoded.userID,
profiles: {
$elemMatch: {
id: decoded.email,
provider: 'local',
},
},
});
if (!user) {
throw errors.ErrNotFound;
}
const profile = user.profiles.find(({id}) => id === decoded.email);
if (!profile) {
throw errors.ErrNotFound;
}
if (profile.metadata && profile.metadata.confirmed_at !== null) {
throw errors.ErrEmailVerificationToken;
}
return decoded;
}
/**
* This verifies that a given token was for the email confirmation and updates
* that user's profile with a 'confirmed_at' parameter with the current date.
@@ -859,9 +905,7 @@ class UsersService {
* @return {Promise}
*/
static async verifyEmailConfirmation(token) {
let {userID, email, referer} = await UsersService.verifyToken(token, {
subject: EMAIL_CONFIRM_JWT_SUBJECT,
});
let {userID, email, referer} = await UsersService.verifyEmailConfirmationToken(token);
await UsersService.confirmEmail(userID, email);
@@ -22,7 +22,7 @@ describe('graph.mutations.banUser', () => {
let spy;
before(() => {
spy = sinon.spy(MailerService, 'sendSimple');
spy = sinon.spy(MailerService, 'send');
});
afterEach(() => {
@@ -24,7 +24,7 @@ describe('graph.mutations.suspendUser', () => {
let spy;
before(() => {
spy = sinon.spy(MailerService, 'sendSimple');
spy = sinon.spy(MailerService, 'send');
});
afterEach(() => {
+1 -1
View File
@@ -54,7 +54,7 @@ describe('/api/v1/auth/local', () => {
.catch((err) => {
expect(err).to.not.be.null;
expect(err.response).to.have.status(401);
expect(err.response.body).to.have.property('message', 'not authorized');
expect(err.response.body).to.have.property('message', 'You are not authorized to perform this action.');
});
});
+2 -2
View File
@@ -29,11 +29,11 @@ describe('services.UsersService', () => {
password: '3Coral!3'
}]);
sinon.spy(MailerService, 'sendSimple');
sinon.spy(MailerService, 'send');
});
afterEach(() => {
MailerService.sendSimple.restore();
MailerService.send.restore();
});
describe('#findById()', () => {
+5 -2
View File
@@ -34,12 +34,15 @@
height: 100%;
}
</style>
<%_ if (locals.customCssUrl) { _%>
<link href="<%= customCssUrl %>" rel="stylesheet" type="text/css">
<%_ } _%>
<% if (data != null) { %>
<script id="data" type="application/json"><%- JSON.stringify(data) %></script>
<script id="data" type="application/json"><%- JSON.stringify(data) %></script>
<% } %>
<base href="<%= BASE_URL %>"/>
</head>
<body>
<body class="admin-page">
<div id="root"></div>
<script src='https://www.google.com/recaptcha/api.js?render=explicit' async defer></script>
<script src="<%= STATIC_URL %>static/coral-admin/bundle.js" charset="utf-8"></script>
+26 -58
View File
@@ -6,68 +6,25 @@
<title>Email Verification</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
<link rel="stylesheet" href="https://code.getmdl.io/1.2.1/material.indigo-pink.min.css">
<style media="screen">
#root {
max-width: 400px;
padding-top: 100px;
margin: 0 auto;
background: #fff;
}
.coral-card-wide > .mdl-card__title {
color: #fff;
height: 176px;
background: #F47E6B url('/path/to/logo.jpg') center / cover;
}
.coral-card-wide > .mdl-card__menu {
color: #fff;
}
.error-console {
display: none;
margin-top: 10px;
border-radius: 4px;
background-color: pink;
color: red;
border: 1px solid red;
padding: 10px;
}
.error-console.active {
display: block;
}
</style>
<link rel="stylesheet" href="/public/css/admin.css">
<%_ if (locals.customCssUrl) { _%>
<link href="<%= customCssUrl %>" rel="stylesheet" type="text/css">
<%_ } _%>
</head>
<body>
<body class="confirm-email-page">
<div id="root">
<div class="coral-card-wide mdl-card mdl-shadow--2dp">
<div class="mdl-card__title">
<h2 class="mdl-card__title-text">Verify Email Address</h2>
</div>
<div class="mdl-card__supporting-text">
Click the button below to verify the email on your new user account.
</div>
<div class="mdl-card__actions mdl-card--border">
<a class="mdl-button mdl-button--colored mdl-js-button mdl-js-ripple-effect" id="verify-email">
Verify
</a>
<div style="display: none" id="p2" class="mdl-progress mdl-js-progress mdl-progress__indeterminate"></div>
</div>
</div>
<div class="error-console container"></div>
<form id="verify-email-form" class="container">
<legend class="legend"><%= t('confirm_email.click_to_confirm') %></legend>
<button type="submit"><%= t('confirm_email.confirm') %></button>
</form>
</div>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
<script defer src="https://code.getmdl.io/1.3.0/material.min.js"></script>
<script>
$(function () {
function showError(message) {
$('.error-console').text(message).addClass('active');
}
function handleClick (e) {
<script src="/public/javascripts/admin.js"></script>
<script type="text/javascript">
$(function() {
function handleSubmit(e) {
e.preventDefault();
$('#p2').css('display', 'block');
$('.error-console').removeClass('active');
$.ajax({
@@ -82,7 +39,18 @@
});
}
$('#verify-email').on('click', handleClick);
$.ajax({
url: '<%= BASE_PATH %>api/v1/account/email/verify',
contentType: 'application/json',
method: 'POST',
data: JSON.stringify({token: location.hash.replace('#', ''), check: true})
})
.then(function () {
$('#verify-email-form').fadeIn().on('submit', handleSubmit);
})
.catch(function (error) {
showError(error.responseText);
});
});
</script>
</body>
+4 -1
View File
@@ -25,8 +25,11 @@
font-weight: bold;
}
</style>
<%_ if (locals.customCssUrl) { _%>
<link href="<%= customCssUrl %>" rel="stylesheet" type="text/css">
<%_ } _%>
</head>
<body>
<body class="docs-page">
<div id="root"></div>
<script src="<%= STATIC_URL %>static/coral-docs/bundle.js" charset="utf-8"></script>
</body>
+20 -97
View File
@@ -6,111 +6,33 @@
<title>Password Reset</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
<link rel="stylesheet" href="https://code.getmdl.io/1.2.1/material.indigo-pink.min.css">
<style media="screen">
body, #root {
width: 100%;
height: 100%;
margin: 0;
background: #fff;
}
.container {
max-width: 300px;
margin: 50px auto;
}
#root form {
display: none;
padding: 15px;
}
.legend {
text-align: center;
width: 100%;
font-weight: bold;
}
label {
display: block;
margin-top: 10px;
margin-bottom: 3px;
padding-right: 30px;
}
small {
color: #888;
}
input {
border-radius: 4px;
margin-top: 3px;
border: 1px solid lightgrey;
font-size: 16px;
width: 100%;
padding: 14px;
height: 100%;
display: inline-block;
}
.submit-password-reset {
border-radius: 4px;
border: none;
display: block;
background-color: #333;
color: white;
text-align: center;
width: 100%;
padding: 10px;
margin-top: 10px;
cursor: pointer;
}
.error-console {
display: none;
margin-top: 10px;
border-radius: 4px;
background-color: pink;
color: red;
border: 1px solid red;
padding: 10px;
}
.error-console.active {
display: block;
}
</style>
<link rel="stylesheet" href="/public/css/admin.css">
<%_ if (locals.customCssUrl) { _%>
<link href="<%= customCssUrl %>" rel="stylesheet" type="text/css">
<%_ } _%>
</head>
<body>
<body class="password-reset-page">
<div id="root">
<div class="error-console container"></div>
<form id="reset-password-form" class="container">
<legend class="legend">Set new password</legend>
<legend class="legend"><%= t('password_reset.set_new_password') %></legend>
<label for="password">
New password
<input type="password" name="password" placeholder="new password" />
<p><small>Password must be at least 8 characters</small></p>
<%= t('password_reset.new_password') %>
<input type="password" name="password" placeholder="<%= t('password_reset.new_password') %>" />
<p><small><%= t('password_reset.new_password_help') %></small></p>
</label>
<label for="confirm-password">
Confirm password
<input type="password" name="confirm-password" placeholder="confirm password" />
<%= t('password_reset.confirm_new_password') %>
<input type="password" name="confirm-password" placeholder="<%= t('password_reset.confirm_new_password') %>" />
</label>
<button class="submit-password-reset" type="submit">Apply</button>
<button type="submit"><%= t('password_reset.change_password') %></button>
</form>
</div>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
<script src="/public/javascripts/admin.js"></script>
<script>
$(function () {
function showError(error) {
try {
var err = JSON.parse(error);
$('.error-console').text(err.message).addClass('active');
} catch (err) {
$('.error-console').text(error).addClass('active');
}
}
$(function() {
function handleSubmit (e) {
e.preventDefault();
$('.error-console').removeClass('active');
@@ -140,15 +62,16 @@
});
}
$.ajax({
url: '<%= BASE_PATH %>api/v1/account/password/reset?check=true',
url: '<%= BASE_PATH %>api/v1/account/password/reset',
contentType: 'application/json',
method: 'PUT',
data: JSON.stringify({token: location.hash.replace('#', '')})
}).then(function () {
data: JSON.stringify({token: location.hash.replace('#', ''), check: true})
})
.then(function () {
$('#reset-password-form').fadeIn().on('submit', handleSubmit);
}).catch(function (error) {
})
.catch(function (error) {
showError(error.responseText);
});
});
+1 -1
View File
@@ -14,7 +14,7 @@
<%_ } _%>
<base href="<%= BASE_URL %>"/>
</head>
<body>
<body class="embed-stream-page">
<div id="talk-embed-stream-container"></div>
<script src="<%= STATIC_URL %>static/embed/stream/bundle.js"></script>
</body>