mirror of
https://github.com/wassname/talk.git
synced 2026-08-18 12:30:39 +08:00
add a confirm-email landing page
This commit is contained in:
@@ -32,6 +32,10 @@ const updateModeration = (updateSettings, mod) => () => {
|
||||
updateSettings({moderation});
|
||||
};
|
||||
|
||||
const updateEmailConfirmation = (updateSettings, verify) => () => {
|
||||
updateSettings({requireEmailConfirmation: !verify});
|
||||
};
|
||||
|
||||
const updateInfoBoxEnable = (updateSettings, infoBox) => () => {
|
||||
const infoBoxEnable = !infoBox;
|
||||
updateSettings({infoBoxEnable});
|
||||
@@ -67,106 +71,123 @@ const CommentSettings = ({fetchingSettings, title, updateSettings, settingsError
|
||||
return <Card shadow="4"><Spinner/>Loading settings...</Card>;
|
||||
}
|
||||
|
||||
// just putting this here for shorthand below
|
||||
const on = styles.enabledSetting;
|
||||
const off = styles.disabledSetting;
|
||||
|
||||
return (
|
||||
<div className={styles.commentSettingsSection}>
|
||||
<h3>{title}</h3>
|
||||
<Card className={`${styles.configSetting} ${settings.moderation === 'PRE' ? styles.enabledSetting : styles.disabledSetting}`}>
|
||||
<div className={styles.action}>
|
||||
<Checkbox
|
||||
onChange={updateModeration(updateSettings, settings.moderation)}
|
||||
checked={settings.moderation === 'PRE'} />
|
||||
</div>
|
||||
<div className={styles.content}>
|
||||
<Card className={`${styles.configSetting} ${settings.moderation === 'PRE' ? on : off}`}>
|
||||
<div className={styles.action}>
|
||||
<Checkbox
|
||||
onChange={updateModeration(updateSettings, settings.moderation)}
|
||||
checked={settings.moderation === 'PRE'} />
|
||||
</div>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.settingsHeader}>{lang.t('configure.enable-pre-moderation')}</div>
|
||||
<p className={settings.moderation === 'PRE' ? '' : styles.disabledSettingText}>
|
||||
{lang.t('configure.enable-pre-moderation-text')}
|
||||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
<Card className={`${styles.configSetting} ${settings.charCountEnable ? styles.enabledSetting : styles.disabledSetting}`}>
|
||||
<div className={styles.action}>
|
||||
<Checkbox
|
||||
onChange={updateCharCountEnable(updateSettings, settings.charCountEnable)}
|
||||
checked={settings.charCountEnable} />
|
||||
</div>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.settingsHeader}>{lang.t('configure.comment-count-header')}</div>
|
||||
<p className={settings.charCountEnable ? '' : styles.disabledSettingText}>
|
||||
<span>{lang.t('configure.comment-count-text-pre')}</span>
|
||||
<input type='text'
|
||||
className={`${styles.charCountTexfield} ${settings.charCountEnable && styles.charCountTexfieldEnabled}`}
|
||||
htmlFor='charCount'
|
||||
onChange={updateCharCount(updateSettings, settingsError)}
|
||||
value={settings.charCount}/>
|
||||
<span>{lang.t('configure.comment-count-text-post')}</span>
|
||||
{
|
||||
errors.charCount &&
|
||||
<span className={styles.settingsError}>
|
||||
<br/>
|
||||
<Icon name="error_outline"/>
|
||||
{lang.t('configure.comment-count-error')}
|
||||
</span>
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
<Card className={`${styles.configSettingInfoBox} ${settings.infoBoxEnable ? styles.enabledSetting : styles.disabledSetting}`}>
|
||||
<div className={styles.action}>
|
||||
<Checkbox
|
||||
onChange={updateInfoBoxEnable(updateSettings, settings.infoBoxEnable)}
|
||||
checked={settings.infoBoxEnable} />
|
||||
</div>
|
||||
<div className={styles.content}>
|
||||
{lang.t('configure.include-comment-stream')}
|
||||
<p>
|
||||
{lang.t('configure.include-comment-stream-desc')}
|
||||
</p>
|
||||
<div className={`${styles.configSettingInfoBox} ${settings.infoBoxEnable ? null : styles.hidden}`} >
|
||||
<div className={styles.content}>
|
||||
<Textfield
|
||||
onChange={updateInfoBoxContent(updateSettings)}
|
||||
value={settings.infoBoxContent}
|
||||
label={lang.t('configure.include-text')}
|
||||
rows={3}/>
|
||||
</div>
|
||||
</Card>
|
||||
<Card className={`${styles.configSetting} ${settings.requireEmailConfirmation ? on : off}`}>
|
||||
<div className={styles.action}>
|
||||
<Checkbox
|
||||
onChange={updateEmailConfirmation(updateSettings, settings.requireEmailConfirmation)}
|
||||
checked={settings.requireEmailConfirmation} />
|
||||
</div>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.settingsHeader}>{lang.t('configure.require-email-verification')}</div>
|
||||
<p className={settings.requireEmailConfirmation ? '' : styles.disabledSettingText}>
|
||||
{lang.t('configure.require-email-verification-text')}
|
||||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
<Card className={`${styles.configSetting} ${settings.charCountEnable ? on : off}`}>
|
||||
<div className={styles.action}>
|
||||
<Checkbox
|
||||
onChange={updateCharCountEnable(updateSettings, settings.charCountEnable)}
|
||||
checked={settings.charCountEnable} />
|
||||
</div>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.settingsHeader}>{lang.t('configure.comment-count-header')}</div>
|
||||
<p className={settings.charCountEnable ? '' : styles.disabledSettingText}>
|
||||
<span>{lang.t('configure.comment-count-text-pre')}</span>
|
||||
<input type='text'
|
||||
className={`${styles.charCountTexfield} ${settings.charCountEnable && styles.charCountTexfieldEnabled}`}
|
||||
htmlFor='charCount'
|
||||
onChange={updateCharCount(updateSettings, settingsError)}
|
||||
value={settings.charCount}/>
|
||||
<span>{lang.t('configure.comment-count-text-post')}</span>
|
||||
{
|
||||
errors.charCount &&
|
||||
<span className={styles.settingsError}>
|
||||
<br/>
|
||||
<Icon name="error_outline"/>
|
||||
{lang.t('configure.comment-count-error')}
|
||||
</span>
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
<Card className={`${styles.configSettingInfoBox} ${settings.infoBoxEnable ? on : off}`}>
|
||||
<div className={styles.action}>
|
||||
<Checkbox
|
||||
onChange={updateInfoBoxEnable(updateSettings, settings.infoBoxEnable)}
|
||||
checked={settings.infoBoxEnable} />
|
||||
</div>
|
||||
<div className={styles.content}>
|
||||
{lang.t('configure.include-comment-stream')}
|
||||
<p>
|
||||
{lang.t('configure.include-comment-stream-desc')}
|
||||
</p>
|
||||
<div className={`${styles.configSettingInfoBox} ${settings.infoBoxEnable ? null : styles.hidden}`} >
|
||||
<div className={styles.content}>
|
||||
<Textfield
|
||||
onChange={updateInfoBoxContent(updateSettings)}
|
||||
value={settings.infoBoxContent}
|
||||
label={lang.t('configure.include-text')}
|
||||
rows={3}/>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
<Card className={styles.configSettingInfoBox}>
|
||||
<div className={styles.content}>
|
||||
{lang.t('configure.closed-comments-desc')}
|
||||
<div>
|
||||
<Textfield
|
||||
onChange={updateClosedMessage(updateSettings)}
|
||||
value={settings.closedMessage}
|
||||
label={lang.t('configure.closed-comments-label')}
|
||||
rows={3}/>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
<Card className={styles.configSettingInfoBox}>
|
||||
<div className={styles.content}>
|
||||
{lang.t('configure.closed-comments-desc')}
|
||||
<div>
|
||||
<Textfield
|
||||
onChange={updateClosedMessage(updateSettings)}
|
||||
value={settings.closedMessage}
|
||||
label={lang.t('configure.closed-comments-label')}
|
||||
rows={3}/>
|
||||
</div>
|
||||
</Card>
|
||||
<Card className={`${styles.configSettingInfoBox}`}>
|
||||
<div className={styles.content}>
|
||||
{lang.t('configure.close-after')}
|
||||
<br />
|
||||
<Textfield
|
||||
type='number'
|
||||
pattern='[0-9]+'
|
||||
style={{width: 50}}
|
||||
onChange={updateClosedTimeout(updateSettings, settings.closedTimeout)}
|
||||
value={getTimeoutAmount(settings.closedTimeout)}
|
||||
label={lang.t('configure.closed-comments-label')} />
|
||||
<div className={styles.configTimeoutSelect}>
|
||||
<SelectField
|
||||
label="comments closed time window"
|
||||
value={getTimeoutMeasure(settings.closedTimeout)}
|
||||
onChange={updateClosedTimeout(updateSettings, settings.closedTimeout, true)}>
|
||||
<Option value={'hours'}>{lang.t('configure.hours')}</Option>
|
||||
<Option value={'days'}>{lang.t('configure.days')}</Option>
|
||||
<Option value={'weeks'}>{lang.t('configure.weeks')}</Option>
|
||||
</SelectField>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
<Card className={`${styles.configSettingInfoBox}`}>
|
||||
<div className={styles.content}>
|
||||
{lang.t('configure.close-after')}
|
||||
<br />
|
||||
<Textfield
|
||||
type='number'
|
||||
pattern='[0-9]+'
|
||||
style={{width: 50}}
|
||||
onChange={updateClosedTimeout(updateSettings, settings.closedTimeout)}
|
||||
value={getTimeoutAmount(settings.closedTimeout)}
|
||||
label={lang.t('configure.closed-comments-label')} />
|
||||
<div className={styles.configTimeoutSelect}>
|
||||
<SelectField
|
||||
label="comments closed time window"
|
||||
value={getTimeoutMeasure(settings.closedTimeout)}
|
||||
onChange={updateClosedTimeout(updateSettings, settings.closedTimeout, true)}>
|
||||
<Option value={'hours'}>{lang.t('configure.hours')}</Option>
|
||||
<Option value={'days'}>{lang.t('configure.days')}</Option>
|
||||
<Option value={'weeks'}>{lang.t('configure.weeks')}</Option>
|
||||
</SelectField>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -43,6 +43,8 @@
|
||||
"configure": {
|
||||
"enable-pre-moderation": "Enable pre-moderation",
|
||||
"enable-pre-moderation-text": "Moderators must approve any comment before it is published.",
|
||||
"require-email-verification": "Require Email Confirmation",
|
||||
"require-email-verification-text": "New Users must verify their email before commenting",
|
||||
"include-comment-stream": "Include Comment Stream Description for Readers.",
|
||||
"include-comment-stream-desc": "Write a message to be added to the top of your comment stream. Pose a topic, include community guidelines, etc.",
|
||||
"include-text": "Include your text here.",
|
||||
@@ -129,6 +131,8 @@
|
||||
"configure": {
|
||||
"enable-pre-moderation": "Habilitar pre-moderación",
|
||||
"enable-pre-moderation-text": "Los moderadores deben aprobar cada comentario antes de que sea publicado.",
|
||||
"require-email-verification": "Necesita confirmación de correo",
|
||||
"require-email-verification-text": "Nuevos usuarios deben verificar sus correos antes de comentar",
|
||||
"include-comment-stream": "Incluir la Descripción a un Hilo de Comentario para los y las Lectoras.",
|
||||
"include-comment-stream-desc": "Escribir un mensaje que será agregado a la parte de arriba del tu hilo de comentarios. Por ejemplo, un tema, guias de comunidad, etc.",
|
||||
"include-text": "Incluir tu texto aqui.",
|
||||
|
||||
@@ -1,6 +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) => {
|
||||
|
||||
@@ -28,8 +28,8 @@ router.post('/email/confirm', (req, res, next) => {
|
||||
|
||||
UsersService
|
||||
.verifyEmailConfirmation(token)
|
||||
.then(() => {
|
||||
res.status(204).end();
|
||||
.then(([, referer]) => {
|
||||
res.json({redirectUri: referer});
|
||||
})
|
||||
.catch((err) => {
|
||||
next(err);
|
||||
|
||||
@@ -69,8 +69,8 @@ router.post('/:user_id/status', authorization.needed('ADMIN'), (req, res, next)
|
||||
* @param {String} userID the id for the user to send the email to
|
||||
* @param {String} email the email for the user to send the email to
|
||||
*/
|
||||
const SendEmailConfirmation = (app, userID, email) => UsersService
|
||||
.createEmailConfirmToken(userID, email)
|
||||
const SendEmailConfirmation = (app, userID, email, referer) => UsersService
|
||||
.createEmailConfirmToken(userID, email, referer)
|
||||
.then((token) => {
|
||||
return mailer.sendSimple({
|
||||
app, // needed to render the templates.
|
||||
@@ -103,7 +103,7 @@ router.post('/', (req, res, next) => {
|
||||
|
||||
if (requireEmailConfirmation) {
|
||||
|
||||
SendEmailConfirmation(req.app, user.id, email)
|
||||
SendEmailConfirmation(req.app, user.id, email, req.header('Referer'))
|
||||
.then(() => {
|
||||
|
||||
// Then send back the user.
|
||||
|
||||
+7
-3
@@ -531,7 +531,7 @@ module.exports = class UsersService {
|
||||
* @param {String} email The email that we are needing to get confirmed.
|
||||
* @return {Promise}
|
||||
*/
|
||||
static createEmailConfirmToken(userID, email) {
|
||||
static createEmailConfirmToken(userID, email, referer) {
|
||||
if (!email || typeof email !== 'string') {
|
||||
return Promise.reject('email is required when creating a JWT for resetting passord');
|
||||
}
|
||||
@@ -555,6 +555,7 @@ module.exports = class UsersService {
|
||||
|
||||
const payload = {
|
||||
email,
|
||||
referer,
|
||||
userID
|
||||
};
|
||||
|
||||
@@ -579,9 +580,9 @@ module.exports = class UsersService {
|
||||
.verifyToken(token, {
|
||||
subject: EMAIL_CONFIRM_JWT_SUBJECT
|
||||
})
|
||||
.then(({userID, email}) => {
|
||||
.then(({userID, email, referer}) => {
|
||||
|
||||
return UserModel
|
||||
const userUpdate = UserModel
|
||||
.update({
|
||||
id: userID,
|
||||
profiles: {
|
||||
@@ -595,7 +596,10 @@ module.exports = class UsersService {
|
||||
'profiles.$.metadata.confirmed_at': new Date()
|
||||
}
|
||||
});
|
||||
|
||||
return Promise.all([userUpdate, referer]);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="initial-scale=1, maximum-scale=1">
|
||||
<title>Confirm Email</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>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root">
|
||||
<div class="coral-card-wide mdl-card mdl-shadow--2dp">
|
||||
<div class="mdl-card__title">
|
||||
<h2 class="mdl-card__title-text">Confirm Email Address</h2>
|
||||
</div>
|
||||
<div class="mdl-card__supporting-text">
|
||||
Click the button below to confirm 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="confirm-email">
|
||||
Confirm
|
||||
</a>
|
||||
<div style="display: none" id="p2" class="mdl-progress mdl-js-progress mdl-progress__indeterminate"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</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) {
|
||||
e.preventDefault();
|
||||
$('#p2').css('display', 'block');
|
||||
$('.error-console').removeClass('active');
|
||||
|
||||
$.ajax({
|
||||
url: '/api/v1/account/email/confirm',
|
||||
contentType: 'application/json',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-CSRF-Token': '<%= csrfToken %>'
|
||||
},
|
||||
data: JSON.stringify({token: location.hash.replace('#', '')})
|
||||
}).then(function (success) {
|
||||
location.href = success.redirectUri;
|
||||
}).catch(function (error) {
|
||||
showError(error.responseText);
|
||||
});
|
||||
}
|
||||
|
||||
$('#confirm-email').on('click', handleClick);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,3 +1,3 @@
|
||||
<p>A email confirmation has been requested for the following account: <b><%= email %></b>.</p>
|
||||
<p>To confirm the account, please visit the following link: <a href="http://example.com/email/confirm/endpoint#<%= token %>">http://example.com/email/confirm/endpoint#<%= token %></a></p>
|
||||
<p>To confirm the account, please visit the following link: <a href="<%= rootURL %>/admin/confirm-email#<%= token %>">http://example.com/email/confirm/endpoint#<%= token %></a></p>
|
||||
<p>If you did not request this, you can safely ignore this email.</p>
|
||||
|
||||
@@ -4,6 +4,6 @@ A email confirmation has been requested for the following account:
|
||||
|
||||
To confirm the account, please visit the following link:
|
||||
|
||||
http://example.com/email/confirm/endpoint#<%= token %>
|
||||
<%= rootURL %>/confirm/endpoint#<%= token %>
|
||||
|
||||
If you did not request this, you can safely ignore this email.
|
||||
|
||||
Reference in New Issue
Block a user