- {/* TODO: translate string */}
- Contains Link
+ {t('common.contains_link')}
diff --git a/client/coral-admin/src/routes/Stories/containers/Stories.js b/client/coral-admin/src/routes/Stories/containers/Stories.js
index 144076b41..7549855c0 100644
--- a/client/coral-admin/src/routes/Stories/containers/Stories.js
+++ b/client/coral-admin/src/routes/Stories/containers/Stories.js
@@ -50,7 +50,7 @@ class StoriesContainer extends Component {
const { updateAssetState } = this.props;
try {
- updateAssetState(id, closeStream ? Date.now() : null);
+ await updateAssetState(id, closeStream ? Date.now() : null);
this.fetchAssets();
} catch (err) {
console.error(err);
diff --git a/client/coral-embed-stream/src/tabs/stream/components/Comment.css b/client/coral-embed-stream/src/tabs/stream/components/Comment.css
index 58ff0e731..8b7257b46 100644
--- a/client/coral-embed-stream/src/tabs/stream/components/Comment.css
+++ b/client/coral-embed-stream/src/tabs/stream/components/Comment.css
@@ -138,6 +138,7 @@
}
.commentAvatar {
+ margin-top: 10px;
max-width: 60px;
}
diff --git a/client/coral-embed-stream/src/tabs/stream/components/Stream.js b/client/coral-embed-stream/src/tabs/stream/components/Stream.js
index b097f3d57..73b4fbcfd 100644
--- a/client/coral-embed-stream/src/tabs/stream/components/Stream.js
+++ b/client/coral-embed-stream/src/tabs/stream/components/Stream.js
@@ -206,15 +206,30 @@ class Stream extends React.Component {
);
}
+ renderQuestionBox() {
+ const {
+ root,
+ asset,
+ asset: {
+ settings: { questionBoxEnable, questionBoxContent, questionBoxIcon },
+ },
+ } = this.props;
+ const slotPassthrough = { root, asset };
+ if (questionBoxEnable) {
+ return (
+
+
+
+ );
+ }
+ }
+
render() {
const {
root,
appendItemArray,
asset,
- asset: {
- comment: highlightedComment,
- settings: { questionBoxEnable },
- },
+ asset: { comment: highlightedComment },
postComment,
notify,
updateItem,
@@ -260,14 +275,7 @@ class Stream extends React.Component {
content={asset.settings.infoBoxContent}
enable={asset.settings.infoBoxEnable}
/>
- {questionBoxEnable && (
-
-
-
- )}
+ {this.renderQuestionBox()}
{!banned &&
temporarilySuspended && (
@@ -305,6 +313,7 @@ class Stream extends React.Component {
) : (
)}
+ {this.renderQuestionBox()}
)}
diff --git a/client/coral-embed-stream/style/default.css b/client/coral-embed-stream/style/default.css
index b9330ba62..6cd9bd580 100644
--- a/client/coral-embed-stream/style/default.css
+++ b/client/coral-embed-stream/style/default.css
@@ -112,6 +112,11 @@ body {
position: relative;
}
+.talk-stream-comment {
+ display: flex;
+ flex-direction: row;
+}
+
/* Comment styles */
.comment {
margin-bottom: 10px;
diff --git a/client/coral-framework/actions/auth.js b/client/coral-framework/actions/auth.js
index 20aa4ce57..bf7964cf9 100644
--- a/client/coral-framework/actions/auth.js
+++ b/client/coral-framework/actions/auth.js
@@ -1,10 +1,5 @@
import * as actions from '../constants/auth';
-import jwtDecode from 'jwt-decode';
-
-function cleanAuthData(localStorage) {
- localStorage.removeItem('token');
- localStorage.removeItem('exp');
-}
+import { setStorageAuthToken, clearStorageAuthToken } from '../services/auth';
export const checkLogin = () => (
dispatch,
@@ -15,7 +10,7 @@ export const checkLogin = () => (
rest('/auth')
.then(result => {
if (!result.user) {
- cleanAuthData(localStorage);
+ clearStorageAuthToken(localStorage);
dispatch(checkLoginSuccess(null));
client.resetWebsocket();
return;
@@ -32,7 +27,7 @@ export const checkLogin = () => (
.catch(error => {
if (error.status && error.status === 401 && localStorage) {
// Unauthorized.
- cleanAuthData(localStorage);
+ clearStorageAuthToken(localStorage);
client.resetWebsocket();
} else {
console.error(error);
@@ -58,8 +53,7 @@ export const setAuthToken = token => (
_,
{ localStorage, client }
) => {
- localStorage.setItem('exp', jwtDecode(token).exp);
- localStorage.setItem('token', token);
+ setStorageAuthToken(localStorage, token);
// Dispatch the set auth token action. For some browsers and situations, we
// may not be able to persist the auth token any other way. Keep it in redux!
@@ -76,9 +70,7 @@ export const handleSuccessfulLogin = (user, token) => (
_,
{ client, localStorage, postMessage }
) => {
- const { exp } = jwtDecode(token);
- localStorage.setItem('exp', exp);
- localStorage.setItem('token', token);
+ setStorageAuthToken(localStorage, token);
// Send the message via the messages service to the window.opener if it
// exists.
@@ -117,7 +109,7 @@ export const logout = () => async (
}
// Clear the auth data persisted to localStorage.
- cleanAuthData(localStorage);
+ clearStorageAuthToken(localStorage);
// Reset the websocket.
client.resetWebsocket();
diff --git a/client/coral-framework/components/ConfigureCard.css b/client/coral-framework/components/ConfigureCard.css
index f72ea0c73..5adc70009 100644
--- a/client/coral-framework/components/ConfigureCard.css
+++ b/client/coral-framework/components/ConfigureCard.css
@@ -1,50 +1,48 @@
.card {
- margin-bottom: 20px;
- align-items: flex-start;
- min-height: 100px;
max-width: 600px;
+ min-height: 100px;
+ flex-direction: row;
+ align-items: flex-start;
+ margin-bottom: 20px;
overflow: visible;
}
+.collapsibleCard {
+ min-height: auto;
+}
+
+.enabledCard {
+ border-left: 7px solid #00796b;
+}
+
+.action {
+ flex-shrink: 0;
+ margin-right: 12px;
+}
+
+.wrapper {
+ flex-grow: 1;
+}
+
.header {
- margin-top: 3px;
- margin-bottom: 7px;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-top: 2px;
+}
+
+.title {
font-size: 18px;
font-weight: 500;
}
-.wrapper {
- width: 100%;
+.body {
+ margin-top: 7px;
font-size: 14px;
letter-spacing: 0;
}
-.action {
- display: inline-block;
- position: absolute;
- top: 0;
- left: 0;
- padding: 20px;
-}
-
-.content {
- display: inline-block;
- padding: 0px 30px;
- box-sizing: border-box;
-}
-
-.enabledSetting {
- border-left-color: #00796b;
- border-left-style: solid;
- border-left-width: 7px;
-}
-
-.disabledSetting {
- padding-left: 22px;
-}
-
-.disabledSettingText {
+.disabledBody {
color: #ccc;
- pointer-events: none;
}
diff --git a/client/coral-framework/components/ConfigureCard.js b/client/coral-framework/components/ConfigureCard.js
index 7a2872318..4bc6bb967 100644
--- a/client/coral-framework/components/ConfigureCard.js
+++ b/client/coral-framework/components/ConfigureCard.js
@@ -1,46 +1,69 @@
-import React from 'react';
+import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
-import styles from './ConfigureCard.css';
+import classnames from 'classnames/bind';
import { Card } from 'coral-ui';
-import { Checkbox } from 'react-mdl';
-import cn from 'classnames';
+import { Checkbox, IconButton } from 'react-mdl';
-const ConfigureCard = ({
- title,
- children,
- className,
- onCheckbox,
- checked,
- ...rest
-}) => (
-
- {checked !== undefined && (
-
-
-
- )}
-
-
{title}
-
this.setState({ isOpen: !this.state.isOpen });
+
+ render() {
+ const {
+ title,
+ children,
+ className,
+ onCheckbox,
+ checked,
+ collapsible,
+ ...rest
+ } = this.props;
+
+ const { isOpen } = this.state;
+
+ const iconName = isOpen ? 'keyboard_arrow_up' : 'keyboard_arrow_down';
+
+ return (
+
- {children}
-
-
-
-);
+ {checked !== undefined && (
+
+
+
+ )}
+
+
+
{title}
+ {collapsible && (
+
+ )}
+
+ {isOpen && (
+
+ {children}
+
+ )}
+
+
+ );
+ }
+}
ConfigureCard.propTypes = {
title: PropTypes.string,
@@ -48,6 +71,7 @@ ConfigureCard.propTypes = {
onCheckbox: PropTypes.func,
checked: PropTypes.bool,
children: PropTypes.node,
+ collapsible: PropTypes.bool,
};
export default ConfigureCard;
diff --git a/client/coral-framework/services/auth.js b/client/coral-framework/services/auth.js
new file mode 100644
index 000000000..a23a6dd0d
--- /dev/null
+++ b/client/coral-framework/services/auth.js
@@ -0,0 +1,11 @@
+import jwtDecode from 'jwt-decode';
+
+export const setStorageAuthToken = (storage, token) => {
+ storage.setItem('exp', jwtDecode(token).exp);
+ storage.setItem('token', token);
+};
+
+export const clearStorageAuthToken = storage => {
+ storage.removeItem('token');
+ storage.removeItem('exp');
+};
diff --git a/client/coral-framework/services/bootstrap.js b/client/coral-framework/services/bootstrap.js
index c96893d4e..300cd3e70 100644
--- a/client/coral-framework/services/bootstrap.js
+++ b/client/coral-framework/services/bootstrap.js
@@ -22,6 +22,7 @@ import {
} from 'coral-framework/services/storage';
import { createHistory } from 'coral-framework/services/history';
import { createIntrospection } from 'coral-framework/services/introspection';
+import { setStorageAuthToken } from 'coral-framework/services/auth';
import introspectionData from 'coral-framework/graphql/introspection.json';
import coreReducers from '../reducers';
import { checkLogin as checkLoginAction } from '../actions/auth';
@@ -46,7 +47,30 @@ const getAuthToken = (store, storage) => {
// capable of storing the token in localStorage, then we would have
// persisted it to the redux state.
return state.config.auth_token || state.auth.token;
- } else if (!bowser.safari && !bowser.ios && storage) {
+ } else if (location.hash && location.hash.startsWith('#access_token=')) {
+ // Check to see if the access token is living in the URL as a hash.
+ const token = location.hash.substring(14);
+
+ history.replaceState(
+ {},
+ document.title,
+ window.location.pathname + window.location.search
+ );
+
+ // Once we clear the hash above, this login method will not persist across
+ // refreshes. We will need to persist the token to storage if it's
+ // available.
+ if (storage) {
+ setStorageAuthToken(storage, token);
+ }
+
+ return token;
+ } else if (
+ !bowser.safari &&
+ !bowser.ios &&
+ storage &&
+ storage.getItem('token')
+ ) {
// Use local storage auth tokens where there's a stable api.
return storage.getItem('token');
}
diff --git a/config.js b/config.js
index 8987f1114..99600508a 100644
--- a/config.js
+++ b/config.js
@@ -30,7 +30,7 @@ const CONFIG = {
ENABLE_TRACING: Boolean(process.env.APOLLO_ENGINE_KEY),
// EMAIL_SUBJECT_PREFIX is the string before emails in the subject.
- EMAIL_SUBJECT_PREFIX: process.env.TALK_EMAIL_SUBJECT_PREFIX || '[Talk]',
+ EMAIL_SUBJECT_PREFIX: process.env.TALK_EMAIL_SUBJECT_PREFIX,
// DEFAULT_LANG is the default language used for server sent emails and
// rendered text.
@@ -271,6 +271,10 @@ const CONFIG = {
// CONFIG VALIDATION
//==============================================================================
+if (typeof CONFIG.EMAIL_SUBJECT_PREFIX === 'undefined') {
+ CONFIG.EMAIL_SUBJECT_PREFIX = '[Talk]';
+}
+
if (process.env.NODE_ENV === 'test') {
if (!CONFIG.ROOT_URL) {
CONFIG.ROOT_URL = `http://${localAddress}:3001`;
diff --git a/docs/source/02-02-advanced-configuration.md b/docs/source/02-02-advanced-configuration.md
index 083b4bcdb..49769be0b 100644
--- a/docs/source/02-02-advanced-configuration.md
+++ b/docs/source/02-02-advanced-configuration.md
@@ -246,6 +246,21 @@ Refer to the documentation for [TALK_JWT_ALG](#talk-jwt-alg) for other signing
methods and other forms of the `TALK_JWT_SECRET`. If you are interested in using
multiple keys, then refer to [TALK_JWT_SECRETS](#talk-jwt-secrets).
+You can also encode your secret as a base64 string (if you are using a symmetric
+algorithm) as long as you prefix it with `base64:`. For example:
+
+```plain
+TALK_JWT_SECRET={"secret": "base64:dGVzdA=="}
+```
+
+Would be the same as:
+
+```plain
+TALK_JWT_SECRET={"secret": "test"}
+```
+
+As `dGVzdA==` is just `test` encoded using base64.
+
## TALK_JWT_SECRETS
Used when specifying multiple secrets used for key rotations. This is a JSON
@@ -271,7 +286,6 @@ Note that the secret is stored in a JSON object, keyed by `secret`. This is only
needed when specifying in the multiple secrets for `TALK_JWT_SECRETS`, but may
be used to specify the single [TALK_JWT_SECRET](#talk-jwt-secret).
-
When the value of [TALK_JWT_ALG](#talk-jwt-alg) is **not** a `HS*` value, then
the value of the `TALK_JWT_SECRETS` should take the form:
@@ -282,7 +296,6 @@ TALK_JWT_SECRETS=[{"kid": "1", "private": "", "public": "
@@ -307,6 +310,7 @@ Profile.propTypes = {
notify: PropTypes.func.isRequired,
username: PropTypes.string,
emailAddress: PropTypes.string,
+ success: PropTypes.bool.isRequired,
};
export default Profile;
diff --git a/plugins/talk-plugin-local-auth/translations.yml b/plugins/talk-plugin-local-auth/translations.yml
index 0a25eca51..7d4c6d1c0 100644
--- a/plugins/talk-plugin-local-auth/translations.yml
+++ b/plugins/talk-plugin-local-auth/translations.yml
@@ -302,25 +302,28 @@ de:
path: "Mein Profil > Profil-Einstellungen"
alert: "E-Mail-Adresse hinzugefügt!"
es:
+ email:
+ email_change_original:
+ subject: Cambio de correo electrónico
+ body: Su dirección de correo electrónico ha cambiado de {0} a {1}. Si no solicitó este cambio, póngase en contacto con {2}.
+ error:
+ NO_LOCAL_PROFILE: No hay una dirección de correo electrónico asociada a esta cuenta.
+ LOCAL_PROFILE: Una dirección de correo electrónico ya está asociada a esta cuenta.
+ INCORRECT_PASSWORD: La contraseña dada fue incorrecta.
talk-plugin-local-auth:
- email:
- email_change_original:
- subject: Cambio de correo electrónico
- body: Su dirección de correo electrónico ha cambiado de {0} a {1}. Si no solicitó este cambio, póngase en contacto con {2}.
- error:
- NO_LOCAL_PROFILE: No hay una dirección de correo electrónico asociada a esta cuenta.
- LOCAL_PROFILE: Una dirección de correo electrónico ya está asociada a esta cuenta.
- INCORRECT_PASSWORD: La contraseña dada fue incorrecta.
change_password:
+ save: "Salvar"
+ cancel: "Cancelar"
+ edit: "Editar"
+ changed_password_msg: "Senha alterada - Sua senha foi alterada com sucesso"
+ forgot_password_sent: "Esqueceu a senha - Nós enviamos um email para recuperação da senha"
change_password: "Cambiar Contraseña"
passwords_dont_match: "Las contraseñas no coinciden"
required_field: "Este campo es requerido"
forgot_password: "Olvidaste tu contraseña?"
- save: "Guardar"
- cancel: "Cancelar"
- edit: "Editar"
- changed_password_msg: "Contraseña Actualizada - Tu contraseña ha sido exitosamente actualizada"
- forgot_password_sent: "Contraseña Olvidada - Te enviamos un email para recuperar tu contraseña"
+ old_password: "Contraseña anterior"
+ new_password: "Contraseña nueva"
+ confirm_new_password: "Confirme contraseña nueva"
change_username:
change_username_note: "El usuario puede ser cambiado cada 14 días."
is_not_eligible: "Ahora mismo no se puede cambiar su nombre de usuario."
@@ -329,11 +332,13 @@ es:
cancel: "Cancelar"
confirm_username_change: "Confirmar Cambio de Usuario"
description: "Estás intentando cambiar tu usuario. Tu nuevo usuario aparecerá en todos tus pasados y futuros comentarios."
- old_username: "Usuario viejo"
+ old_username: "Usuario anterior"
new_username: "Usuario nuevo"
+ re_enter: "Escriba usuario nuevo otra vez"
bottom_note: "Nota: No podrás cambiar tu usuario por 14 días"
confirm_changes: "Confirmar Cambios"
username_does_not_match: "El usuario no coincide"
+ cant_be_equal: "Tu nuev@ {0} tiene que ser diferente"
changed_username_success_msg: "Usuario Actualizado - Tu usuario ha sido exitosamente actualizado. No podrás cambiar el usuario por 14 días."
change_username_attempt: "El usuario no puede ser actualizado. Los usuarios pueden ser cambiados cada 14 días."
change_email:
diff --git a/plugins/talk-plugin-toxic-comments/server/perspective.js b/plugins/talk-plugin-toxic-comments/server/perspective.js
index 424543b2b..7966d3b36 100644
--- a/plugins/talk-plugin-toxic-comments/server/perspective.js
+++ b/plugins/talk-plugin-toxic-comments/server/perspective.js
@@ -10,7 +10,8 @@ const debug = require('debug')('talk:plugin:toxic-comments');
/**
* Get scores from the perspective api
- * @param {string} text text to be anaylized
+ *
+ * @param {string} text text to be analyzed
* @return {object} object containing toxicity scores
*/
async function getScores(text) {
@@ -43,13 +44,13 @@ async function getScores(text) {
// If we get an error, just say it's not a toxic comment.
if (data.error) {
- debug('Recieved Error when submitting: %o', data.error);
+ debug('Received Error when submitting: %o', data.error);
return {
TOXICITY: {
- summaryScore: 0.0,
+ summaryScore: null,
},
SEVERE_TOXICITY: {
- summaryScore: 0.0,
+ summaryScore: null,
},
};
}
@@ -66,6 +67,7 @@ async function getScores(text) {
/**
* Get toxicity probability
+ *
* @param {object} scores scores as returned by `getScores`
* @return {number} toxicity probability from 0 - 1.0
*/
@@ -74,7 +76,9 @@ function getProbability(scores) {
}
/**
- * isToxic determines if given probabilty or scores meets the toxicity threshold.
+ * isToxic determines if given probability or scores meets the toxicity
+ * threshold.
+ *
* @param {object|number} scoresOrProbability scores or probability
* @return {boolean}
*/
@@ -89,6 +93,7 @@ function isToxic(scoresOrProbability) {
/**
* maskKeyInError is a decorator that calls fn and masks the
* API_KEY in errors before throwing.
+ *
* @param {function} fn Function that returns a Promise
* @return {function} decorated function
*/
diff --git a/services/jwt.js b/services/jwt.js
index c36ea4c97..b1e444dbd 100644
--- a/services/jwt.js
+++ b/services/jwt.js
@@ -120,6 +120,11 @@ function SharedSecret({ kid = undefined, secret = null }, algorithm) {
throw new Error('Secret cannot have a zero length');
}
+ // If the secret is base64 encoded, then decode it!
+ if (secret.startsWith('base64:')) {
+ secret = Buffer.from(secret.substring(7), 'base64').toString();
+ }
+
return new Secret({
kid,
signingKey: secret,
diff --git a/services/moderation/phases/links.js b/services/moderation/phases/links.js
index ec05834b6..c31cf0c13 100644
--- a/services/moderation/phases/links.js
+++ b/services/moderation/phases/links.js
@@ -11,7 +11,7 @@ module.exports = (
},
}
) => {
- if (premodLinksEnable && linkify.test(comment.body)) {
+ if (premodLinksEnable && linkify.test(comment.body.replace(/\xAD/g, ''))) {
// Add the flag related to Trust to the comment.
return {
status: 'SYSTEM_WITHHELD',
diff --git a/views/account/password/reset.njk b/views/account/password/reset.njk
index 6eee12dd0..bede0b9fc 100644
--- a/views/account/password/reset.njk
+++ b/views/account/password/reset.njk
@@ -8,7 +8,7 @@
{{ t('password_reset.set_new_password') }}
{{ t('password_reset.change_password_help') }}
-