css and standardization on all admin pages

This commit is contained in:
Wyatt Johnson
2017-12-21 23:27:37 -07:00
parent 5488d5a7fa
commit 20878ba040
25 changed files with 367 additions and 325 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
@@ -1,5 +1,5 @@
{
"verbose": true,
"ignore": ["test/*", "client/*", "dist/*", "plugins/*/client"],
"ext": "js,json,graphql"
"ext": "js,json,graphql,yml"
}
+3 -3
View File
@@ -175,11 +175,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
+13 -4
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
});
@@ -231,7 +239,8 @@ module.exports = {
ErrMaxRateLimit,
ErrMissingEmail,
ErrMissingPassword,
ErrMissingToken,
ErrEmailVerificationToken,
ErrPasswordResetToken,
ErrMissingUsername,
ErrNotAuthorized,
ErrNotFound,
@@ -244,4 +253,4 @@ module.exports = {
ErrSpecialChars,
ErrUsernameTaken,
ExtendableError,
};
};
+11
View File
@@ -14,6 +14,15 @@ en:
yes_ban_user: "Yes, Ban User"
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"
@@ -184,6 +193,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"
+16 -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,22 @@ 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;
@@ -26,4 +26,4 @@ ApproveCommentAction.propTypes = {
comment: PropTypes.object,
};
export default ApproveCommentAction;
export default ApproveCommentAction;
+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');
});
+32 -28
View File
@@ -17,27 +17,31 @@ router.get('/', authorization.needed(), (req, res, next) => {
// 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;
const {token, check} = req.body;
if (!token) {
return next(errors.ErrMissingToken);
return next(errors.ErrEmailVerificationToken);
}
if (check) {
try {
await UsersService.verifyEmailConfirmationToken(token);
return res.status(204).end();
} catch (err) {
console.error(err);
return next(errors.ErrEmailVerificationToken);
}
}
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 +52,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 +68,34 @@ 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;
const {token, password} = req.body;
const {token, password, check = false} = req.body;
if (!token) {
return next(errors.ErrMissingToken);
return next(errors.ErrPasswordResetToken);
}
if (check !== 'true' && (!password || password.length < 8)) {
if (check) {
try {
await UsersService.verifyPasswordResetToken(token);
return res.status(204).end();
} catch (err) {
console.error(err);
return next(errors.ErrPasswordResetToken);
}
}
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 -2
View File
@@ -99,7 +99,7 @@ router.post('/:user_id/email', authorization.needed('ADMIN', 'MODERATOR'), async
return next(errors.ErrMissingEmail);
}
await mailer.sendSimple({
await mailer.send({
template: 'notification', // needed to know which template to render!
locals: { // specifies the template locals.
body: req.body.body
@@ -122,7 +122,7 @@ router.post('/:user_id/email', authorization.needed('ADMIN', 'MODERATOR'), async
const SendEmailConfirmation = async (user, email, referer) => {
let token = await UsersService.createEmailConfirmToken(user, email, referer);
return mailer.sendSimple({
return mailer.send({
template: 'email-confirm',
locals: {
token,
+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
@@ -173,7 +173,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 {
@@ -189,7 +189,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(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') %>
+11
View File
@@ -32,6 +32,14 @@ let translations = fs.readdirSync(resolve())
// Create a list of all supported translations.
const languages = Object.keys(translations);
// Move the default language to the front.
if (languages.includes(DEFAULT_LANG)) {
const from = languages.indexOf(DEFAULT_LANG);
languages.splice(from, 1);
languages.splice(0, 0, DEFAULT_LANG);
}
debug(`loaded language sets for ${languages}`);
let loadedPluginTranslations = false;
const loadPluginTranslations = () => {
if (loadedPluginTranslations) {
@@ -80,8 +88,11 @@ const t = (language) => (key, ...replacements) => {
*/
const i18n = {
request(req) {
debug(`possible languages given request '${accepts(req).languages()}'`);
const lang = accepts(req).language(languages);
debug(`parsed request language as '${lang}'`);
const language = lang ? lang : DEFAULT_LANG;
debug(`decided language as '${language}'`);
return t(language);
},
+97 -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 {attachStaticLocals} = require('../middleware/staticTemplate');
const i18n = require('./i18n');
@@ -54,102 +54,108 @@ 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_HOST.length > 0 &&
SMTP_USERNAME && SMTP_USERNAME.length > 0 &&
SMTP_PORT && SMTP_PORT.length > 0 &&
SMTP_PASSWORD && SMTP_PASSWORD.length > 0 &&
SMTP_FROM_ADDRESS && SMTP_FROM_ADDRESS.length > 0
) || 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 ({template, locals, to, subject}) => {
if (!mailer.enabled) {
throw new Error('email is not enabled because required configuration is not available');
}
// Attach the template locals.
attachStaticLocals(locals);
// Attach the translation function.
locals.t = i18n.t;
// Render the templates.
const [
html,
text,
] = await Promise.all(['html', 'txt'].map((fmt) => {
return templates.render(template, fmt, locals);
}));
// Create the job.
return mailer.task.create({
title: 'Mail',
message: {
to,
subject: `${EMAIL_SUBJECT_PREFIX} ${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;
+34 -6
View File
@@ -439,7 +439,7 @@ module.exports = class UsersService {
subject: i18n.t('email.banned.subject'),
to: localProfile.id
};
await MailerService.sendSimple(options);
await MailerService.send(options);
}
}
@@ -475,7 +475,7 @@ module.exports = class UsersService {
to: localProfile.id,
};
await MailerService.sendSimple(options);
await MailerService.send(options);
}
}
@@ -511,7 +511,7 @@ module.exports = class UsersService {
// We may want a standard way to access a user's e-mail address in the future
};
await MailerService.sendSimple(options);
await MailerService.send(options);
}
}
@@ -767,6 +767,36 @@ module.exports = class UsersService {
}, tokenOptions);
}
static async verifyEmailConfirmationToken(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.
@@ -775,9 +805,7 @@ module.exports = 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);
+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.');
});
});
+4 -4
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()', () => {
@@ -160,7 +160,7 @@ describe('services.UsersService', () => {
expect(user).to.have.property('status', 'ACTIVE');
})
.then(() => {
expect(MailerService.sendSimple).to.not.have.been.called;
expect(MailerService.send).to.not.have.been.called;
});
});
@@ -203,7 +203,7 @@ describe('services.UsersService', () => {
expect(user).to.have.property('status', 'BANNED');
})
.then(() => {
expect(MailerService.sendSimple).to.have.been.calledWithMatch({
expect(MailerService.send).to.have.been.calledWithMatch({
template: 'banned',
to: mockUsers[0].profiles[0].id
});
+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 %>client/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 %>client/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 %>client/embed/stream/bundle.js"></script>
</body>