mirror of
https://github.com/wassname/talk.git
synced 2026-08-11 11:27:10 +08:00
css and standardization on all admin pages
This commit is contained in:
+1
-1
@@ -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
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"verbose": true,
|
||||
"ignore": ["test/*", "client/*", "dist/*", "plugins/*/client"],
|
||||
"ext": "js,json,graphql"
|
||||
"ext": "js,json,graphql,yml"
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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') %>
|
||||
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
|
||||
|
||||
@@ -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.');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user