diff --git a/README.md b/README.md index a57451e00..dd04b096f 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ You’ve installed Talk on your server, and you’re preparing to launch it on y ## Advanced Usage -For advanced configuration and usage of Talk, check out our [Configuration](https://docs.coralproject.net/talk/advanced-configuration/) and [Integration](https://docs.coralproject.net/talk/integrating/authentication/) how-tos. This covers topics in whih you will need dev support to fully customize and integrate Talk, such as SSO/authentication, creating and managing assets and articles, styling Talk with custom CSS, and setting up Notifications and SMTP support. +For advanced configuration and usage of Talk, check out our [Configuration](https://docs.coralproject.net/talk/advanced-configuration/) and [Integration](https://docs.coralproject.net/talk/integrating/authentication/) how-tos. This covers topics in which you will need dev support to fully customize and integrate Talk, such as SSO/authentication, creating and managing assets and articles, styling Talk with custom CSS, and setting up Notifications and SMTP support. ## Versions & Upgrading diff --git a/client/coral-admin/src/components/UserDetailComment.js b/client/coral-admin/src/components/UserDetailComment.js index fcc53553c..f98ff3f0f 100644 --- a/client/coral-admin/src/components/UserDetailComment.js +++ b/client/coral-admin/src/components/UserDetailComment.js @@ -123,7 +123,7 @@ class UserDetailComment extends React.Component { {/* TODO: translate string */} - Contains Link + {t('common.contains_link')}
diff --git a/client/coral-admin/src/routes/Configure/components/StreamSettings.js b/client/coral-admin/src/routes/Configure/components/StreamSettings.js index 969155d48..2526a3ab9 100644 --- a/client/coral-admin/src/routes/Configure/components/StreamSettings.js +++ b/client/coral-admin/src/routes/Configure/components/StreamSettings.js @@ -155,9 +155,19 @@ class StreamSettings extends React.Component { -

{t('configure.include_comment_stream_desc')}

+

+ {t('configure.code_of_conduct_summary_desc')} +   + + Code of Conduct Guide. + +

diff --git a/client/coral-admin/src/routes/Configure/components/Wordlist.js b/client/coral-admin/src/routes/Configure/components/Wordlist.js index 1a5c177d0..6acc045b8 100644 --- a/client/coral-admin/src/routes/Configure/components/Wordlist.js +++ b/client/coral-admin/src/routes/Configure/components/Wordlist.js @@ -6,7 +6,7 @@ import ConfigureCard from 'coral-framework/components/ConfigureCard'; const Wordlist = ({ suspectWords, bannedWords, onChangeWordlist }) => (
- +

{t('configure.banned_word_text')}

( onChange={tags => onChangeWordlist('banned', tags)} />
- +

{t('configure.suspect_word_text')}

- {/* 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') }}

-
+