diff --git a/.gitignore b/.gitignore index 0cab02aeb..24022b9d7 100644 --- a/.gitignore +++ b/.gitignore @@ -38,6 +38,7 @@ plugins/* !plugins/talk-plugin-google-auth !plugins/talk-plugin-ignore-user !plugins/talk-plugin-like +!plugins/talk-plugin-local-auth !plugins/talk-plugin-love !plugins/talk-plugin-member-since !plugins/talk-plugin-mod @@ -53,6 +54,7 @@ plugins/* !plugins/talk-plugin-profile-data !plugins/talk-plugin-remember-sort !plugins/talk-plugin-respect +!plugins/talk-plugin-rich-text !plugins/talk-plugin-slack-notifications !plugins/talk-plugin-sort-most-downvoted !plugins/talk-plugin-sort-most-liked @@ -66,7 +68,6 @@ plugins/* !plugins/talk-plugin-toxic-comments !plugins/talk-plugin-upvote !plugins/talk-plugin-viewing-options -!plugins/talk-plugin-rich-text **/node_modules/* yarn-error.log diff --git a/.nsprc b/.nsprc index da6cb9865..530a17a9f 100644 --- a/.nsprc +++ b/.nsprc @@ -2,6 +2,11 @@ "exceptions": [ "https://nodesecurity.io/advisories/531", "https://nodesecurity.io/advisories/532", - "https://nodesecurity.io/advisories/566" + "https://nodesecurity.io/advisories/566", + "https://nodesecurity.io/advisories/577", + "https://nodesecurity.io/advisories/594", + "https://nodesecurity.io/advisories/603", + "https://nodesecurity.io/advisories/611", + "https://nodesecurity.io/advisories/612" ] } diff --git a/bin/cli b/bin/cli index df2253261..9713d6f40 100755 --- a/bin/cli +++ b/bin/cli @@ -1,14 +1,12 @@ #!/usr/bin/env node -/** - * Module dependencies. - */ - -require('./util'); const program = require('commander'); -const { head, map } = require('lodash'); -const Matcher = require('did-you-mean'); +// We're requiring this here so it'll setup some promise rejection hooks to log +// out. +require('./util'); + +// Setup the program. program .command('serve', 'serve the application') .command('db', 'run database commands') @@ -24,20 +22,3 @@ program 'provides utilities for interacting with the plugin system' ) .parse(process.argv); - -// If the command wasn't found, output help. -const commands = map(program.commands, '_name'); -const command = head(program.args); -if (!commands.includes(command)) { - const m = new Matcher(commands); - const similarCommands = m.list(command); - - console.error( - `cli '${command}' is not a talk cli command. See 'cli --help'.` - ); - if (similarCommands.length > 0) { - const sc = similarCommands.map(({ value }) => `\t${value}\n`).join(''); - console.error(`\nThe most similar commands are\n${sc}`); - } - process.exit(1); -} diff --git a/bin/cli-settings b/bin/cli-settings index 7f449e0d5..72f42672d 100755 --- a/bin/cli-settings +++ b/bin/cli-settings @@ -4,7 +4,8 @@ const util = require('./util'); const program = require('commander'); const inquirer = require('inquirer'); const mongoose = require('../services/mongoose'); -const SettingsService = require('../services/settings'); +const Settings = require('../services/settings'); +const cache = require('../services/cache'); // Register the shutdown criteria. util.onshutdown([() => mongoose.disconnect()]); @@ -14,9 +15,12 @@ util.onshutdown([() => mongoose.disconnect()]); */ async function changeOrgName() { try { - let settings = await SettingsService.retrieve(); + await cache.init(); - let { organizationName } = await inquirer.prompt([ + // Get the original settings. + const settings = await Settings.retrieve('organizationName'); + + const { organizationName } = await inquirer.prompt([ { name: 'organizationName', message: 'Organization Name', @@ -25,9 +29,8 @@ async function changeOrgName() { ]); if (settings.organizationName !== organizationName) { - settings.organizationName = organizationName; - - await SettingsService.update(settings); + // Set the organization name if there was a mutation to it. + await Settings.update({ organizationName }); console.log('Settings were updated.'); } else { @@ -36,9 +39,9 @@ async function changeOrgName() { } catch (err) { console.error(err); util.shutdown(1); + } finally { + util.shutdown(); } - - util.shutdown(); } //============================================================================== diff --git a/bin/util.js b/bin/util.js index 648c79bcc..78664daa3 100644 --- a/bin/util.js +++ b/bin/util.js @@ -4,7 +4,8 @@ require('../services/env'); const debug = require('debug')('talk:util'); const { uniq } = require('lodash'); -const util = (module.exports = {}); +// Setup the utilities. +const util = {}; /** * Stores an array of functions that should be executed in the event that the @@ -15,7 +16,7 @@ util.toshutdown = []; /** * Calls all the shutdown functions and then ends the process. - * @param {Number} [defaultCode=0] default return code upon sucesfull shutdown. + * @param {Number} [defaultCode=0] default return code upon successful shutdown. */ util.shutdown = (defaultCode = 0, signal = null) => { if (signal) { @@ -63,3 +64,5 @@ process.on('unhandledRejection', err => { console.error(err); process.exit(1); }); + +module.exports = util; diff --git a/client/coral-admin/src/components/UserDetail.js b/client/coral-admin/src/components/UserDetail.js index e45e3f095..5dabc5b6f 100644 --- a/client/coral-admin/src/components/UserDetail.js +++ b/client/coral-admin/src/components/UserDetail.js @@ -96,8 +96,6 @@ class UserDetail extends React.Component { bulkReject, } = this.props; - console.log(rejectedComments, totalComments); - // if totalComments is 0, you're dividing by zero let rejectedPercent = rejectedComments / totalComments * 100; diff --git a/client/coral-admin/src/graphql/index.js b/client/coral-admin/src/graphql/index.js index 95595c265..3874e6c94 100644 --- a/client/coral-admin/src/graphql/index.js +++ b/client/coral-admin/src/graphql/index.js @@ -291,5 +291,36 @@ export default { }, }, }), + SetCommentStatus: ({ variables: { status } }) => ({ + updateQueries: { + CoralAdmin_UserDetail: prev => { + const increment = { + rejectedComments: { + $apply: count => (count < prev.totalComments ? count + 1 : count), + }, + }; + + const decrement = { + rejectedComments: { + $apply: count => (count > 0 ? count - 1 : 0), + }, + }; + + // If rejected then increment rejectedComments by one + if (status === 'REJECTED') { + const updated = update(prev, increment); + return updated; + } + + // If approved then decrement rejectedComments by one + if (status === 'ACCEPTED') { + const updated = update(prev, decrement); + return updated; + } + + return prev; + }, + }, + }), }, }; diff --git a/client/coral-admin/src/routes/Configure/components/OrganizationSettings.js b/client/coral-admin/src/routes/Configure/components/OrganizationSettings.js index f3daeffc2..3505ef7b5 100644 --- a/client/coral-admin/src/routes/Configure/components/OrganizationSettings.js +++ b/client/coral-admin/src/routes/Configure/components/OrganizationSettings.js @@ -57,7 +57,7 @@ class OrganizationSettings extends React.Component { } const updater = { organizationContactEmail: { $set: email } }; - const errorUpdater = { organizationEmail: { $set: error } }; + const errorUpdater = { organizationContactEmail: { $set: error } }; this.props.updatePending({ updater, errorUpdater }); }; diff --git a/client/coral-embed-stream/src/tabs/profile/containers/Profile.js b/client/coral-embed-stream/src/tabs/profile/containers/Profile.js index 0272c1535..4a84f1810 100644 --- a/client/coral-embed-stream/src/tabs/profile/containers/Profile.js +++ b/client/coral-embed-stream/src/tabs/profile/containers/Profile.js @@ -46,7 +46,7 @@ ProfileContainer.propTypes = { currentUser: PropTypes.object, }; -const slots = ['profileSections']; +const slots = ['profileSections', 'profileSettings', 'profileHeader']; const withProfileQuery = withQuery( gql` diff --git a/client/coral-embed-stream/src/tabs/stream/components/Comment.js b/client/coral-embed-stream/src/tabs/stream/components/Comment.js index 0983b11c4..8b1524839 100644 --- a/client/coral-embed-stream/src/tabs/stream/components/Comment.js +++ b/client/coral-embed-stream/src/tabs/stream/components/Comment.js @@ -27,6 +27,7 @@ import { getActionSummary, iPerformedThisAction, isCommentActive, + isCommentDeleted, getShallowChanges, } from 'coral-framework/utils'; import t from 'coral-framework/services/i18n'; @@ -744,8 +745,14 @@ export default class Comment extends React.Component { return (
{% for tag in plugin.tags %} - {{ tag }} + {{ tag }} {% endfor %}
{% endif %} @@ -35,4 +36,4 @@ {% endfor %} - \ No newline at end of file + diff --git a/docs/themes/coral/source/css/talk.scss b/docs/themes/coral/source/css/talk.scss index 391337876..afbd6f3af 100644 --- a/docs/themes/coral/source/css/talk.scss +++ b/docs/themes/coral/source/css/talk.scss @@ -448,3 +448,17 @@ a.brand { .plugin { display: none; } + +.badge-tag { + font-family: monospace; +} + +.badge-tag-default { + background: #28a745; + color: #fff; +} + +.badge-tag-gdpr { + background: rgb(0, 102, 176); + color: #fff; +} diff --git a/docs/themes/coral/source/js/plugins.js b/docs/themes/coral/source/js/plugins.js index 83f14d356..26f7707c3 100644 --- a/docs/themes/coral/source/js/plugins.js +++ b/docs/themes/coral/source/js/plugins.js @@ -1,6 +1,17 @@ /* global lunr */ /* eslint-env browser */ +// Sourced from https://stackoverflow.com/questions/901115/how-can-i-get-query-string-values-in-javascript +function getParameterByName(name, url) { + if (!url) url = window.location.href; + name = name.replace(/[\[\]]/g, '\\$&'); + var regex = new RegExp('[?&]' + name + '(=([^]*)|&|#|$)'), + results = regex.exec(url); + if (!results) return null; + if (!results[2]) return ''; + return decodeURIComponent(results[2].replace(/\+/g, ' ')); +} + // Sourced from https://github.com/hexojs/site/blob/8e8ed4901769abbf76263125f82832df76ced58b/themes/navy/source/js/plugins.js. (function() { 'use strict'; @@ -60,6 +71,12 @@ updateCount(elements.length); } + var searchParam = getParameterByName('q'); + if (searchParam && searchParam.length > 0) { + $input.value = searchParam; + search(searchParam); + } + $input.addEventListener('input', function() { var value = this.value; diff --git a/errors.js b/errors.js index 37f42f4ff..da1287dd3 100644 --- a/errors.js +++ b/errors.js @@ -371,6 +371,14 @@ class ErrParentDoesNotVisible extends TalkError { } } +class ErrPasswordIncorrect extends TalkError { + constructor() { + super('Your current password was entered incorrectly', { + translation_key: 'PASSWORD_INCORRECT', + }); + } +} + module.exports = { TalkError, ErrAlreadyExists, @@ -395,6 +403,7 @@ module.exports = { ErrNotFound, ErrNotVerified, ErrParentDoesNotVisible, + ErrPasswordIncorrect, ErrPasswordResetToken, ErrPasswordTooShort, ErrPermissionUpdateUsername, diff --git a/graph/mutators/user.js b/graph/mutators/user.js index 04fdcd366..1f43ca838 100644 --- a/graph/mutators/user.js +++ b/graph/mutators/user.js @@ -1,4 +1,8 @@ -const { ErrNotFound, ErrNotAuthorized } = require('../../errors'); +const { + ErrNotFound, + ErrNotAuthorized, + ErrPasswordIncorrect, +} = require('../../errors'); const Users = require('../../services/users'); const migrationHelpers = require('../../services/migration/helpers'); const { @@ -8,7 +12,7 @@ const { SET_USER_BAN_STATUS, SET_USER_SUSPENSION_STATUS, UPDATE_USER_ROLES, - DELETE_USER, + DELETE_OTHER_USER, CHANGE_PASSWORD, } = require('../../perms/constants'); @@ -93,7 +97,7 @@ const delUser = async (ctx, id) => { updateBatchSize: 10000, }); - // Remove all actions against comments. + // Remove all actions against this users comments. await transformSingleWithCursor( Action.collection.find({ user_id: user.id, item_type: 'COMMENTS' }), actionDecrTransformer, @@ -112,34 +116,50 @@ const delUser = async (ctx, id) => { .setOptions({ multi: true }) .remove(); - // Removes all the user's reply counts on each of the comments that they - // have commented on. + // Remove the user from all other user's ignore lists. + await User.update( + { ignoresUsers: user.id }, + { + $pull: { ignoresUsers: user.id }, + }, + { multi: true } + ); + + // For each comment that the user has authored, purge the comment data from it + // and unset their id from those comments. await transformSingleWithCursor( - Comment.collection.aggregate([ - { $match: { author_id: user.id } }, - { - $group: { - _id: '$parent_id', - count: { $sum: 1 }, - }, - }, - ]), - ({ _id: parent_id, count }) => ({ - query: { id: parent_id }, - update: { - $inc: { - reply_count: -1 * count, - }, + Comment.collection.find({ author_id: user.id }), + ({ + id, + asset_id, + status, + parent_id, + reply_count, + created_at, + updated_at, + }) => ({ + query: { id }, + replace: { + id, + body: null, + body_history: [], + asset_id, + author_id: null, + status_history: [], + status, + parent_id, + reply_count, + action_counts: {}, + tags: [], + metadata: {}, + deleted_at: new Date(), + created_at, + updated_at, }, }), Comment ); - // Remove all the user's comments. - await Comment.where({ author_id: user.id }) - .setOptions({ multi: true }) - .remove(); - // Remove the user. await user.remove(); }; @@ -154,7 +174,7 @@ const changeUserPassword = async (ctx, oldPassword, newPassword) => { // Verify the old password. const validPassword = await user.verifyPassword(oldPassword); if (!validPassword) { - throw new ErrNotAuthorized(); + throw new ErrPasswordIncorrect(); } // Change the users password now. @@ -225,7 +245,7 @@ module.exports = ctx => { setUserSuspensionStatus(ctx, id, until, message); } - if (ctx.user.can(DELETE_USER)) { + if (ctx.user.can(DELETE_OTHER_USER)) { mutators.User.del = id => delUser(ctx, id); } diff --git a/graph/resolvers/comment.js b/graph/resolvers/comment.js index 064835f8f..e562c062c 100644 --- a/graph/resolvers/comment.js +++ b/graph/resolvers/comment.js @@ -4,6 +4,7 @@ const { SEARCH_ACTIONS, SEARCH_COMMENT_STATUS_HISTORY, VIEW_BODY_HISTORY, + VIEW_COMMENT_DELETED_AT, } = require('../../perms/constants'); const { decorateWithTags, @@ -23,7 +24,9 @@ const Comment = { return Comments.get.load(parent_id); }, user({ author_id }, _, { loaders: { Users } }) { - return Users.getByID.load(author_id); + if (author_id) { + return Users.getByID.load(author_id); + } }, replies({ id, asset_id, reply_count }, { query }, { loaders: { Comments } }) { // Don't bother looking up replies if there aren't any there! @@ -83,6 +86,7 @@ decorateWithTags(Comment); decorateWithPermissionCheck(Comment, { actions: [SEARCH_ACTIONS], status_history: [SEARCH_COMMENT_STATUS_HISTORY], + deleted_at: [VIEW_COMMENT_DELETED_AT], }); // Protect privileged fields. diff --git a/graph/typeDefs.graphql b/graph/typeDefs.graphql index 03b199e0e..b5c193e3d 100644 --- a/graph/typeDefs.graphql +++ b/graph/typeDefs.graphql @@ -503,7 +503,7 @@ type Comment { id: ID! # The actual comment data. - body: String! + body: String # The body history of the comment. Requires the `ADMIN` or `MODERATOR` role or # the author. @@ -537,6 +537,9 @@ type Comment { # The status history of the comment. Requires the `ADMIN` or `MODERATOR` role. status_history: [CommentStatusHistory!] + # The date that the comment was deleted at if it was. + deleted_at: Date + # The time when the comment was created created_at: Date! diff --git a/locales/en.yml b/locales/en.yml index 393d01e23..e293fb40b 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -225,6 +225,7 @@ en: embedlink: copy: "Copy to Clipboard" error: + PASSWORD_INCORRECT: "Your current password was entered incorrectly" COMMENT_PARENT_NOT_VISIBLE: "The comment that you're replying to has been removed or doesn't exist." EMAIL_VERIFICATION_TOKEN_INVALID: "Email verification token is invalid." EMAIL_ALREADY_VERIFIED: "Email address already verified." @@ -250,7 +251,9 @@ en: ALREADY_EXISTS: "Resource already exists" INVALID_ASSET_URL: "Assert URL is invalid" CANNOT_IGNORE_STAFF: "Cannot ignore Staff members." + INCORRECT_PASSWORD: "Incorrect Password" email: "Please enter a valid email." + DELETION_NOT_SCHEDULED: "Deletion was not scheduled" confirm_password: "Passwords don't match. Please check again" network_error: "Failed to connect to server. Check your internet connection and try again." email_not_verified: "Email address {0} not verified." @@ -271,6 +274,7 @@ en: comment: comment comment_is_ignored: "This comment is hidden because you ignored this user." comment_is_rejected: "You have rejected this comment." + comment_is_deleted: "This comment was deleted." comment_is_hidden: "This comment is not available." comments: comments configure_stream: "Configure" diff --git a/locales/es.yml b/locales/es.yml index 628dd8faa..ff2777c41 100644 --- a/locales/es.yml +++ b/locales/es.yml @@ -16,26 +16,27 @@ es: send: "Enviar" notify_ban_headline: "Notificar al usuario de la suspensión" notify_ban_description: "Esto notificará al usuario por email que fueron suspendidos de la comunidad" - email_message_ban: "Dear {0},\n\nSomeone with access to your account has violated our community guidelines. As a result, your account has been banned. You will no longer be able to comment, like or report comments. if you think this has been done in error, please contact our community team." + email_message_ban: "Estimado/a {0},\n\nUsted o alguien con acceso a su cuenta a violado los lineamientos de nuestra comunidad. Como consecuencia de esto, su cuenta fue bloqueada. No podrá realizar ni reportar más comentarios. Si usted piensa que esto ha sido un error, por favor contáctese con nosotros." bio_offensive: "Esta biografia es ofensiva" cancel: Cancelar confirm_email: - click_to_confirm: "Click below to confirm your email address" - confirm: "Confirm" + click_to_confirm: "Haga click debajo para confirmar su dirección de email" + confirm: "Confirmar" 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: "carácteres restantes" + mail_sent: 'Si tiene una cuenta registrada, un enlace para resetear su clave ha sido enviado a esa dirección de correo.' + set_new_password: "Cambie su contraseña" + new_password: "Nueva contraseña" + new_password_help: "La contraseña debe tener al menos 8 caracteres" + confirm_new_password: "Confirme su nueva contraseña" + change_password: "Cambio de contraseña" + characters_remaining: "caracteres restantes" comment: anon: Anónimo - undo_reject: "Undo" + undo_reject: "Deshacer" ban_user: "Usuario Suspendido" comment: "Publicar un comentario" edited: Editado - flagged: reportado + flagged: Reportado view_context: "Ver contexto" comment_box: post: "Publicar" @@ -62,16 +63,16 @@ es: reactions: 'reacciones' story: 'Artículo' flagged_usernames: - notify_approved: '{0} approved username {1}' - notify_rejected: '{0} rejected username {1}' - notify_flagged: '{0} reported username {1}' - notify_changed: 'user {0} changed their username to {1}' + notify_approved: '{0} ha aprobado el nombre de usuario {1}' + notify_rejected: '{0} ha rechazado el nombre de usuario {1}' + notify_flagged: '{0} ha reportado el nombre de usuario {1}' + notify_changed: 'El usuario {0} ha modificado su nombre de usuario por {1}' community: account_creation_date: "Fecha de creación de la cuenta" active: Activa admin: Administrator - ads_marketing: "This looks like an ad/marketing" - are_you_sure: "¿Estas segura que quieres suspender a {0}?" + ads_marketing: "Esto parece ser un ad/marketing" + are_you_sure: "¿Estás segura que quieres suspender a {0}?" ban_user: "¿Quieres suspender al Usuario?" banned: Suspendido banned_user: "Usuario Suspendido" @@ -201,10 +202,10 @@ es: minutes_plural: "minutos" email: suspended: - subject: "Your account has been suspended" + subject: "Su cuenta ha sido suspendida" banned: - subject: "Your account has been banned" - body: "In accordance with The Coral Project’s community guidelines, your account has been banned. You are now longer allowed to comment, flag or engage with our community." + subject: "Su cuenta ha sido bloqueada" + body: "De acuerdo con las guías de comunidad de The Coral Project, su cuenta a sido bloqueada. No podrá hacer comentarios, reportar o entrar en contacto con nuestra comunidad." confirm: has_been_requested: "Un correo de confirmación ha sido pedido para la siguiente cuenta:" to_confirm: "Para confirmar la cuenta, por favor visitar el siguiente enlace:" @@ -220,8 +221,8 @@ es: copy: "Copiar al portapapeles" error: COMMENT_PARENT_NOT_VISIBLE: "El comentario a la que estás contestando ha sido eliminado o no existe." - EMAIL_VERIFICATION_TOKEN_INVALID: "Email verification token is invalid." - PASSWORD_RESET_TOKEN_INVALID: "Your password reset link is invalid." + EMAIL_VERIFICATION_TOKEN_INVALID: "El token de verificación de su cuenta de correo es inválido." + PASSWORD_RESET_TOKEN_INVALID: "El enlace para resetear su clave es inválido." COMMENT_TOO_SHORT: "Tu comentario debe tener algo escrito" NOT_AUTHORIZED: "Acción no autorizada." NO_SPECIAL_CHARACTERS: "Los nombres pueden contener letras números y _" @@ -230,19 +231,20 @@ es: RATE_LIMIT_EXCEEDED: "Rate limit exceeded" USERNAME_IN_USE: "Este nombre ya está siendo usado." USERNAME_REQUIRED: "Debe ingresar un nombre" - EMAIL_NOT_VERIFIED: "E-mail address not verified" - EDIT_WINDOW_ENDED:: "No puedes editar este comentario. El tiempo de edición ha expirado." + EMAIL_NOT_VERIFIED: "La cuenta de email no ha sido verificada." + EDIT_WINDOW_ENDED: "No puedes editar este comentario. El tiempo de edición ha expirado." EDIT_USERNAME_NOT_AUTHORIZED: "No tiene permiso para editar el nombre de usuario." - SAME_USERNAME_PROVIDED: "You must submit a different username." + SAME_USERNAME_PROVIDED: "Debe proporcinar un nombre de usuario diferente." EMAIL_IN_USE: "Este correo se encuentra en uso" EMAIL_REQUIRED: "Se requiere un correo" LOGIN_MAXIMUM_EXCEEDED: "Ha realizado demasiados intentos fallidos de usar la contraseña. Por favor espere." PASSWORD_REQUIRED: "Debe ingresar la contraseña" COMMENTING_CLOSED: "Los comentarios ya estan cerrados" NOT_FOUND: "Recurso no encontrado" - ALREADY_EXISTS: "Resource already exists" + ALREADY_EXISTS: "El recurso ya existe" INVALID_ASSET_URL: "La URL del articulo no es valida" - CANNOT_IGNORE_STAFF: "Cannot ignore Staff members." + CANNOT_IGNORE_STAFF: "No puede ignorar a miembros del Staff." + INCORRECT_PASSWORD: "Contraseña Incorrecta" email: "No es un correo válido" confirm_password: "Las contraseñas no coinciden. Inténtelo nuevamente" network_error: "Error al conectar con el servidor. Compruebe su conexión a Internet y vuelva a intentarlo." @@ -253,8 +255,8 @@ es: password: "La contraseña debe tener por lo menos 8 caracteres" username: "Los nombres pueden contener letras números y _" required_field: "Este campo es requerido" - unexpected: "Lo siento. Ha habido un error no previsto." - temporarily_suspended: "Your account is currently suspended. It will be reactivated {0}. Please contact us if you have any questions." + unexpected: "Lo siento. Ha ocurrido un error no previsto." + temporarily_suspended: "Su cuenta fue suspendida. Será reactivada {0}. Si tiene preguntas, por favor contáctese con nosotros." flag_comment: "Reportar este comentario" flag_reason: "Razón por la que hacer este reporte (Opcional)" flag_username: "Reportar el nombre de usuario" @@ -274,7 +276,7 @@ es: label: "Nuevo Nombre" msg: "Tu cuenta está suspendida porque tu nombre de usuario ha sido considerado no apropiado para el espacio. Para recuperar la cuenta, por favor ingresar un nuevo nombre de usuario. Contáctanos si tienes alguna pregunta." changed_name: - msg: "Your username change is under review by our moderation team." + msg: "El cambio en su nombre de usuario está siendo revisado por nuestro equipo de moderación." my_comments: "Mis Comentarios" my_profile: "Mi perfil" new_count: "Ver {0} más {1} " @@ -305,9 +307,9 @@ es: comment_spam: "Contiene spam" comment_noagree: "No está de acuerdo" comment_other: "Otra razón" - suspect_word: "Suspect Word" - banned_word: "Banned Word" - body_count: "Body exceeds max length" + suspect_word: "Palabra sospechosa" + banned_word: "Palabra prohibida" + body_count: "El texto exede el límite permitido" trust: "Trust" links: "Link" modqueue: @@ -315,14 +317,14 @@ es: actions: Acciones all: todos all_streams: "Todos los Hilos" - notify_edited: '{0} edited comment "{1}"' - notify_accepted: '{0} accepted comment "{1}"' - notify_rejected: '{0} rejected comment "{1}"' - notify_flagged: '{0} flagged comment "{1}"' - notify_reset: '{0} reset status of comment "{1}"' + notify_edited: '{0} ha editado el comentario "{1}"' + notify_accepted: '{0} ha aceptado el comentario "{1}"' + notify_rejected: '{0} ha rechazado el comentario "{1}"' + notify_flagged: '{0} ha reportado el comentario "{1}"' + notify_reset: '{0} ha reseteado el status del comentario "{1}"' approve: "Aprobar" approved: "Aprobado" - ban_user: "Ban" + ban_user: "Bloquear" billion: B close: Cerrar empty_queue: "¡No hay más comentarios para moderar! Tiempo para un ☕️" @@ -362,7 +364,7 @@ es: no_agree_comment: "No estoy de acuerdo con este comentario" no_like_bio: "No me gusta esta biografia" no_like_username: "No me gusta este nombre de usuario" - already_flagged_username: "You have already flagged this username." + already_flagged_username: "Usted ya reportó a este usuario." other: Otro permalink: Compartir personal_info: "Este comentario muestra información personal" @@ -438,28 +440,28 @@ es: user_bio: "Bio de Usuario" username_flags: "reportes para este nombre de usuario" user_detail: - remove_suspension: "Remove Suspension" - suspend: "Suspend User" - remove_ban: "Remove Ban" - ban: "Ban User" - member_since: "Member Since" + remove_suspension: "Cancelar suspensión" + suspend: "Suspender usuario" + remove_ban: "Cancelar bloqueo" + ban: "Bloquear usuario" + member_since: "Miembro desde" email: "Email" - total_comments: "Total Comments" + total_comments: "Comentarios totales" reject_rate: "Reject Rate" - reports: "Reports" - all: "All" - rejected: "Rejected" - user_history: "User History" + reports: "Reportes" + all: "Todo" + rejected: "Rechazar" + user_history: "Historial del usuario" user_history: - user_banned: "User banned" - ban_removed: "Ban removed" - username_status: "Username {0}" - suspended: "Suspended, {0}" - suspension_removed: "Suspension removed" - system: "System" - date: "Date" - action: "Action" - taken_by: "Taken By" + user_banned: "Usuario bloqueado" + ban_removed: "Bloqueo cancelado" + username_status: "Nombre de usuario {0}" + suspended: "Suspendido, {0}" + suspension_removed: "Suspensión cancelada" + system: "Sistema" + date: "Fecha" + action: "Acción" + taken_by: "Está en uso" user_impersonating: "Este usuario suplanta a alguien" user_no_comment: "No has dejado aún ningún comentario. ¡Únete a la conversación!" username_offensive: "Este nombre de usuario es ofensivo" @@ -488,5 +490,5 @@ es: launch: "Iniciar Talk" close: "Cerrar este instalador" admin_sidebar: - view_options: "View Options" - sort_comments: "Sort Comments" + view_options: "Ver opciones" + sort_comments: "Ordenar comentarios" diff --git a/middleware/trace.js b/middleware/trace.js index 24cbf38f4..6ac6f4b2c 100644 --- a/middleware/trace.js +++ b/middleware/trace.js @@ -3,5 +3,9 @@ const uuid = require('uuid/v1'); // Trace middleware attaches a request id to each incoming request. module.exports = (req, res, next) => { req.id = uuid(); + + // Add the context ID to the request as an HTTP header. + res.set('X-Talk-Trace-ID', req.id); + next(); }; diff --git a/models/schema/comment.js b/models/schema/comment.js index d9586e850..a211884ff 100644 --- a/models/schema/comment.js +++ b/models/schema/comment.js @@ -58,8 +58,6 @@ const Comment = new Schema( }, body: { type: String, - required: [true, 'The body is required.'], - minlength: 2, }, body_history: [BodyHistoryItemSchema], asset_id: String, @@ -89,6 +87,12 @@ const Comment = new Schema( // Tags are added by the self or by administrators. tags: [TagLinkSchema], + // deleted_at stores the date that the given comment was deleted. + deleted_at: { + type: Date, + default: null, + }, + // Additional metadata stored on the field. metadata: { default: {}, diff --git a/package.json b/package.json index c7e9a78c1..9ec249f83 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "talk", - "version": "4.3.2", + "version": "4.4.0", "description": "A better commenting experience from Mozilla, The New York Times, and the Washington Post. https://coralproject.net", "main": "app.js", "private": true, @@ -18,7 +18,7 @@ "lint:js": "eslint bin/cli* .", "lint": "npm-run-all lint:*", "plugins:reconcile": "./bin/cli plugins reconcile", - "test": "npm-run-all test:jest test:mocha", + "test": "npm-run-all test:jest test:server:mocha", "test:jest": "NODE_ENV=test jest --runInBand", "test:client": "NODE_ENV=test jest --projects client", "test:server": "npm-run-all test:server:jest test:server:mocha", @@ -79,11 +79,11 @@ "babel-polyfill": "^6.26.0", "babel-preset-es2015": "6.24.1", "babel-preset-react": "^6.23.0", - "bunyan-debug-stream": "^1.0.8", "bcryptjs": "^2.4.3", "bowser": "^1.7.2", "brotli-webpack-plugin": "^0.5.0", "bunyan": "^1.8.12", + "bunyan-debug-stream": "^1.0.8", "cli-table": "^0.3.1", "clipboard": "^1.7.1", "colors": "^1.1.2", @@ -98,7 +98,6 @@ "dataloader": "^1.3.0", "debug": "3.1.0", "dialog-polyfill": "^0.4.9", - "did-you-mean": "^0.0.1", "dotenv": "^4.0.0", "ejs": "^2.5.7", "env-rewrite": "^1.0.2", @@ -137,8 +136,8 @@ "keymaster": "^1.6.2", "kue": "0.11.6", "linkify-it": "^2.0.3", - "lodash": "^4.16.6", - "lodash-es": "^4.16.6", + "lodash": "^4.17.10", + "lodash-es": "^4.17.10", "marked": "^0.3.6", "material-design-lite": "^1.2.1", "metascraper": "^3.9.2", diff --git a/perms/constants/mutation.js b/perms/constants/mutation.js index 57aa254e7..2d4ae04cb 100644 --- a/perms/constants/mutation.js +++ b/perms/constants/mutation.js @@ -18,6 +18,6 @@ module.exports = { UPDATE_ASSET_SETTINGS: 'UPDATE_ASSET_SETTINGS', UPDATE_ASSET_STATUS: 'UPDATE_ASSET_STATUS', UPDATE_SETTINGS: 'UPDATE_SETTINGS', - DELETE_USER: 'DELETE_USER', + DELETE_OTHER_USER: 'DELETE_OTHER_USER', CHANGE_PASSWORD: 'CHANGE_PASSWORD', }; diff --git a/perms/constants/query.js b/perms/constants/query.js index 197c5d9f9..0c7f4024d 100644 --- a/perms/constants/query.js +++ b/perms/constants/query.js @@ -11,4 +11,5 @@ module.exports = { VIEW_USER_ROLE: 'VIEW_USER_ROLE', VIEW_USER_EMAIL: 'VIEW_USER_EMAIL', VIEW_BODY_HISTORY: 'VIEW_BODY_HISTORY', + VIEW_COMMENT_DELETED_AT: 'VIEW_COMMENT_DELETED_AT', }; diff --git a/perms/reducers/mutation.js b/perms/reducers/mutation.js index 44f66cf88..8133fcd47 100644 --- a/perms/reducers/mutation.js +++ b/perms/reducers/mutation.js @@ -56,6 +56,7 @@ module.exports = (user, perm) => { case types.UPDATE_USER_ROLES: case types.CREATE_TOKEN: case types.REVOKE_TOKEN: + case types.DELETE_OTHER_USER: return check(user, ['ADMIN']); default: diff --git a/perms/reducers/query.js b/perms/reducers/query.js index ed507139d..5c8b17307 100644 --- a/perms/reducers/query.js +++ b/perms/reducers/query.js @@ -14,6 +14,7 @@ module.exports = (user, perm) => { case types.VIEW_USER_ROLE: case types.VIEW_USER_EMAIL: case types.VIEW_BODY_HISTORY: + case types.VIEW_COMMENT_DELETED_AT: return check(user, ['ADMIN', 'MODERATOR']); case types.LIST_OWN_TOKENS: return check(user, ['ADMIN']); diff --git a/plugin-api/beta/client/utils/index.js b/plugin-api/beta/client/utils/index.js index 3fe742569..aeb9841f9 100644 --- a/plugin-api/beta/client/utils/index.js +++ b/plugin-api/beta/client/utils/index.js @@ -8,4 +8,5 @@ export { getErrorMessages, getDefinitionName, getShallowChanges, + createDefaultResponseFragments, } from 'coral-framework/utils'; diff --git a/plugin-api/beta/server/getReactionConfig.js b/plugin-api/beta/server/getReactionConfig.js index e90a06122..5aa3063b3 100644 --- a/plugin-api/beta/server/getReactionConfig.js +++ b/plugin-api/beta/server/getReactionConfig.js @@ -6,6 +6,7 @@ const { CREATE_MONGO_INDEXES } = require('../../../config'); const Comment = require('models/comment'); function getReactionConfig(reaction) { + // Ensure that the reaction is a lowercase string. reaction = reaction.toLowerCase(); if (CREATE_MONGO_INDEXES) { diff --git a/plugins.default.json b/plugins.default.json index e1011aa87..0fbc6e658 100644 --- a/plugins.default.json +++ b/plugins.default.json @@ -1,9 +1,9 @@ { "server": [ - "talk-plugin-auth", "talk-plugin-featured-comments", - "talk-plugin-respect", - "talk-plugin-profile-data" + "talk-plugin-local-auth", + "talk-plugin-profile-data", + "talk-plugin-respect" ], "client": [ "talk-plugin-auth", @@ -11,15 +11,16 @@ "talk-plugin-featured-comments", "talk-plugin-flag-details", "talk-plugin-ignore-user", + "talk-plugin-local-auth", "talk-plugin-member-since", "talk-plugin-moderation-actions", "talk-plugin-permalink", + "talk-plugin-profile-data", "talk-plugin-respect", "talk-plugin-sort-most-replied", "talk-plugin-sort-most-respected", "talk-plugin-sort-newest", "talk-plugin-sort-oldest", - "talk-plugin-viewing-options", - "talk-plugin-profile-data" + "talk-plugin-viewing-options" ] } diff --git a/plugins/talk-plugin-auth/README.md b/plugins/talk-plugin-auth/README.md index fd5218365..c09dc218b 100644 --- a/plugins/talk-plugin-auth/README.md +++ b/plugins/talk-plugin-auth/README.md @@ -14,3 +14,9 @@ utilize our internal authentication system. To sync Talk auth with your own auth systems, you can use this plugin as a template. + +## GDPR Compliance + +In order to facilitate compliance with the +[EU General Data Protection Regulation (GDPR)](https://www.eugdpr.org/), you +should review our [GDPR Compliance](/talk/integrating/gdpr/) guidelines. diff --git a/plugins/talk-plugin-auth/client/index.js b/plugins/talk-plugin-auth/client/index.js index 8fedb8033..13abce1d9 100644 --- a/plugins/talk-plugin-auth/client/index.js +++ b/plugins/talk-plugin-auth/client/index.js @@ -4,8 +4,6 @@ import SetUsernameDialog from './stream/containers/SetUsernameDialog'; import translations from './translations.yml'; import Login from './login/containers/Main'; import reducer from './login/reducer'; -import ChangePassword from './profile-settings/containers/ChangePassword'; -import ChangeUsername from './profile-settings/containers/ChangeUsername'; export default { reducer, @@ -13,7 +11,5 @@ export default { slots: { stream: [UserBox, SignInButton, SetUsernameDialog], login: [Login], - profileHeader: [ChangeUsername], - profileSettings: [ChangePassword], }, }; diff --git a/plugins/talk-plugin-auth/client/profile-settings/components/ChangeUsername.js b/plugins/talk-plugin-auth/client/profile-settings/components/ChangeUsername.js deleted file mode 100644 index 8188bdfbe..000000000 --- a/plugins/talk-plugin-auth/client/profile-settings/components/ChangeUsername.js +++ /dev/null @@ -1,188 +0,0 @@ -import React from 'react'; -import cn from 'classnames'; -import PropTypes from 'prop-types'; -import styles from './ChangeUsername.css'; -import { Button } from 'plugin-api/beta/client/components/ui'; -import ChangeUsernameDialog from './ChangeUsernameDialog'; -import { t } from 'plugin-api/beta/client/services'; -import InputField from './InputField'; -import { getErrorMessages } from 'coral-framework/utils'; -import { canUsernameBeUpdated } from 'coral-framework/utils/user'; - -const initialState = { - editing: false, - showDialog: false, - formData: {}, -}; - -class ChangeUsername extends React.Component { - state = initialState; - - clearForm = () => { - this.setState(initialState); - }; - - enableEditing = () => { - this.setState({ - editing: true, - }); - }; - - disableEditing = () => { - this.setState({ - editing: false, - }); - }; - - cancel = () => { - this.clearForm(); - this.disableEditing(); - }; - - showDialog = () => { - this.setState({ - showDialog: true, - }); - }; - - onSave = async () => { - this.showDialog(); - }; - - saveChanges = async () => { - const { newUsername } = this.state.formData; - const { changeUsername } = this.props; - - try { - await changeUsername(newUsername); - this.props.notify( - 'success', - t('talk-plugin-auth.change_username.changed_username_success_msg') - ); - } catch (err) { - this.props.notify('error', getErrorMessages(err)); - } - - this.clearForm(); - this.disableEditing(); - }; - - onChange = e => { - const { name, value } = e.target; - - this.setState(state => ({ - formData: { - ...state.formData, - [name]: value, - }, - })); - }; - - closeDialog = () => { - this.setState({ - showDialog: false, - }); - }; - - render() { - const { - username, - emailAddress, - root: { me: { state: { status } } }, - notify, - } = this.props; - const { editing, formData, showDialog } = this.state; - - return ( -{emailAddress}
- ) : null} -+ {t('talk-plugin-local-auth.add_email.content.description')} +
++ {t('talk-plugin-local-auth.change_email.description')} +
+