mirror of
https://github.com/wassname/talk.git
synced 2026-08-08 11:28:12 +08:00
Merge branch 'master' into 155836892
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
const express = require('express');
|
||||
const morgan = require('morgan');
|
||||
const trace = require('./middleware/trace');
|
||||
const logging = require('./middleware/logging');
|
||||
const path = require('path');
|
||||
const merge = require('lodash/merge');
|
||||
const helmet = require('helmet');
|
||||
@@ -12,6 +13,10 @@ const { ENABLE_TRACING, APOLLO_ENGINE_KEY, PORT } = require('./config');
|
||||
|
||||
const app = express();
|
||||
|
||||
// Add the trace middleware first, it will create a request ID for each request
|
||||
// downstream.
|
||||
app.use(trace);
|
||||
|
||||
//==============================================================================
|
||||
// PLUGIN PRE APPLICATION MIDDLEWARE
|
||||
//==============================================================================
|
||||
@@ -30,7 +35,7 @@ plugins.get('server', 'app').forEach(({ plugin, app: callback }) => {
|
||||
|
||||
// Add the logging middleware only if we aren't testing.
|
||||
if (process.env.NODE_ENV !== 'test') {
|
||||
app.use(morgan('dev'));
|
||||
app.use(logging.log);
|
||||
}
|
||||
|
||||
if (ENABLE_TRACING && APOLLO_ENGINE_KEY) {
|
||||
|
||||
+7
-5
@@ -14,7 +14,7 @@ const SettingsService = require('../services/settings');
|
||||
const SetupService = require('../services/setup');
|
||||
const UsersService = require('../services/users');
|
||||
const MigrationService = require('../services/migration');
|
||||
const errors = require('../errors');
|
||||
const { ErrSettingsInit, ErrSettingsNotInit } = require('../errors');
|
||||
const Context = require('../graph/context');
|
||||
|
||||
// Register the shutdown criteria.
|
||||
@@ -41,13 +41,15 @@ const performSetup = async () => {
|
||||
|
||||
// We should NOT have gotten a settings object, this means that the
|
||||
// application is already setup. Error out here.
|
||||
throw errors.ErrSettingsInit;
|
||||
} catch (e) {
|
||||
throw new ErrSettingsInit();
|
||||
} catch (err) {
|
||||
// If the error is `not init`, then we're good, otherwise, it's something
|
||||
// else.
|
||||
if (e !== errors.ErrSettingsNotInit) {
|
||||
throw e;
|
||||
if (err instanceof ErrSettingsNotInit) {
|
||||
return;
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
|
||||
if (program.defaults) {
|
||||
|
||||
@@ -5,14 +5,7 @@ export const singleView = () => ({ type: actions.SINGLE_VIEW });
|
||||
|
||||
// hide shortcuts note
|
||||
export const hideShortcutsNote = () => (dispatch, _, { localStorage }) => {
|
||||
try {
|
||||
if (localStorage) {
|
||||
localStorage.setItem('coral:shortcutsNote', 'hide');
|
||||
}
|
||||
} catch (e) {
|
||||
// above will fail in Safari private mode
|
||||
}
|
||||
|
||||
localStorage.setItem('coral:shortcutsNote', 'hide');
|
||||
dispatch({ type: actions.HIDE_SHORTCUTS_NOTE });
|
||||
};
|
||||
|
||||
|
||||
@@ -15,7 +15,8 @@ import { hideShortcutsNote } from './actions/moderation';
|
||||
smoothscroll.polyfill();
|
||||
|
||||
function init({ store, localStorage }) {
|
||||
if (localStorage && localStorage.getItem('coral:shortcutsNote') === 'hide') {
|
||||
const shouldHide = localStorage.getItem('coral:shortcutsNote') === 'hide';
|
||||
if (shouldHide) {
|
||||
store.dispatch(hideShortcutsNote());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
import React from 'react';
|
||||
import { render } from 'react-dom';
|
||||
import { GraphQLDocs } from 'graphql-docs';
|
||||
|
||||
import fetcher from './services/fetcher';
|
||||
|
||||
// Render the application into the DOM
|
||||
render(<GraphQLDocs fetcher={fetcher} />, document.querySelector('#root'));
|
||||
@@ -1,10 +0,0 @@
|
||||
export default function fetcher(query) {
|
||||
return fetch(`${window.location.origin}/api/v1/graph/ql`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ query }),
|
||||
}).then(res => res.json());
|
||||
}
|
||||
@@ -15,9 +15,7 @@ export const checkLogin = () => (
|
||||
rest('/auth')
|
||||
.then(result => {
|
||||
if (!result.user) {
|
||||
if (localStorage) {
|
||||
cleanAuthData(localStorage);
|
||||
}
|
||||
cleanAuthData(localStorage);
|
||||
dispatch(checkLoginSuccess(null));
|
||||
return;
|
||||
}
|
||||
@@ -52,10 +50,8 @@ const checkLoginSuccess = user => ({
|
||||
});
|
||||
|
||||
export const setAuthToken = token => (dispatch, _, { localStorage }) => {
|
||||
if (localStorage) {
|
||||
localStorage.setItem('exp', jwtDecode(token).exp);
|
||||
localStorage.setItem('token', token);
|
||||
}
|
||||
localStorage.setItem('exp', jwtDecode(token).exp);
|
||||
localStorage.setItem('token', 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!
|
||||
@@ -70,11 +66,8 @@ export const handleSuccessfulLogin = (user, token) => (
|
||||
{ client, localStorage, postMessage }
|
||||
) => {
|
||||
const { exp } = jwtDecode(token);
|
||||
|
||||
if (localStorage) {
|
||||
localStorage.setItem('exp', exp);
|
||||
localStorage.setItem('token', token);
|
||||
}
|
||||
localStorage.setItem('exp', exp);
|
||||
localStorage.setItem('token', token);
|
||||
|
||||
// Send the message via the messages service to the window.opener if it
|
||||
// exists.
|
||||
@@ -105,9 +98,8 @@ export const logout = () => async (
|
||||
) => {
|
||||
await rest('/auth', { method: 'DELETE' });
|
||||
|
||||
if (localStorage) {
|
||||
cleanAuthData(localStorage);
|
||||
}
|
||||
// Clear the auth data persisted to localStorage.
|
||||
cleanAuthData(localStorage);
|
||||
|
||||
// Reset the websocket.
|
||||
client.resetWebsocket();
|
||||
|
||||
@@ -52,26 +52,41 @@ let lang;
|
||||
let timeagoInstance;
|
||||
|
||||
function setLocale(storage, locale) {
|
||||
try {
|
||||
if (storage) {
|
||||
storage.setItem('locale', locale);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
storage.setItem('locale', locale);
|
||||
}
|
||||
|
||||
function getLocale(storage) {
|
||||
// detectLanguage will try to get the locale from storage if available,
|
||||
// otherwise will try to get it from the navigator, otherwise, it will fallback
|
||||
// to the default language.
|
||||
function detectLanguage(storage) {
|
||||
try {
|
||||
return (
|
||||
(storage && storage.getItem('locale')) ||
|
||||
navigator.language ||
|
||||
defaultLanguage
|
||||
).split('-')[0];
|
||||
const lang = storage.getItem('locale') || navigator.language;
|
||||
if (lang) {
|
||||
return lang;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
return null;
|
||||
console.warn(
|
||||
'Error while trying to detect language, will fallback to',
|
||||
err
|
||||
);
|
||||
}
|
||||
|
||||
console.warn('Could not detect language, will fallback to', defaultLanguage);
|
||||
return defaultLanguage;
|
||||
}
|
||||
|
||||
// getLocale will get the users locale from the local detector and parse it to a
|
||||
// format we can work with.
|
||||
function getLocale(storage) {
|
||||
// Get the language from the local detector.
|
||||
const lang = detectLanguage(storage);
|
||||
|
||||
// Some language strings come with additional subtags as defined in:
|
||||
//
|
||||
// https://www.ietf.org/rfc/bcp/bcp47.txt
|
||||
//
|
||||
// So we should strip that off if we find it.
|
||||
return lang.split('-')[0];
|
||||
}
|
||||
|
||||
export function setupTranslations() {
|
||||
|
||||
@@ -1,40 +1,116 @@
|
||||
import uuid from 'uuid/v4';
|
||||
|
||||
function getStorage(type) {
|
||||
let storage;
|
||||
try {
|
||||
storage = window[type];
|
||||
const x = '__storage_test__';
|
||||
storage.setItem(x, x);
|
||||
storage.removeItem(x);
|
||||
} catch (e) {
|
||||
const ignore =
|
||||
e instanceof DOMException &&
|
||||
// everything except Firefox
|
||||
(e.code === 22 ||
|
||||
// SecurityError related to having 3rd party cookies disabled.
|
||||
e.code === 18 ||
|
||||
// Firefox
|
||||
function testStorageAccess(storage) {
|
||||
const key = '__storage_test__';
|
||||
|
||||
e.code === 1014 ||
|
||||
// test name field too, because code might not be present
|
||||
// Create a unique test value.
|
||||
const expectedValue = String(Date.now());
|
||||
|
||||
// everything except Firefox
|
||||
e.name === 'QuotaExceededError' ||
|
||||
// Firefox
|
||||
e.name === 'NS_ERROR_DOM_QUOTA_REACHED');
|
||||
if (!ignore) {
|
||||
console.warn(e);
|
||||
// Try to set, get, and remove that item.
|
||||
storage.setItem(key, expectedValue);
|
||||
const canSetGet = expectedValue === storage.getItem(key);
|
||||
storage.removeItem(key);
|
||||
|
||||
if (!canSetGet) {
|
||||
// We can't access the desired storage!
|
||||
throw new Error('Storage access test failed');
|
||||
}
|
||||
}
|
||||
|
||||
// InMemoryStorage is a dumb implementation of the Storage interface that will
|
||||
// not persist the data at all. It implements the Storage interface found:
|
||||
//
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/Storage
|
||||
//
|
||||
class InMemoryStorage {
|
||||
constructor() {
|
||||
this.storage = {};
|
||||
}
|
||||
|
||||
get length() {
|
||||
return Object.keys(this.storage).length;
|
||||
}
|
||||
|
||||
key(n) {
|
||||
if (this.length <= n) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// When third party cookies are disabled, session storage is readable/
|
||||
// writable, but localStorage is not. Try to get the sessionStorage to use.
|
||||
if (type !== 'sessionStorage') {
|
||||
return getStorage('sessionStorage');
|
||||
return this.storage[Object.keys(this.storage)[n]];
|
||||
}
|
||||
|
||||
getItem(key) {
|
||||
return this.storage[key];
|
||||
}
|
||||
|
||||
setItem(key, value) {
|
||||
this.storage[key] = value;
|
||||
|
||||
try {
|
||||
// Test sessionStorage. We could have been given access recently.
|
||||
testStorageAccess(sessionStorage);
|
||||
|
||||
// Test passed! Set the item in sessionStorage.
|
||||
sessionStorage.setItem(key, value);
|
||||
console.log(
|
||||
'Attempt to persist InMemoryStorage value to sessionStorage succeeded'
|
||||
);
|
||||
} catch (err) {
|
||||
console.warn(
|
||||
'Attempt to persist InMemoryStorage value to sessionStorage failed',
|
||||
err
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return storage;
|
||||
removeItem(key) {
|
||||
delete this.storage[key];
|
||||
|
||||
try {
|
||||
// Test sessionStorage. We could have been given access recently.
|
||||
testStorageAccess(sessionStorage);
|
||||
|
||||
// Test passed! Remove the item from sessionStorage.
|
||||
sessionStorage.removeItem(key);
|
||||
console.log(
|
||||
'Attempt to persist InMemoryStorage delete to sessionStorage succeeded'
|
||||
);
|
||||
} catch (err) {
|
||||
console.warn(
|
||||
'Attempt to persist InMemoryStorage delete to sessionStorage failed',
|
||||
err
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// getStorage will test to see if the requested storage type is available, if it
|
||||
// is not, it will try sessionStorage, and if that is also not available, it
|
||||
// will fallback to InMemoryStorage.
|
||||
function getStorage(type) {
|
||||
try {
|
||||
// Get the desired storage from the window and test it out.
|
||||
const storage = window[type];
|
||||
testStorageAccess(storage);
|
||||
|
||||
// Storage test was successful! Return it.
|
||||
return storage;
|
||||
} catch (err) {
|
||||
// When third party cookies are disabled, session storage is readable/
|
||||
// writable, but localStorage is not. Try to get the sessionStorage to use.
|
||||
if (type !== 'sessionStorage') {
|
||||
console.warn('Could not access', type, 'trying sessionStorage', err);
|
||||
return getStorage('sessionStorage');
|
||||
}
|
||||
|
||||
console.warn(
|
||||
'Could not access sessionStorage falling back to InMemoryStorage',
|
||||
err
|
||||
);
|
||||
}
|
||||
|
||||
// No acceptable storage could be found, returning the InMemoryStorage.
|
||||
return new InMemoryStorage();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -27,11 +27,14 @@ const TextField = ({
|
||||
);
|
||||
|
||||
TextField.propTypes = {
|
||||
id: PropTypes.string,
|
||||
label: PropTypes.string,
|
||||
value: PropTypes.string,
|
||||
onChange: PropTypes.func,
|
||||
errorMsg: PropTypes.string,
|
||||
type: PropTypes.string,
|
||||
className: PropTypes.string,
|
||||
showErrors: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default TextField;
|
||||
|
||||
@@ -122,6 +122,8 @@ sidebar:
|
||||
url: /commenter-features/
|
||||
- title: Moderator Features
|
||||
url: /moderator-features/
|
||||
- title: User Roles in Talk
|
||||
url: /roles/
|
||||
- title: Trust
|
||||
url: /trust/
|
||||
- title: Toxic Comments
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
---
|
||||
title: User Roles in Talk
|
||||
permalink: /roles/
|
||||
---
|
||||
|
||||
We have four preset roles in Talk:
|
||||
|
||||
**Commenter**
|
||||
* A standard community member
|
||||
* Could receive a badge (eg. 'Subscriber') via [a custom newsroom Plugin Recipe](https://docs.coralproject.net/talk/plugin-recipes/#recipe-subscriber)
|
||||
* No moderation abilities
|
||||
* No configuration abilities
|
||||
|
||||
**Staff**
|
||||
* A standard community member
|
||||
* Receives a Staff badge when they comment
|
||||
* Comments are automatically approved
|
||||
* No moderation abilities
|
||||
* No configuration abilities
|
||||
|
||||
**Moderator**
|
||||
* A standard community member
|
||||
* Receives a Staff badge when they comment
|
||||
* Comments are automatically approved
|
||||
* Has full moderation privileges
|
||||
* Can configure individual articles via the Configure tab on the article page
|
||||
* No site-wide configuration abilities
|
||||
|
||||
**Administrator**
|
||||
* A standard community member
|
||||
* Receives a Staff badge when they comment
|
||||
* Comments are automatically approved
|
||||
* Has full moderation privileges
|
||||
* Can configure individual articles via the Configure tab on the article page
|
||||
* Can configure site settings via the Configure tab in the moderation interface
|
||||
@@ -10,21 +10,21 @@ class ExtendableError {
|
||||
}
|
||||
|
||||
/**
|
||||
* APIError is the base error that all application issued errors originate, they
|
||||
* are composed of data used by the front end and backend to handle errors
|
||||
* TalkError is the base error that all application issued errors originate,
|
||||
* they are composed of data used by the front end and backend to handle errors
|
||||
* consistently.
|
||||
*/
|
||||
class APIError extends ExtendableError {
|
||||
class TalkError extends ExtendableError {
|
||||
constructor(
|
||||
message,
|
||||
{ status = 500, translation_key = null },
|
||||
{ status = 500, translation_key = null } = {},
|
||||
metadata = {}
|
||||
) {
|
||||
super(message);
|
||||
|
||||
this.status = status;
|
||||
this.translation_key = translation_key;
|
||||
this.metadata = metadata;
|
||||
this.status = status || 500;
|
||||
this.translation_key = translation_key || null;
|
||||
this.metadata = metadata || {};
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
@@ -38,85 +38,114 @@ class APIError extends ExtendableError {
|
||||
}
|
||||
|
||||
// ErrPasswordTooShort is returned when the password length is too short.
|
||||
const ErrPasswordTooShort = new APIError(
|
||||
'password must be at least 8 characters',
|
||||
{
|
||||
status: 400,
|
||||
translation_key: 'PASSWORD_LENGTH',
|
||||
class ErrPasswordTooShort extends TalkError {
|
||||
constructor() {
|
||||
super('password must be at least 8 characters', {
|
||||
status: 400,
|
||||
translation_key: 'PASSWORD_LENGTH',
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const ErrMissingEmail = new APIError('email is required', {
|
||||
translation_key: 'EMAIL_REQUIRED',
|
||||
status: 400,
|
||||
});
|
||||
|
||||
const ErrMissingPassword = new APIError('password is required', {
|
||||
translation_key: 'PASSWORD_REQUIRED',
|
||||
status: 400,
|
||||
});
|
||||
|
||||
const ErrEmailTaken = new APIError('Email address already in use', {
|
||||
translation_key: 'EMAIL_IN_USE',
|
||||
status: 400,
|
||||
});
|
||||
|
||||
const ErrUsernameTaken = new APIError('Username already in use', {
|
||||
translation_key: 'USERNAME_IN_USE',
|
||||
status: 400,
|
||||
});
|
||||
|
||||
const ErrSameUsernameProvided = new APIError(
|
||||
'Username provided for change is the same as current',
|
||||
{
|
||||
translation_key: 'SAME_USERNAME_PROVIDED',
|
||||
status: 400,
|
||||
class ErrMissingEmail extends TalkError {
|
||||
constructor() {
|
||||
super('email is required', {
|
||||
translation_key: 'EMAIL_REQUIRED',
|
||||
status: 400,
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const ErrSpecialChars = new APIError(
|
||||
'No special characters are allowed in a username',
|
||||
{
|
||||
translation_key: 'NO_SPECIAL_CHARACTERS',
|
||||
status: 400,
|
||||
class ErrMissingPassword extends TalkError {
|
||||
constructor() {
|
||||
super('password is required', {
|
||||
translation_key: 'PASSWORD_REQUIRED',
|
||||
status: 400,
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const ErrMissingUsername = new APIError(
|
||||
'A username is required to create a user',
|
||||
{
|
||||
translation_key: 'USERNAME_REQUIRED',
|
||||
status: 400,
|
||||
class ErrEmailTaken extends TalkError {
|
||||
constructor() {
|
||||
super('Email address already in use', {
|
||||
translation_key: 'EMAIL_IN_USE',
|
||||
status: 400,
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
class ErrUsernameTaken extends TalkError {
|
||||
constructor() {
|
||||
super('Username already in use', {
|
||||
translation_key: 'USERNAME_IN_USE',
|
||||
status: 400,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class ErrSameUsernameProvided extends TalkError {
|
||||
constructor() {
|
||||
super('Username provided for change is the same as current', {
|
||||
translation_key: 'SAME_USERNAME_PROVIDED',
|
||||
status: 400,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class ErrSpecialChars extends TalkError {
|
||||
constructor() {
|
||||
super('No special characters are allowed in a username', {
|
||||
translation_key: 'NO_SPECIAL_CHARACTERS',
|
||||
status: 400,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class ErrMissingUsername extends TalkError {
|
||||
constructor() {
|
||||
super('A username is required to create a user', {
|
||||
translation_key: 'USERNAME_REQUIRED',
|
||||
status: 400,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ErrEmailVerificationToken is returned in the event that the password reset is requested
|
||||
// without a token.
|
||||
const ErrEmailVerificationToken = new APIError('token is required', {
|
||||
translation_key: 'EMAIL_VERIFICATION_TOKEN_INVALID',
|
||||
status: 400,
|
||||
});
|
||||
class ErrEmailVerificationToken extends TalkError {
|
||||
constructor() {
|
||||
super('token is required', {
|
||||
translation_key: 'EMAIL_VERIFICATION_TOKEN_INVALID',
|
||||
status: 400,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ErrEmailAlreadyVerified is returned when the user tries to verify an email
|
||||
// address that has already been verified.
|
||||
const ErrEmailAlreadyVerified = new APIError(
|
||||
'email address is already verified',
|
||||
{
|
||||
translation_key: 'EMAIL_ALREADY_VERIFIED',
|
||||
status: 409,
|
||||
class ErrEmailAlreadyVerified extends TalkError {
|
||||
constructor() {
|
||||
super('email address is already verified', {
|
||||
translation_key: 'EMAIL_ALREADY_VERIFIED',
|
||||
status: 409,
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// ErrPasswordResetToken is returned in the event that the password reset is requested
|
||||
// without a token.
|
||||
const ErrPasswordResetToken = new APIError('token is required', {
|
||||
translation_key: 'PASSWORD_RESET_TOKEN_INVALID',
|
||||
status: 400,
|
||||
});
|
||||
class ErrPasswordResetToken extends TalkError {
|
||||
constructor() {
|
||||
super('token is required', {
|
||||
translation_key: 'PASSWORD_RESET_TOKEN_INVALID',
|
||||
status: 400,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ErrAssetCommentingClosed is returned when a comment or action is attempted on
|
||||
// a stream where commenting has been closed.
|
||||
class ErrAssetCommentingClosed extends APIError {
|
||||
class ErrAssetCommentingClosed extends TalkError {
|
||||
constructor(closedMessage = null) {
|
||||
super(
|
||||
'asset commenting is closed',
|
||||
@@ -136,7 +165,7 @@ class ErrAssetCommentingClosed extends APIError {
|
||||
* ErrAuthentication is returned when there is an error authenticating and the
|
||||
* message is provided.
|
||||
*/
|
||||
class ErrAuthentication extends APIError {
|
||||
class ErrAuthentication extends TalkError {
|
||||
constructor(message = null) {
|
||||
super(
|
||||
'authentication error occurred',
|
||||
@@ -154,7 +183,7 @@ class ErrAuthentication extends APIError {
|
||||
/**
|
||||
* ErrAlreadyExists is returned when an attempt to create a resource failed due to an existing one.
|
||||
*/
|
||||
class ErrAlreadyExists extends APIError {
|
||||
class ErrAlreadyExists extends TalkError {
|
||||
constructor(existing = null) {
|
||||
super(
|
||||
'resource already exists',
|
||||
@@ -171,121 +200,179 @@ class ErrAlreadyExists extends APIError {
|
||||
|
||||
// ErrContainsProfanity is returned in the event that the middleware detects
|
||||
// profanity/banned/suspect words in the payload.
|
||||
const ErrContainsProfanity = new APIError(
|
||||
'This username contains elements which are not permitted in our community. If you think this is in error, please contact us or try again.',
|
||||
{
|
||||
translation_key: 'PROFANITY_ERROR',
|
||||
status: 400,
|
||||
class ErrContainsProfanity extends TalkError {
|
||||
constructor(phrase) {
|
||||
super(
|
||||
'This username contains elements which are not permitted in our community. If you think this is in error, please contact us or try again.',
|
||||
{
|
||||
translation_key: 'PROFANITY_ERROR',
|
||||
status: 400,
|
||||
},
|
||||
{ phrase }
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const ErrNotFound = new APIError('not found', {
|
||||
translation_key: 'NOT_FOUND',
|
||||
status: 404,
|
||||
});
|
||||
class ErrNotFound extends TalkError {
|
||||
constructor() {
|
||||
super('not found', {
|
||||
translation_key: 'NOT_FOUND',
|
||||
status: 404,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const ErrInvalidAssetURL = new APIError('asset_url is invalid', {
|
||||
translation_key: 'INVALID_ASSET_URL',
|
||||
status: 400,
|
||||
});
|
||||
class ErrInvalidAssetURL extends TalkError {
|
||||
constructor() {
|
||||
super('asset_url is invalid', {
|
||||
translation_key: 'INVALID_ASSET_URL',
|
||||
status: 400,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ErrNotAuthorized is an error that is returned in the event an operation is
|
||||
// deemed not authorized.
|
||||
const ErrNotAuthorized = new APIError('not authorized', {
|
||||
translation_key: 'NOT_AUTHORIZED',
|
||||
status: 401,
|
||||
});
|
||||
class ErrNotAuthorized extends TalkError {
|
||||
constructor() {
|
||||
super('not authorized', {
|
||||
translation_key: 'NOT_AUTHORIZED',
|
||||
status: 401,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ErrSettingsNotInit is returned when the settings are required but not
|
||||
// initialized.
|
||||
const ErrSettingsNotInit = new Error(
|
||||
'Talk is currently not setup. Please proceed to our web installer at $ROOT_URL/admin/install or run ./bin/cli-setup. Visit https://docs.coralproject.net/talk/ for more information on installation and configuration instructions'
|
||||
);
|
||||
class ErrSettingsNotInit extends TalkError {
|
||||
constructor() {
|
||||
super(
|
||||
'Talk is currently not setup. Please proceed to our web installer at $ROOT_URL/admin/install or run ./bin/cli-setup. Visit https://docs.coralproject.net/talk/ for more information on installation and configuration instructions'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ErrSettingsInit is returned when the setup endpoint is hit and we are already
|
||||
// initialized.
|
||||
const ErrSettingsInit = new APIError('settings are already initialized', {
|
||||
status: 500,
|
||||
});
|
||||
class ErrSettingsInit extends TalkError {
|
||||
constructor() {
|
||||
super('settings are already initialized', {
|
||||
status: 500,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ErrInstallLock is returned when the setup endpoint is hit and the install
|
||||
// lock is present.
|
||||
const ErrInstallLock = new APIError('install lock active', {
|
||||
status: 500,
|
||||
});
|
||||
class ErrInstallLock extends TalkError {
|
||||
constructor() {
|
||||
super('install lock active', {
|
||||
status: 500,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ErrPermissionUpdateUsername is returned when the user does not have permission to update their username.
|
||||
const ErrPermissionUpdateUsername = new APIError(
|
||||
'You do not have permission to update your username.',
|
||||
{
|
||||
translation_key: 'EDIT_USERNAME_NOT_AUTHORIZED',
|
||||
status: 403,
|
||||
class ErrPermissionUpdateUsername extends TalkError {
|
||||
constructor() {
|
||||
super('You do not have permission to update your username.', {
|
||||
translation_key: 'EDIT_USERNAME_NOT_AUTHORIZED',
|
||||
status: 403,
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// ErrLoginAttemptMaximumExceeded is returned when the login maximum is exceeded.
|
||||
const ErrLoginAttemptMaximumExceeded = new APIError(
|
||||
'You have made too many incorrect password attempts.',
|
||||
{
|
||||
translation_key: 'LOGIN_MAXIMUM_EXCEEDED',
|
||||
status: 429,
|
||||
class ErrLoginAttemptMaximumExceeded extends TalkError {
|
||||
constructor() {
|
||||
super('You have made too many incorrect password attempts.', {
|
||||
translation_key: 'LOGIN_MAXIMUM_EXCEEDED',
|
||||
status: 429,
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// ErrEditWindowHasEnded is returned when the edit window has expired.
|
||||
const ErrEditWindowHasEnded = new APIError('Edit window is over', {
|
||||
translation_key: 'EDIT_WINDOW_ENDED',
|
||||
status: 403,
|
||||
});
|
||||
class ErrEditWindowHasEnded extends TalkError {
|
||||
constructor() {
|
||||
super('Edit window is over', {
|
||||
translation_key: 'EDIT_WINDOW_ENDED',
|
||||
status: 403,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ErrCommentTooShort is returned when the comment is too short.
|
||||
const ErrCommentTooShort = new APIError('Comment was too short', {
|
||||
translation_key: 'COMMENT_TOO_SHORT',
|
||||
status: 400,
|
||||
});
|
||||
class ErrCommentTooShort extends TalkError {
|
||||
constructor(length) {
|
||||
super(
|
||||
'Comment was too short',
|
||||
{
|
||||
translation_key: 'COMMENT_TOO_SHORT',
|
||||
status: 400,
|
||||
},
|
||||
{ length }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ErrAssetURLAlreadyExists is returned when a rename operation is requested
|
||||
// but an asset already exists with the new url.
|
||||
const ErrAssetURLAlreadyExists = new APIError(
|
||||
'Asset URL already exists, cannot rename',
|
||||
{
|
||||
translation_key: 'ASSET_URL_ALREADY_EXISTS',
|
||||
status: 409,
|
||||
class ErrAssetURLAlreadyExists extends TalkError {
|
||||
constructor() {
|
||||
super('Asset URL already exists, cannot rename', {
|
||||
translation_key: 'ASSET_URL_ALREADY_EXISTS',
|
||||
status: 409,
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// ErrNotVerified is returned when a user tries to login with valid credentials
|
||||
// but their email address is not yet verified.
|
||||
const ErrNotVerified = new APIError(
|
||||
'User does not have a verified email address',
|
||||
{
|
||||
translation_key: 'EMAIL_NOT_VERIFIED',
|
||||
status: 401,
|
||||
class ErrNotVerified extends TalkError {
|
||||
constructor() {
|
||||
super('User does not have a verified email address', {
|
||||
translation_key: 'EMAIL_NOT_VERIFIED',
|
||||
status: 401,
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const ErrMaxRateLimit = new APIError('Rate limit exceeded', {
|
||||
translation_key: 'RATE_LIMIT_EXCEEDED',
|
||||
status: 429,
|
||||
});
|
||||
class ErrMaxRateLimit extends TalkError {
|
||||
constructor(max, tries) {
|
||||
super(
|
||||
'Rate limit exceeded',
|
||||
{
|
||||
translation_key: 'RATE_LIMIT_EXCEEDED',
|
||||
status: 429,
|
||||
},
|
||||
{ tries, max }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ErrCannotIgnoreStaff is returned when a user tries to ignore a staff member.
|
||||
const ErrCannotIgnoreStaff = new APIError('Cannot ignore staff members.', {
|
||||
translation_key: 'CANNOT_IGNORE_STAFF',
|
||||
status: 400,
|
||||
});
|
||||
class ErrCannotIgnoreStaff extends TalkError {
|
||||
constructor() {
|
||||
super('Cannot ignore staff members.', {
|
||||
translation_key: 'CANNOT_IGNORE_STAFF',
|
||||
status: 400,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ErrParentDoesNotVisible is returned when the user tries to reply to a comment
|
||||
// that isn't visible.
|
||||
const ErrParentDoesNotVisible = new APIError(
|
||||
'Cannot reply to a comment that is not visible',
|
||||
{
|
||||
translation_key: 'COMMENT_PARENT_NOT_VISIBLE',
|
||||
class ErrParentDoesNotVisible extends TalkError {
|
||||
constructor() {
|
||||
super('Cannot reply to a comment that is not visible', {
|
||||
translation_key: 'COMMENT_PARENT_NOT_VISIBLE',
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
APIError,
|
||||
TalkError,
|
||||
ErrAlreadyExists,
|
||||
ErrAssetCommentingClosed,
|
||||
ErrAssetURLAlreadyExists,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const { forEachField } = require('./utils');
|
||||
const { maskErrors } = require('graphql-errors');
|
||||
const errors = require('../errors');
|
||||
const { TalkError } = require('../errors');
|
||||
const { Error: { ValidationError } } = require('mongoose');
|
||||
|
||||
// If an APIError happens in a mutation, then respond with `{errors: Array}`
|
||||
@@ -11,7 +11,7 @@ const decorateWithMutationErrorHandler = field => {
|
||||
try {
|
||||
return await fieldResolver(obj, args, ctx, info);
|
||||
} catch (err) {
|
||||
if (err instanceof errors.APIError) {
|
||||
if (err instanceof TalkError) {
|
||||
return {
|
||||
errors: [err],
|
||||
};
|
||||
|
||||
@@ -57,7 +57,7 @@ const findOrCreateAssetByURL = async (ctx, url) => {
|
||||
try {
|
||||
new URL(url);
|
||||
} catch (err) {
|
||||
throw ErrInvalidAssetURL;
|
||||
throw new ErrInvalidAssetURL(url);
|
||||
}
|
||||
|
||||
// Try the easy lookup first.
|
||||
@@ -76,7 +76,7 @@ const findOrCreateAssetByURL = async (ctx, url) => {
|
||||
|
||||
// If the domain wasn't whitelisted, then we shouldn't create this asset!
|
||||
if (!whitelisted) {
|
||||
throw ErrInvalidAssetURL;
|
||||
throw new ErrInvalidAssetURL(url);
|
||||
}
|
||||
|
||||
// Construct the update operator that we'll use to create the asset.
|
||||
@@ -135,7 +135,7 @@ const findByUrl = async (
|
||||
try {
|
||||
new URL(asset_url);
|
||||
} catch (err) {
|
||||
throw errors.ErrInvalidAssetURL;
|
||||
throw new errors.ErrInvalidAssetURL(asset_url);
|
||||
}
|
||||
|
||||
return Assets.findByUrl(asset_url);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const errors = require('../../errors');
|
||||
const { ErrNotFound, ErrNotAuthorized } = require('../../errors');
|
||||
const { CREATE_ACTION, DELETE_ACTION } = require('../../perms/constants');
|
||||
const { IGNORE_FLAGS_AGAINST_STAFF } = require('../../config');
|
||||
|
||||
@@ -40,7 +40,7 @@ const createAction = async (
|
||||
// Gets the item referenced by the action.
|
||||
const item = await getActionItem(ctx, { item_id, item_type });
|
||||
if (!item || item === null) {
|
||||
throw errors.ErrNotFound;
|
||||
throw new ErrNotFound();
|
||||
}
|
||||
|
||||
// If we are ignoring flags against staff, ensure that the target isn't a
|
||||
@@ -59,7 +59,7 @@ const createAction = async (
|
||||
// The item is a user, and this is a flag. Check to see if they are staff,
|
||||
// if they are, don't permit the flag.
|
||||
if (item.isStaff()) {
|
||||
throw errors.ErrNotAuthorized;
|
||||
throw new ErrNotAuthorized();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,8 +108,8 @@ const deleteAction = (ctx, { id }) => {
|
||||
module.exports = ctx => {
|
||||
let mutators = {
|
||||
Action: {
|
||||
create: () => Promise.reject(errors.ErrNotAuthorized),
|
||||
delete: () => Promise.reject(errors.ErrNotAuthorized),
|
||||
create: () => Promise.reject(new ErrNotAuthorized()),
|
||||
delete: () => Promise.reject(new ErrNotAuthorized()),
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const errors = require('../../errors');
|
||||
const { ErrNotAuthorized } = require('../../errors');
|
||||
const {
|
||||
UPDATE_ASSET_SETTINGS,
|
||||
UPDATE_ASSET_STATUS,
|
||||
@@ -71,10 +71,10 @@ const scrapeAsset = async (ctx, id) => {
|
||||
module.exports = ctx => {
|
||||
let mutators = {
|
||||
Asset: {
|
||||
updateSettings: () => Promise.reject(errors.ErrNotAuthorized),
|
||||
updateStatus: () => Promise.reject(errors.ErrNotAuthorized),
|
||||
closeNow: () => Promise.reject(errors.ErrNotAuthorized),
|
||||
scrape: () => Promise.reject(errors.ErrNotAuthorized),
|
||||
updateSettings: () => Promise.reject(new ErrNotAuthorized()),
|
||||
updateStatus: () => Promise.reject(new ErrNotAuthorized()),
|
||||
closeNow: () => Promise.reject(new ErrNotAuthorized()),
|
||||
scrape: () => Promise.reject(new ErrNotAuthorized()),
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const errors = require('../../errors');
|
||||
const { ErrNotAuthorized } = require('../../errors');
|
||||
const ActionModel = require('../../models/action');
|
||||
const ActionsService = require('../../services/actions');
|
||||
const TagsService = require('../../services/tags');
|
||||
@@ -312,9 +312,9 @@ const editComment = async (
|
||||
module.exports = ctx => {
|
||||
let mutators = {
|
||||
Comment: {
|
||||
create: () => Promise.reject(errors.ErrNotAuthorized),
|
||||
setStatus: () => Promise.reject(errors.ErrNotAuthorized),
|
||||
edit: () => Promise.reject(errors.ErrNotAuthorized),
|
||||
create: () => Promise.reject(new ErrNotAuthorized()),
|
||||
setStatus: () => Promise.reject(new ErrNotAuthorized()),
|
||||
edit: () => Promise.reject(new ErrNotAuthorized()),
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const errors = require('../../errors');
|
||||
const { ErrNotAuthorized } = require('../../errors');
|
||||
|
||||
const { UPDATE_SETTINGS } = require('../../perms/constants');
|
||||
|
||||
@@ -9,7 +9,7 @@ const update = async (ctx, settings) => SettingsService.update(settings);
|
||||
module.exports = ctx => {
|
||||
let mutators = {
|
||||
Settings: {
|
||||
update: () => Promise.reject(errors.ErrNotAuthorized),
|
||||
update: () => Promise.reject(new ErrNotAuthorized()),
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const TagsService = require('../../services/tags');
|
||||
const errors = require('../../errors');
|
||||
const { ErrNotAuthorized } = require('../../errors');
|
||||
const {
|
||||
ADD_COMMENT_TAG,
|
||||
REMOVE_COMMENT_TAG,
|
||||
@@ -31,8 +31,8 @@ const modify = async (
|
||||
module.exports = context => {
|
||||
let mutators = {
|
||||
Tag: {
|
||||
add: () => Promise.reject(errors.ErrNotAuthorized),
|
||||
remove: () => Promise.reject(errors.ErrNotAuthorized),
|
||||
add: () => Promise.reject(new ErrNotAuthorized()),
|
||||
remove: () => Promise.reject(new ErrNotAuthorized()),
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const errors = require('../../errors');
|
||||
const { ErrNotAuthorized } = require('../../errors');
|
||||
const TokensService = require('../../services/tokens');
|
||||
const { CREATE_TOKEN, REVOKE_TOKEN } = require('../../perms/constants');
|
||||
|
||||
@@ -21,8 +21,8 @@ const revokeToken = async ({ user }, { id }) => {
|
||||
module.exports = context => {
|
||||
let mutators = {
|
||||
Token: {
|
||||
create: () => Promise.reject(errors.ErrNotAuthorized),
|
||||
revoke: () => Promise.reject(errors.ErrNotAuthorized),
|
||||
create: () => Promise.reject(new ErrNotAuthorized()),
|
||||
revoke: () => Promise.reject(new ErrNotAuthorized()),
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
+11
-11
@@ -1,4 +1,4 @@
|
||||
const errors = require('../../errors');
|
||||
const { ErrNotFound, ErrNotAuthorized } = require('../../errors');
|
||||
const UsersService = require('../../services/users');
|
||||
const migrationHelpers = require('../../services/migration/helpers');
|
||||
const {
|
||||
@@ -92,7 +92,7 @@ const delUser = async (ctx, id) => {
|
||||
// Find the user we're removing.
|
||||
const user = await User.findOne({ id });
|
||||
if (!user) {
|
||||
throw errors.ErrNotFound;
|
||||
throw new ErrNotFound();
|
||||
}
|
||||
|
||||
// Get the query transformer we'll use to help batch process the user
|
||||
@@ -156,15 +156,15 @@ const delUser = async (ctx, id) => {
|
||||
module.exports = ctx => {
|
||||
let mutators = {
|
||||
User: {
|
||||
changeUsername: () => Promise.reject(errors.ErrNotAuthorized),
|
||||
ignoreUser: () => Promise.reject(errors.ErrNotAuthorized),
|
||||
setRole: () => Promise.reject(errors.ErrNotAuthorized),
|
||||
setUserBanStatus: () => Promise.reject(errors.ErrNotAuthorized),
|
||||
setUserSuspensionStatus: () => Promise.reject(errors.ErrNotAuthorized),
|
||||
setUserUsernameStatus: () => Promise.reject(errors.ErrNotAuthorized),
|
||||
setUsername: () => Promise.reject(errors.ErrNotAuthorized),
|
||||
stopIgnoringUser: () => Promise.reject(errors.ErrNotAuthorized),
|
||||
del: () => Promise.reject(errors.ErrNotAuthorized),
|
||||
changeUsername: () => Promise.reject(new ErrNotAuthorized()),
|
||||
ignoreUser: () => Promise.reject(new ErrNotAuthorized()),
|
||||
setRole: () => Promise.reject(new ErrNotAuthorized()),
|
||||
setUserBanStatus: () => Promise.reject(new ErrNotAuthorized()),
|
||||
setUserSuspensionStatus: () => Promise.reject(new ErrNotAuthorized()),
|
||||
setUserUsernameStatus: () => Promise.reject(new ErrNotAuthorized()),
|
||||
setUsername: () => Promise.reject(new ErrNotAuthorized()),
|
||||
stopIgnoringUser: () => Promise.reject(new ErrNotAuthorized()),
|
||||
del: () => Promise.reject(new ErrNotAuthorized()),
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
const { property } = require('lodash');
|
||||
const { SEARCH_ACTIONS } = require('../../perms/constants');
|
||||
const { decorateWithTags, decorateWithPermissionCheck } = require('./util');
|
||||
const {
|
||||
SEARCH_ACTIONS,
|
||||
SEARCH_COMMENT_STATUS_HISTORY,
|
||||
VIEW_BODY_HISTORY,
|
||||
} = require('../../perms/constants');
|
||||
const {
|
||||
decorateWithTags,
|
||||
decorateWithPermissionCheck,
|
||||
checkSelfField,
|
||||
} = require('./util');
|
||||
|
||||
const Comment = {
|
||||
hasParent({ parent_id }) {
|
||||
@@ -60,9 +68,19 @@ const Comment = {
|
||||
// Decorate the Comment type resolver with a tags field.
|
||||
decorateWithTags(Comment);
|
||||
|
||||
// Protect direct action access.
|
||||
// Protect direct action and status history access.
|
||||
decorateWithPermissionCheck(Comment, {
|
||||
actions: [SEARCH_ACTIONS],
|
||||
status_history: [SEARCH_COMMENT_STATUS_HISTORY],
|
||||
});
|
||||
|
||||
// Protect privileged fields.
|
||||
decorateWithPermissionCheck(
|
||||
Comment,
|
||||
{
|
||||
body_history: [VIEW_BODY_HISTORY],
|
||||
},
|
||||
checkSelfField('author_id')
|
||||
);
|
||||
|
||||
module.exports = Comment;
|
||||
|
||||
@@ -29,9 +29,9 @@ const User = {
|
||||
return Comments.getByQuery(query);
|
||||
},
|
||||
|
||||
ignoredUsers({ ignoresUsers }, args, { user, loaders: { Users } }) {
|
||||
ignoredUsers({ ignoresUsers }, args, { loaders: { Users } }) {
|
||||
// Return nothing if there is nothing to query for.
|
||||
if (!user.ignoresUsers || user.ignoresUsers.length <= 0) {
|
||||
if (!ignoresUsers || ignoresUsers.length <= 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
|
||||
@@ -505,8 +505,9 @@ type Comment {
|
||||
# The actual comment data.
|
||||
body: String!
|
||||
|
||||
# The body history of the comment.
|
||||
body_history: [CommentBodyHistory!]!
|
||||
# The body history of the comment. Requires the `ADMIN` or `MODERATOR` role or
|
||||
# the author.
|
||||
body_history: [CommentBodyHistory!]
|
||||
|
||||
# The tags on the comment
|
||||
tags: [TagLink!]
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
const path = require('path');
|
||||
const { pluginsPath } = require('./plugins');
|
||||
|
||||
const buildTargets = ['coral-admin', 'coral-docs'];
|
||||
const buildTargets = ['coral-admin'];
|
||||
|
||||
const buildEmbeds = ['stream'];
|
||||
|
||||
|
||||
+2
-2
@@ -4,7 +4,6 @@ const { createLogger } = require('../services/logging');
|
||||
const logger = createLogger('jobs:mailer');
|
||||
const Context = require('../graph/context');
|
||||
const { get } = require('lodash');
|
||||
|
||||
const {
|
||||
SMTP_HOST,
|
||||
SMTP_USERNAME,
|
||||
@@ -12,6 +11,7 @@ const {
|
||||
SMTP_PASSWORD,
|
||||
SMTP_FROM_ADDRESS,
|
||||
} = require('../config');
|
||||
const { ErrMissingEmail } = require('../errors');
|
||||
|
||||
// parseSMTPPort will return the port for SMTP.
|
||||
const parseSMTPPort = () => {
|
||||
@@ -99,7 +99,7 @@ const getEmailAddress = async ({ email, user }) => {
|
||||
|
||||
const email = get(data, 'user.email');
|
||||
if (!email) {
|
||||
throw errors.ErrMissingEmail;
|
||||
throw new ErrMissingEmail();
|
||||
}
|
||||
|
||||
return email;
|
||||
|
||||
@@ -7,7 +7,7 @@ const authorization = (module.exports = {
|
||||
});
|
||||
|
||||
const debug = require('debug')('talk:middleware:authorization');
|
||||
const ErrNotAuthorized = require('../errors').ErrNotAuthorized;
|
||||
const { ErrNotAuthorized } = require('../errors');
|
||||
|
||||
/**
|
||||
* has returns true if the user has at least one of the roles specified,
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
const { logger } = require('../services/logging');
|
||||
const now = require('performance-now');
|
||||
|
||||
const log = (req, res, next) => {
|
||||
const startTime = now();
|
||||
const end = res.end;
|
||||
res.end = function(chunk, encoding) {
|
||||
// Compute the end time.
|
||||
const responseTime = Math.round(now() - startTime);
|
||||
|
||||
// Get some extra goodies from the request.
|
||||
const userAgent = req.get('User-Agent');
|
||||
|
||||
// Reattach the old end, and finish.
|
||||
res.end = end;
|
||||
res.end(chunk, encoding);
|
||||
|
||||
// Log this out.
|
||||
logger.info(
|
||||
{
|
||||
traceID: req.id,
|
||||
url: req.originalUrl || req.url,
|
||||
method: req.method,
|
||||
statusCode: res.statusCode,
|
||||
userAgent,
|
||||
responseTime,
|
||||
},
|
||||
'http request'
|
||||
);
|
||||
};
|
||||
|
||||
next();
|
||||
};
|
||||
|
||||
const error = (err, req, res, next) => {
|
||||
logger.error({ err }, 'http error');
|
||||
next(err);
|
||||
};
|
||||
|
||||
module.exports = { log, error };
|
||||
@@ -0,0 +1,7 @@
|
||||
const uuid = require('uuid/v1');
|
||||
|
||||
// Trace middleware attaches a request id to each incoming request.
|
||||
module.exports = (req, res, next) => {
|
||||
req.id = uuid();
|
||||
next();
|
||||
};
|
||||
+3
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "talk",
|
||||
"version": "4.3.1",
|
||||
"version": "4.3.2",
|
||||
"description": "A better commenting experience from Mozilla, The New York Times, and the Washington Post. https://coralproject.net",
|
||||
"main": "app.js",
|
||||
"private": true,
|
||||
@@ -146,7 +146,6 @@
|
||||
"minimist": "^1.2.0",
|
||||
"moment": "^2.18.1",
|
||||
"mongoose": "^4.12.3",
|
||||
"morgan": "^1.9.0",
|
||||
"ms": "^2.0.0",
|
||||
"murmurhash-js": "^1.0.0",
|
||||
"name-all-modules-plugin": "^1.0.1",
|
||||
@@ -158,6 +157,7 @@
|
||||
"passport": "^0.4.0",
|
||||
"passport-jwt": "^3.0.0",
|
||||
"passport-local": "^1.0.0",
|
||||
"performance-now": "^2.1.0",
|
||||
"pluralize": "^7.0.0",
|
||||
"postcss-loader": "^1.3.3",
|
||||
"postcss-smart-import": "^0.5.1",
|
||||
@@ -215,6 +215,7 @@
|
||||
"babel-plugin-dynamic-import-node": "^1.1.0",
|
||||
"babel-plugin-transform-es2015-modules-commonjs": "^6.26.0",
|
||||
"browserstack-local": "^1.3.0",
|
||||
"bunyan-debug-stream": "^1.0.8",
|
||||
"chai": "^3.5.0",
|
||||
"chai-as-promised": "^6.0.0",
|
||||
"chai-datetime": "^1.5.0",
|
||||
|
||||
@@ -10,4 +10,5 @@ module.exports = {
|
||||
LIST_OWN_TOKENS: 'LIST_OWN_TOKENS',
|
||||
VIEW_USER_ROLE: 'VIEW_USER_ROLE',
|
||||
VIEW_USER_EMAIL: 'VIEW_USER_EMAIL',
|
||||
VIEW_BODY_HISTORY: 'VIEW_BODY_HISTORY',
|
||||
};
|
||||
|
||||
@@ -13,6 +13,7 @@ module.exports = (user, perm) => {
|
||||
case types.VIEW_PROTECTED_SETTINGS:
|
||||
case types.VIEW_USER_ROLE:
|
||||
case types.VIEW_USER_EMAIL:
|
||||
case types.VIEW_BODY_HISTORY:
|
||||
return check(user, ['ADMIN', 'MODERATOR']);
|
||||
case types.LIST_OWN_TOKENS:
|
||||
return check(user, ['ADMIN']);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const { SEARCH_OTHER_USERS } = require('../../../perms/constants');
|
||||
const errors = require('../../../errors');
|
||||
const { ErrNotFound, ErrAlreadyExists } = require('../../../errors');
|
||||
const pluralize = require('pluralize');
|
||||
const sc = require('snake-case');
|
||||
const CommentModel = require('../../../models/comment');
|
||||
@@ -192,7 +192,7 @@ function getReactionConfig(reaction) {
|
||||
) => {
|
||||
const comment = await Comments.get.load(item_id);
|
||||
if (!comment) {
|
||||
throw errors.ErrNotFound;
|
||||
throw new ErrNotFound();
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -211,7 +211,7 @@ function getReactionConfig(reaction) {
|
||||
[reaction]: action,
|
||||
};
|
||||
} catch (err) {
|
||||
if (err instanceof errors.ErrAlreadyExists) {
|
||||
if (err instanceof ErrAlreadyExists) {
|
||||
return err.metadata.existing;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
const { APIError } = require('errors');
|
||||
const { TalkError } = require('errors');
|
||||
|
||||
// ErrSpam is sent during a `CreateComment` mutation where
|
||||
// `input.checkSpam` is set to true and the comment contains
|
||||
// detected spam as determined by the akismet service.
|
||||
const ErrSpam = new APIError('Comment is spam', {
|
||||
status: 400,
|
||||
translation_key: 'COMMENT_IS_SPAM',
|
||||
});
|
||||
class ErrSpam extends TalkError {
|
||||
constructor() {
|
||||
super('Comment is spam', {
|
||||
status: 400,
|
||||
translation_key: 'COMMENT_IS_SPAM',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
ErrSpam,
|
||||
|
||||
@@ -100,7 +100,7 @@ module.exports = {
|
||||
|
||||
if (spam) {
|
||||
if (input.checkSpam) {
|
||||
throw ErrSpam;
|
||||
throw new ErrSpam();
|
||||
}
|
||||
|
||||
// Attach reason information for the flag being added.
|
||||
|
||||
@@ -75,6 +75,7 @@ class SignUp extends React.Component {
|
||||
showErrors={!!emailError}
|
||||
errorMsg={emailError}
|
||||
onChange={this.handleEmailChange}
|
||||
autocomplete="off"
|
||||
/>
|
||||
<TextField
|
||||
id="username"
|
||||
@@ -85,6 +86,8 @@ class SignUp extends React.Component {
|
||||
showErrors={!!usernameError}
|
||||
errorMsg={usernameError}
|
||||
onChange={this.handleUsernameChange}
|
||||
autocomplete="off"
|
||||
autocapitalize="none"
|
||||
/>
|
||||
<TextField
|
||||
id="password"
|
||||
@@ -96,6 +99,7 @@ class SignUp extends React.Component {
|
||||
errorMsg={passwordError}
|
||||
onChange={this.handlePasswordChange}
|
||||
minLength="8"
|
||||
autocomplete="off"
|
||||
/>
|
||||
{passwordError && (
|
||||
<span className={styles.hint}>
|
||||
@@ -113,6 +117,7 @@ class SignUp extends React.Component {
|
||||
errorMsg={passwordRepeatError}
|
||||
onChange={this.handlePasswordRepeatChange}
|
||||
minLength="8"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<Slot
|
||||
fill="talkPluginAuth.formField"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const { get } = require('lodash');
|
||||
const { get, map } = require('lodash');
|
||||
const path = require('path');
|
||||
|
||||
const handle = async (ctx, comment) => {
|
||||
@@ -23,6 +23,9 @@ const handle = async (ctx, comment) => {
|
||||
id
|
||||
user {
|
||||
id
|
||||
ignoredUsers {
|
||||
id
|
||||
}
|
||||
notificationSettings {
|
||||
onReply
|
||||
}
|
||||
@@ -53,13 +56,23 @@ const handle = async (ctx, comment) => {
|
||||
return;
|
||||
}
|
||||
|
||||
// Pull out the author of the new comment.
|
||||
const authorID = get(comment, 'author_id');
|
||||
|
||||
// Check to see if this is yourself replying to yourself, if that's the case
|
||||
// don't send a notification.
|
||||
if (userID === get(comment, 'author_id')) {
|
||||
if (userID === authorID) {
|
||||
ctx.log.info('user id of parent comment is the same as the new comment');
|
||||
return;
|
||||
}
|
||||
|
||||
// Check to see if this user is ignoring the user who replied to their
|
||||
// comment.
|
||||
if (map(get(comment, 'user.ignoredUsers', []), 'id').indexOf(authorID)) {
|
||||
ctx.log.info('parent user has ignored the author of the new comment');
|
||||
return;
|
||||
}
|
||||
|
||||
// The user does have notifications for replied comments enabled, queue the
|
||||
// notification to be sent.
|
||||
return { userID, date: comment.created_at, context: comment.id };
|
||||
|
||||
@@ -29,10 +29,11 @@ async function updateNotificationSettings(ctx, settings) {
|
||||
}
|
||||
|
||||
module.exports = ctx => {
|
||||
const { connectors: { errors: ErrNotAuthorized } } = ctx;
|
||||
|
||||
let mutators = {
|
||||
User: {
|
||||
updateNotificationSettings: () =>
|
||||
Promise.reject(ctx.connectors.errors.ErrNotAuthorized),
|
||||
updateNotificationSettings: () => Promise.reject(new ErrNotAuthorized()),
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
const { APIError } = require('errors');
|
||||
const { TalkError } = require('errors');
|
||||
|
||||
// ErrToxic is sent during a `CreateComment` mutation where
|
||||
// `input.checkToxicity` is set to true and the comment contains
|
||||
// toxic language as determined by the perspective service.
|
||||
const ErrToxic = new APIError('Comment is toxic', {
|
||||
status: 400,
|
||||
translation_key: 'COMMENT_IS_TOXIC',
|
||||
});
|
||||
class ErrToxic extends TalkError {
|
||||
constructor() {
|
||||
super('Comment is toxic', {
|
||||
status: 400,
|
||||
translation_key: 'COMMENT_IS_TOXIC',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
ErrToxic,
|
||||
|
||||
@@ -27,7 +27,7 @@ module.exports = {
|
||||
|
||||
if (isToxic(scores)) {
|
||||
if (input.checkToxicity) {
|
||||
throw ErrToxic;
|
||||
throw new ErrToxic();
|
||||
}
|
||||
|
||||
input.status = 'SYSTEM_WITHHELD';
|
||||
|
||||
+5
-10
@@ -1,7 +1,7 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const UsersService = require('../../../services/users');
|
||||
const errors = require('../../../errors');
|
||||
const { ErrMissingEmail, ErrNotFound } = require('../../../errors');
|
||||
const authorization = require('../../../middleware/authorization');
|
||||
const Limit = require('../../../services/limit');
|
||||
|
||||
@@ -40,17 +40,12 @@ router.post('/resend-verify', async (req, res, next) => {
|
||||
// Clean up and validate the email.
|
||||
email = email.toLowerCase().trim();
|
||||
if (email.length < 5) {
|
||||
return next(errors.ErrMissingEmail);
|
||||
return next(new ErrMissingEmail());
|
||||
}
|
||||
|
||||
// Check if we're past the rate limit, if we are, stop now. Otherwise, record
|
||||
// this as an attempt to send a verification email.
|
||||
try {
|
||||
const tries = await resendRateLimiter.get(email);
|
||||
if (tries > 0) {
|
||||
throw errors.ErrMaxRateLimit;
|
||||
}
|
||||
|
||||
await resendRateLimiter.test(email);
|
||||
} catch (err) {
|
||||
return next(err);
|
||||
@@ -59,7 +54,7 @@ router.post('/resend-verify', async (req, res, next) => {
|
||||
try {
|
||||
const user = await UsersService.findLocalUser(email);
|
||||
if (!user) {
|
||||
throw errors.ErrNotFound;
|
||||
throw new ErrNotFound();
|
||||
}
|
||||
|
||||
await UsersService.sendEmailConfirmation(user, email, redirectUri);
|
||||
@@ -81,13 +76,13 @@ router.post(
|
||||
try {
|
||||
let user = await UsersService.findById(user_id);
|
||||
if (!user) {
|
||||
return next(errors.ErrNotFound);
|
||||
return next(new ErrNotFound());
|
||||
}
|
||||
|
||||
// Find the first local profile.
|
||||
const email = user.firstEmail;
|
||||
if (!email) {
|
||||
return next(errors.ErrMissingEmail);
|
||||
return next(new ErrMissingEmail());
|
||||
}
|
||||
|
||||
// Send the email to the first local profile that was found.
|
||||
|
||||
+8
-15
@@ -1,8 +1,8 @@
|
||||
const SetupService = require('../services/setup');
|
||||
const authentication = require('../middleware/authentication');
|
||||
const logging = require('../middleware/logging');
|
||||
const cookieParser = require('cookie-parser');
|
||||
const enabled = require('debug').enabled;
|
||||
const errors = require('../errors');
|
||||
const { TalkError, ErrNotFound } = require('../errors');
|
||||
const express = require('express');
|
||||
const i18n = require('../middleware/i18n');
|
||||
const path = require('path');
|
||||
@@ -149,19 +149,16 @@ router.use(require('./plugins'));
|
||||
|
||||
// Catch 404 and forward to error handler.
|
||||
router.use((req, res, next) => {
|
||||
next(errors.ErrNotFound);
|
||||
next(new ErrNotFound());
|
||||
});
|
||||
|
||||
// Add logging for errors.
|
||||
router.use(logging.error);
|
||||
|
||||
// General API error handler. Respond with the message and error if we have it
|
||||
// while returning a status code that makes sense.
|
||||
router.use('/api', (err, req, res, next) => {
|
||||
if (err !== errors.ErrNotFound) {
|
||||
if (process.env.NODE_ENV !== 'test' || enabled('talk:errors')) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
if (err instanceof errors.APIError) {
|
||||
if (err instanceof TalkError) {
|
||||
res.status(err.status).json({
|
||||
message: res.locals.t(`error.${err.translation_key}`),
|
||||
error: err,
|
||||
@@ -172,11 +169,7 @@ router.use('/api', (err, req, res, next) => {
|
||||
});
|
||||
|
||||
router.use('/', (err, req, res, next) => {
|
||||
if (err !== errors.ErrNotFound) {
|
||||
console.error(err);
|
||||
}
|
||||
|
||||
if (err instanceof errors.APIError) {
|
||||
if (err instanceof TalkError) {
|
||||
res.status(err.status);
|
||||
res.render('error', {
|
||||
message: res.locals.t(`error.${err.translation_key}`),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const app = require('./app');
|
||||
const errors = require('./errors');
|
||||
const { ErrSettingsInit, ErrInstallLock } = require('./errors');
|
||||
const { createServer } = require('http');
|
||||
const jobs = require('./jobs');
|
||||
const MigrationService = require('./services/migration');
|
||||
@@ -95,20 +95,16 @@ async function serve({
|
||||
await SetupService.isAvailable();
|
||||
|
||||
logger.info('Setup is currently available, migrations not being checked');
|
||||
} catch (e) {
|
||||
} catch (err) {
|
||||
// Check the error.
|
||||
switch (e) {
|
||||
case errors.ErrInstallLock:
|
||||
case errors.ErrSettingsInit:
|
||||
logger.info(
|
||||
'Setup is not currently available, migrations now being checked'
|
||||
);
|
||||
|
||||
// The error was expected, just continue.
|
||||
break;
|
||||
default:
|
||||
// The error was not expected, throw the error!
|
||||
throw e;
|
||||
if (err instanceof ErrInstallLock || err instanceof ErrSettingsInit) {
|
||||
// The error was expected, just continue.
|
||||
logger.info(
|
||||
'Setup is not currently available, migrations now being checked'
|
||||
);
|
||||
} else {
|
||||
// The error was not expected, throw the error!
|
||||
throw err;
|
||||
}
|
||||
|
||||
// Now try and check the migration status.
|
||||
|
||||
+9
-6
@@ -2,9 +2,12 @@ const CommentModel = require('../models/comment');
|
||||
const AssetModel = require('../models/asset');
|
||||
const SettingsService = require('./settings');
|
||||
const DomainList = require('./domain_list');
|
||||
const errors = require('../errors');
|
||||
const merge = require('lodash/merge');
|
||||
const isEmpty = require('lodash/isEmpty');
|
||||
const {
|
||||
ErrAssetURLAlreadyExists,
|
||||
ErrNotFound,
|
||||
ErrInvalidAssetURL,
|
||||
} = require('../errors');
|
||||
const { merge, isEmpty } = require('lodash');
|
||||
const { dotize } = require('./utils');
|
||||
|
||||
module.exports = class AssetsService {
|
||||
@@ -73,7 +76,7 @@ module.exports = class AssetsService {
|
||||
}
|
||||
|
||||
if (!whitelisted) {
|
||||
return Promise.reject(errors.ErrInvalidAssetURL);
|
||||
throw new ErrInvalidAssetURL(url);
|
||||
} else {
|
||||
return AssetModel.findOneAndUpdate({ url }, update, {
|
||||
// Ensure that if it's new, we return the new object created.
|
||||
@@ -211,7 +214,7 @@ module.exports = class AssetsService {
|
||||
// Try to see if an asset already exists with the given url.
|
||||
let asset = await AssetsService.findByUrl(url);
|
||||
if (asset !== null) {
|
||||
throw errors.ErrAssetURLAlreadyExists;
|
||||
throw new ErrAssetURLAlreadyExists();
|
||||
}
|
||||
|
||||
// Seems that there was no other asset with the same url, try and perform
|
||||
@@ -227,7 +230,7 @@ module.exports = class AssetsService {
|
||||
dstAssetID,
|
||||
]);
|
||||
if (!srcAsset || !dstAsset) {
|
||||
throw errors.ErrNotFound;
|
||||
throw new ErrNotFound();
|
||||
}
|
||||
|
||||
// Resolve the merge operation, this invloves moving all resources attached
|
||||
|
||||
+13
-10
@@ -2,10 +2,13 @@ const CommentModel = require('../models/comment');
|
||||
const { dotize } = require('./utils');
|
||||
const debug = require('debug')('talk:services:comments');
|
||||
const SettingsService = require('./settings');
|
||||
|
||||
const cloneDeep = require('lodash/cloneDeep');
|
||||
const errors = require('../errors');
|
||||
const merge = require('lodash/merge');
|
||||
const { merge, cloneDeep } = require('lodash');
|
||||
const {
|
||||
ErrParentDoesNotVisible,
|
||||
ErrNotFound,
|
||||
ErrNotAuthorized,
|
||||
ErrEditWindowHasEnded,
|
||||
} = require('../errors');
|
||||
|
||||
const incrReplyCount = async (comment, value) => {
|
||||
try {
|
||||
@@ -40,7 +43,7 @@ module.exports = {
|
||||
if (parent_id !== null) {
|
||||
const parent = await CommentModel.findOne({ id: parent_id });
|
||||
if (parent === null || !parent.visible) {
|
||||
throw errors.ErrParentDoesNotVisible;
|
||||
throw new ErrParentDoesNotVisible();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,7 +129,7 @@ module.exports = {
|
||||
const comment = await CommentModel.findOne({ id });
|
||||
if (comment == null) {
|
||||
debug('rejecting comment edit because comment was not found');
|
||||
throw errors.ErrNotFound;
|
||||
throw new ErrNotFound();
|
||||
}
|
||||
|
||||
// Check to see if the user was't allowed to edit it.
|
||||
@@ -134,7 +137,7 @@ module.exports = {
|
||||
debug(
|
||||
'rejecting comment edit because author id does not match editing user'
|
||||
);
|
||||
throw errors.ErrNotAuthorized;
|
||||
throw new ErrNotAuthorized();
|
||||
}
|
||||
|
||||
// Check to see if the comment had a status that was editable.
|
||||
@@ -142,13 +145,13 @@ module.exports = {
|
||||
debug(
|
||||
'rejecting comment edit because original comment has a non-editable status'
|
||||
);
|
||||
throw errors.ErrNotAuthorized;
|
||||
throw new ErrNotAuthorized();
|
||||
}
|
||||
|
||||
// Check to see if the edit window expired.
|
||||
if (comment.created_at <= lastEditableCommentCreatedAt) {
|
||||
debug('rejecting comment edit because outside edit time window');
|
||||
throw errors.ErrEditWindowHasEnded;
|
||||
throw new ErrEditWindowHasEnded();
|
||||
}
|
||||
|
||||
throw new Error('comment edit failed for an unexpected reason');
|
||||
@@ -198,7 +201,7 @@ module.exports = {
|
||||
);
|
||||
|
||||
if (originalComment == null) {
|
||||
throw errors.ErrNotFound;
|
||||
throw new ErrNotFound();
|
||||
}
|
||||
|
||||
const editedComment = new CommentModel(originalComment.toObject());
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
const ms = require('ms');
|
||||
const errors = require('../errors');
|
||||
const { ErrMaxRateLimit } = require('../errors');
|
||||
const { createClientFactory } = require('./redis');
|
||||
const client = createClientFactory();
|
||||
|
||||
@@ -60,7 +60,7 @@ class Limit {
|
||||
}
|
||||
|
||||
if (tries > this.max) {
|
||||
throw errors.ErrMaxRateLimit;
|
||||
throw new ErrMaxRateLimit(this.max, tries);
|
||||
}
|
||||
|
||||
return tries;
|
||||
|
||||
+35
-7
@@ -1,18 +1,46 @@
|
||||
const { version } = require('../package.json');
|
||||
const Logger = require('bunyan');
|
||||
const path = require('path');
|
||||
const { createLogger: createBunyanLogger, stdSerializers } = require('bunyan');
|
||||
const { LOGGING_LEVEL, REVISION_HASH } = require('../config');
|
||||
const logger = new Logger({
|
||||
|
||||
// Streams enables the ability for development logs to be readable to a human,
|
||||
// but will send JSON logs in production that's parsable by a system like ELK.
|
||||
const streams = (() => {
|
||||
// In development, use the debug stream printer.
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
const debug = require('bunyan-debug-stream');
|
||||
return [
|
||||
{
|
||||
level: 'debug',
|
||||
type: 'raw',
|
||||
stream: debug({
|
||||
basepath: path.resolve(__dirname, '..'),
|
||||
forceColor: true,
|
||||
}),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
// In production, emit JSON.
|
||||
return [{ stream: process.stdout, level: 'info' }];
|
||||
})();
|
||||
|
||||
// logger is the base logger used by all logging systems in Talk.
|
||||
const logger = createBunyanLogger({
|
||||
src: true,
|
||||
name: 'talk',
|
||||
version,
|
||||
revision: REVISION_HASH,
|
||||
level: LOGGING_LEVEL,
|
||||
serializers: Logger.stdSerializers,
|
||||
streams,
|
||||
serializers: stdSerializers,
|
||||
});
|
||||
|
||||
// Create the logging instance that all logger's are branched from.
|
||||
function createLogger(name, traceID) {
|
||||
return logger.child({ origin: name, traceID });
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @param {String} origin the origin name used by the logger
|
||||
* @param {String} traceID the id of the request being made
|
||||
*/
|
||||
const createLogger = (origin, traceID) => logger.child({ origin, traceID });
|
||||
|
||||
module.exports = { logger, createLogger };
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const errors = require('../../errors');
|
||||
const { ErrNotFound } = require('../../errors');
|
||||
const get = require('lodash/get');
|
||||
|
||||
// Load in the phases to use.
|
||||
@@ -92,14 +92,14 @@ const fetchOptions = async (ctx, comment) => {
|
||||
const assetID = get(comment, 'asset_id', null);
|
||||
if (assetID === null) {
|
||||
// And leave now if this asset wasn't found.
|
||||
throw errors.ErrNotFound;
|
||||
throw new ErrNotFound();
|
||||
}
|
||||
|
||||
// Load the asset.
|
||||
const asset = await Assets.getByID.load(assetID);
|
||||
if (!asset) {
|
||||
// And leave now if this asset wasn't found.
|
||||
throw errors.ErrNotFound;
|
||||
throw new ErrNotFound();
|
||||
}
|
||||
|
||||
// Combine the asset and the settings to get the asset settings.
|
||||
|
||||
@@ -8,7 +8,7 @@ module.exports = (
|
||||
) => {
|
||||
// Check to see if the body is too short, if it is, then complain about it!
|
||||
if (comment.body.length < 2) {
|
||||
throw ErrCommentTooShort;
|
||||
throw new ErrCommentTooShort(comment.body.length);
|
||||
}
|
||||
|
||||
// Reject if the comment is too long
|
||||
|
||||
+33
-38
@@ -1,48 +1,40 @@
|
||||
const { MONGO_URL, WEBPACK, CREATE_MONGO_INDEXES } = require('../config');
|
||||
|
||||
const {
|
||||
MONGO_URL,
|
||||
WEBPACK,
|
||||
CREATE_MONGO_INDEXES,
|
||||
LOGGING_LEVEL,
|
||||
} = require('../config');
|
||||
const { logger } = require('./logging');
|
||||
const mongoose = require('mongoose');
|
||||
const debug = require('debug')('talk:db');
|
||||
const enabled = require('debug').enabled;
|
||||
const queryDebugger = require('debug')('talk:db:query');
|
||||
|
||||
// Loading the formatter from Mongoose:
|
||||
//
|
||||
// https://github.com/Automattic/mongoose/blob/1a93d1f4d12e441e17ddf451e96fbc5f6e8f54b8/lib/drivers/node-mongodb-native/collection.js#L182
|
||||
//
|
||||
// so we can wrap parameters.
|
||||
const formatter = require('mongoose').Collection.prototype.$format;
|
||||
|
||||
// Provide a newly wrapped debugQuery function which wraps the `debug` package.
|
||||
function debugQuery(name, i, ...args) {
|
||||
let functionCall = ['db', name, i].join('.');
|
||||
let _args = [];
|
||||
for (let j = args.length - 1; j >= 0; --j) {
|
||||
if (formatter(args[j]) || _args.length) {
|
||||
_args.unshift(formatter(args[j]));
|
||||
}
|
||||
}
|
||||
|
||||
let params = `(${_args.join(', ')})`;
|
||||
|
||||
queryDebugger(functionCall + params);
|
||||
function debugQuery(name, operation, ...args) {
|
||||
logger.debug(
|
||||
{
|
||||
query: `db.${name}.${operation}(${args
|
||||
.map(arg => JSON.stringify(arg))
|
||||
.join(', ')})`,
|
||||
},
|
||||
'mongodb query'
|
||||
);
|
||||
}
|
||||
|
||||
// Use native promises
|
||||
mongoose.Promise = global.Promise;
|
||||
|
||||
// Check if debugging is enabled on the talk:db prefix.
|
||||
if (enabled('talk:db:query')) {
|
||||
// Check if verbose logging is enabled.
|
||||
if (['debug', 'trace'].includes(LOGGING_LEVEL)) {
|
||||
// Enable the mongoose debugger, here we wrap the similar print function
|
||||
// provided by setting the debug parameter.
|
||||
mongoose.set('debug', debugQuery);
|
||||
}
|
||||
|
||||
if (WEBPACK) {
|
||||
debug('Not connecting to mongodb during webpack build');
|
||||
logger.debug('Not connecting to mongodb during webpack build');
|
||||
|
||||
// @wyattjoh: We didn't call connect, but because we include mongoose, it will hold the socket ready,
|
||||
// preventing node from exiting. Calling disconnect here just ensures that the application
|
||||
// can quit correctly.
|
||||
// @wyattjoh: We didn't call connect, but because we include mongoose, it will
|
||||
// hold the socket ready, preventing node from exiting. Calling disconnect
|
||||
// here just ensures that the application can quit correctly.
|
||||
mongoose.disconnect();
|
||||
} else {
|
||||
// Connect to the Mongo instance.
|
||||
@@ -54,7 +46,7 @@ if (WEBPACK) {
|
||||
},
|
||||
})
|
||||
.then(() => {
|
||||
debug('connection established');
|
||||
logger.debug('mongodb connection established');
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
@@ -66,10 +58,13 @@ module.exports = mongoose;
|
||||
|
||||
// Here we include all the models that mongoose is used for, this ensures that
|
||||
// when we import mongoose that we also start up all the indexing operations
|
||||
// here.
|
||||
require('../models/action');
|
||||
require('../models/asset');
|
||||
require('../models/comment');
|
||||
require('../models/setting');
|
||||
require('../models/user');
|
||||
require('./migration');
|
||||
// here. No point also in importing this if we're not actually doing any
|
||||
// indexing now.
|
||||
if (CREATE_MONGO_INDEXES) {
|
||||
require('../models/action');
|
||||
require('../models/asset');
|
||||
require('../models/comment');
|
||||
require('../models/setting');
|
||||
require('../models/user');
|
||||
require('./migration');
|
||||
}
|
||||
|
||||
+13
-8
@@ -6,7 +6,12 @@ const TokensService = require('./tokens');
|
||||
const fetch = require('node-fetch');
|
||||
const FormData = require('form-data');
|
||||
const LocalStrategy = require('passport-local').Strategy;
|
||||
const errors = require('../errors');
|
||||
const {
|
||||
ErrLoginAttemptMaximumExceeded,
|
||||
ErrNotAuthorized,
|
||||
ErrAuthentication,
|
||||
ErrNotVerified,
|
||||
} = require('../errors');
|
||||
const uuid = require('uuid');
|
||||
const debug = require('debug')('talk:services:passport');
|
||||
const bowser = require('bowser');
|
||||
@@ -75,7 +80,7 @@ const HandleGenerateCredentials = (req, res, next) => (err, user) => {
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
return next(errors.ErrNotAuthorized);
|
||||
return next(new ErrNotAuthorized());
|
||||
}
|
||||
|
||||
// Generate the token to re-issue to the frontend.
|
||||
@@ -117,7 +122,7 @@ const HandleAuthPopupCallback = (req, res, next) => (err, user) => {
|
||||
|
||||
if (!user) {
|
||||
return res.render('auth-callback', {
|
||||
auth: { err: errors.ErrNotAuthorized, data: null },
|
||||
auth: { err: new ErrNotAuthorized(), data: null },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -143,7 +148,7 @@ async function ValidateUserLogin(loginProfile, user, done) {
|
||||
}
|
||||
|
||||
if (user.disabled) {
|
||||
return done(new errors.ErrAuthentication('Account disabled'));
|
||||
return done(new ErrAuthentication('Account disabled'));
|
||||
}
|
||||
|
||||
// If the user isn't a local user (i.e., a social user).
|
||||
@@ -169,7 +174,7 @@ async function ValidateUserLogin(loginProfile, user, done) {
|
||||
// If the profile doesn't have a metadata field, or it does not have a
|
||||
// confirmed_at field, or that field is null, then send them back.
|
||||
if (_.get(profile, 'metadata.confirmed_at', null) === null) {
|
||||
return done(errors.ErrNotVerified);
|
||||
return done(new ErrNotVerified());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -209,7 +214,7 @@ const checkGeneralTokenBlacklist = jwt =>
|
||||
.get(`jtir[${jwt.jti}]`)
|
||||
.then(expiry => {
|
||||
if (expiry != null) {
|
||||
throw new errors.ErrAuthentication('token was revoked');
|
||||
throw new ErrAuthentication('token was revoked');
|
||||
}
|
||||
});
|
||||
|
||||
@@ -392,7 +397,7 @@ const HandleFailedAttempt = async (email, userNeedsRecaptcha) => {
|
||||
await UsersService.recordLoginAttempt(email);
|
||||
} catch (err) {
|
||||
if (
|
||||
err === errors.ErrLoginAttemptMaximumExceeded &&
|
||||
err instanceof ErrLoginAttemptMaximumExceeded &&
|
||||
!userNeedsRecaptcha &&
|
||||
RECAPTCHA_ENABLED
|
||||
) {
|
||||
@@ -448,7 +453,7 @@ passport.use(
|
||||
try {
|
||||
await UsersService.checkLoginAttempts(email);
|
||||
} catch (err) {
|
||||
if (err === errors.ErrLoginAttemptMaximumExceeded) {
|
||||
if (err instanceof ErrLoginAttemptMaximumExceeded) {
|
||||
// This says, we didn't have a recaptcha, yet we needed one.. Reject
|
||||
// here.
|
||||
|
||||
|
||||
+13
-12
@@ -1,7 +1,5 @@
|
||||
const Redis = require('ioredis');
|
||||
const merge = require('lodash/merge');
|
||||
const debug = require('debug')('talk:services:redis');
|
||||
const enabled = require('debug').enabled('talk:services:redis');
|
||||
const {
|
||||
REDIS_URL,
|
||||
REDIS_RECONNECTION_BACKOFF_FACTOR,
|
||||
@@ -9,29 +7,32 @@ const {
|
||||
REDIS_CLIENT_CONFIG,
|
||||
REDIS_CLUSTER_MODE,
|
||||
REDIS_CLUSTER_CONFIGURATION,
|
||||
LOGGING_LEVEL,
|
||||
} = require('../config');
|
||||
const { createLogger } = require('./logging');
|
||||
const logger = createLogger('redis');
|
||||
|
||||
const attachMonitors = client => {
|
||||
debug('client created');
|
||||
logger.debug('client created');
|
||||
|
||||
// Debug events.
|
||||
if (enabled) {
|
||||
client.on('connect', () => debug('client connected'));
|
||||
client.on('ready', () => debug('client ready'));
|
||||
client.on('close', () => debug('client closed the connection'));
|
||||
if (['debug', 'trace'].includes(LOGGING_LEVEL)) {
|
||||
client.on('connect', () => logger.info('client connected'));
|
||||
client.on('ready', () => logger.debug('client ready'));
|
||||
client.on('close', () => logger.debug('client closed the connection'));
|
||||
client.on('reconnecting', () =>
|
||||
debug('client connection lost, attempting to reconnect')
|
||||
logger.debug('client connection lost, attempting to reconnect')
|
||||
);
|
||||
client.on('end', () => debug('client ended'));
|
||||
client.on('end', () => logger.debug('client ended'));
|
||||
}
|
||||
|
||||
// Error events.
|
||||
client.on('error', err => {
|
||||
if (err) {
|
||||
console.error('Error connecting to redis:', err);
|
||||
logger.error({ err }, 'cannot connect to redis');
|
||||
}
|
||||
});
|
||||
client.on('node error', err => debug('node error', err));
|
||||
client.on('node error', err => logger.error({ err }, 'node error'));
|
||||
};
|
||||
|
||||
function retryStrategy(times) {
|
||||
@@ -40,7 +41,7 @@ function retryStrategy(times) {
|
||||
REDIS_RECONNECTION_BACKOFF_MINIMUM_TIME
|
||||
);
|
||||
|
||||
debug(`retry strategy: try to reconnect ${delay} ms from now`);
|
||||
logger.debug(`retry strategy: try to reconnect ${delay} ms from now`);
|
||||
|
||||
return delay;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const SettingModel = require('../models/setting');
|
||||
const cache = require('./cache');
|
||||
const errors = require('../errors');
|
||||
const { ErrSettingsNotInit } = require('../errors');
|
||||
const { dotize } = require('./utils');
|
||||
const { SETTINGS_CACHE_TIME } = require('../config');
|
||||
|
||||
@@ -17,7 +17,7 @@ const retrieve = async fields => {
|
||||
settings = await SettingModel.findOne(selector);
|
||||
}
|
||||
if (!settings) {
|
||||
throw errors.ErrSettingsNotInit;
|
||||
throw new ErrSettingsNotInit();
|
||||
}
|
||||
|
||||
return settings;
|
||||
|
||||
+17
-12
@@ -2,7 +2,12 @@ const UsersService = require('./users');
|
||||
const SettingsService = require('./settings');
|
||||
const MigrationService = require('./migration');
|
||||
const SettingsModel = require('../models/setting');
|
||||
const errors = require('../errors');
|
||||
const {
|
||||
ErrMissingEmail,
|
||||
ErrInstallLock,
|
||||
ErrSettingsInit,
|
||||
ErrSettingsNotInit,
|
||||
} = require('../errors');
|
||||
const { INSTALL_LOCK } = require('../config');
|
||||
|
||||
/**
|
||||
@@ -16,25 +21,25 @@ module.exports = class SetupService {
|
||||
static async isAvailable() {
|
||||
// Check if we have an install lock present.
|
||||
if (INSTALL_LOCK) {
|
||||
throw errors.ErrInstallLock;
|
||||
throw new ErrInstallLock();
|
||||
}
|
||||
|
||||
try {
|
||||
// Get the current settings, we are expecing an error here.
|
||||
// Get the current settings, we are expecting an error here.
|
||||
await SettingsService.retrieve();
|
||||
|
||||
// We should NOT have gotten a settings object, this means that the
|
||||
// application is already setup. Error out here.
|
||||
throw errors.ErrSettingsInit;
|
||||
} catch (e) {
|
||||
// If the error is `not init`, then we're good, otherwise, it's something
|
||||
// else.
|
||||
if (e !== errors.ErrSettingsNotInit) {
|
||||
throw e;
|
||||
throw new ErrSettingsInit();
|
||||
} catch (err) {
|
||||
// Allow the request to keep going here.
|
||||
if (err instanceof ErrSettingsNotInit) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Allow the request to keep going here.
|
||||
return;
|
||||
// If the error is `not init`, then we're good, otherwise, it's something
|
||||
// else.
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,7 +49,7 @@ module.exports = class SetupService {
|
||||
static validate({ settings, user: { email, username, password } }) {
|
||||
// Verify the email address of the user.
|
||||
if (!email) {
|
||||
return Promise.reject(errors.ErrMissingEmail);
|
||||
throw new ErrMissingEmail();
|
||||
}
|
||||
|
||||
// Create a settings model to use for validation.
|
||||
|
||||
+3
-5
@@ -1,12 +1,10 @@
|
||||
const CommentModel = require('../models/comment');
|
||||
const AssetModel = require('../models/asset');
|
||||
const UserModel = require('../models/user');
|
||||
|
||||
const AssetsService = require('./assets');
|
||||
const SettingsService = require('./settings');
|
||||
const { ADD_COMMENT_TAG } = require('../perms/constants');
|
||||
|
||||
const errors = require('../errors');
|
||||
const { ErrNotAuthorized } = require('../errors');
|
||||
|
||||
const updateModel = async (item_type, query, update) => {
|
||||
// Get the model to update with.
|
||||
@@ -120,13 +118,13 @@ class TagsService {
|
||||
return { tagLink, ownership: true };
|
||||
}
|
||||
|
||||
throw errors.ErrNotAuthorized;
|
||||
throw new ErrNotAuthorized();
|
||||
}
|
||||
|
||||
// Only admin/moderators can modify unique tags, these are tags that are not
|
||||
// in the global list.
|
||||
if (!user.can(ADD_COMMENT_TAG)) {
|
||||
throw errors.ErrNotAuthorized;
|
||||
throw new ErrNotAuthorized();
|
||||
}
|
||||
|
||||
// Generate the tag in the event now that we have to create the tag for this
|
||||
|
||||
+41
-28
@@ -1,6 +1,21 @@
|
||||
const uuid = require('uuid');
|
||||
const bcrypt = require('bcryptjs');
|
||||
const errors = require('../errors');
|
||||
const {
|
||||
ErrMaxRateLimit,
|
||||
ErrLoginAttemptMaximumExceeded,
|
||||
ErrNotFound,
|
||||
ErrPermissionUpdateUsername,
|
||||
ErrSameUsernameProvided,
|
||||
ErrUsernameTaken,
|
||||
ErrMissingUsername,
|
||||
ErrSpecialChars,
|
||||
ErrMissingPassword,
|
||||
ErrPasswordTooShort,
|
||||
ErrMissingEmail,
|
||||
ErrEmailTaken,
|
||||
ErrEmailAlreadyVerified,
|
||||
ErrCannotIgnoreStaff,
|
||||
} = require('../errors');
|
||||
const { difference, sample, some, merge, random } = require('lodash');
|
||||
const { ROOT_URL } = require('../config');
|
||||
const { jwt: JWT_SECRET } = require('../secrets');
|
||||
@@ -59,8 +74,8 @@ class UsersService {
|
||||
try {
|
||||
await loginRateLimiter.test(email.toLowerCase().trim());
|
||||
} catch (err) {
|
||||
if (err === errors.ErrMaxRateLimit) {
|
||||
throw errors.ErrLoginAttemptMaximumExceeded;
|
||||
if (err instanceof ErrMaxRateLimit) {
|
||||
throw new ErrLoginAttemptMaximumExceeded();
|
||||
}
|
||||
|
||||
throw err;
|
||||
@@ -91,7 +106,7 @@ class UsersService {
|
||||
if (user === null) {
|
||||
user = await UserModel.findOne({ id });
|
||||
if (user === null) {
|
||||
throw errors.ErrNotFound;
|
||||
throw new ErrNotFound();
|
||||
}
|
||||
|
||||
// Date comparisons are difficult when using MongoDB. Javascript will
|
||||
@@ -150,10 +165,10 @@ class UsersService {
|
||||
runValidators: true,
|
||||
}
|
||||
);
|
||||
if (user === null) {
|
||||
if (!user) {
|
||||
user = await UserModel.findOne({ id });
|
||||
if (user === null) {
|
||||
throw errors.ErrNotFound;
|
||||
if (!user) {
|
||||
throw new ErrNotFound();
|
||||
}
|
||||
|
||||
if (user.status.banned.status === status) {
|
||||
@@ -204,7 +219,7 @@ class UsersService {
|
||||
if (user === null) {
|
||||
user = await UserModel.findOne({ id });
|
||||
if (user === null) {
|
||||
throw errors.ErrNotFound;
|
||||
throw new ErrNotFound();
|
||||
}
|
||||
|
||||
if (user.status.username.status === status) {
|
||||
@@ -259,15 +274,15 @@ class UsersService {
|
||||
if (!user) {
|
||||
user = await UsersService.findById(id);
|
||||
if (user === null) {
|
||||
throw errors.ErrNotFound;
|
||||
throw new ErrNotFound();
|
||||
}
|
||||
|
||||
if (user.status.username.status !== fromStatus) {
|
||||
throw errors.ErrPermissionUpdateUsername;
|
||||
throw new ErrPermissionUpdateUsername();
|
||||
}
|
||||
|
||||
if (!resetAllowed && user.username === username) {
|
||||
throw errors.ErrSameUsernameProvided;
|
||||
throw new ErrSameUsernameProvided();
|
||||
}
|
||||
|
||||
throw new Error('edit username failed for an unexpected reason');
|
||||
@@ -276,7 +291,7 @@ class UsersService {
|
||||
return user;
|
||||
} catch (err) {
|
||||
if (err.code === 11000) {
|
||||
throw errors.ErrUsernameTaken;
|
||||
throw new ErrUsernameTaken();
|
||||
}
|
||||
|
||||
throw err;
|
||||
@@ -317,7 +332,7 @@ class UsersService {
|
||||
}
|
||||
|
||||
if (attempts >= RECAPTCHA_INCORRECT_TRIGGER) {
|
||||
throw errors.ErrLoginAttemptMaximumExceeded;
|
||||
throw new ErrLoginAttemptMaximumExceeded();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -515,11 +530,11 @@ class UsersService {
|
||||
const onlyLettersNumbersUnderscore = /^[A-Za-z0-9_]+$/;
|
||||
|
||||
if (!username) {
|
||||
throw errors.ErrMissingUsername;
|
||||
throw new ErrMissingUsername();
|
||||
}
|
||||
|
||||
if (!onlyLettersNumbersUnderscore.test(username)) {
|
||||
throw errors.ErrSpecialChars;
|
||||
throw new ErrSpecialChars();
|
||||
}
|
||||
|
||||
if (checkAgainstWordlist) {
|
||||
@@ -539,11 +554,11 @@ class UsersService {
|
||||
*/
|
||||
static isValidPassword(password) {
|
||||
if (!password) {
|
||||
throw errors.ErrMissingPassword;
|
||||
throw new ErrMissingPassword();
|
||||
}
|
||||
|
||||
if (password.length < 8) {
|
||||
throw errors.ErrPasswordTooShort;
|
||||
throw new ErrPasswordTooShort();
|
||||
}
|
||||
|
||||
return password;
|
||||
@@ -558,7 +573,7 @@ class UsersService {
|
||||
*/
|
||||
static async createLocalUser(ctx, email, password, username) {
|
||||
if (!email) {
|
||||
throw errors.ErrMissingEmail;
|
||||
throw new ErrMissingEmail();
|
||||
}
|
||||
|
||||
email = email.toLowerCase().trim();
|
||||
@@ -596,9 +611,9 @@ class UsersService {
|
||||
} catch (err) {
|
||||
if (err.code === 11000) {
|
||||
if (err.message.match('Username')) {
|
||||
throw errors.ErrUsernameTaken;
|
||||
throw new ErrUsernameTaken();
|
||||
}
|
||||
throw errors.ErrEmailTaken;
|
||||
throw new ErrEmailTaken();
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
@@ -678,9 +693,7 @@ class UsersService {
|
||||
*/
|
||||
static async createPasswordResetToken(email, loc) {
|
||||
if (!email || typeof email !== 'string') {
|
||||
throw new Error(
|
||||
'email is required when creating a JWT for resetting passord'
|
||||
);
|
||||
throw new ErrMissingEmail();
|
||||
}
|
||||
|
||||
email = email.toLowerCase();
|
||||
@@ -837,7 +850,7 @@ class UsersService {
|
||||
|
||||
// Ensure that the user email hasn't already been verified.
|
||||
if (profile && profile.metadata && profile.metadata.confirmed_at) {
|
||||
throw errors.ErrEmailAlreadyVerified;
|
||||
throw new ErrEmailAlreadyVerified();
|
||||
}
|
||||
|
||||
return JWT_SECRET.sign(
|
||||
@@ -875,16 +888,16 @@ class UsersService {
|
||||
},
|
||||
});
|
||||
if (!user) {
|
||||
throw errors.ErrNotFound;
|
||||
throw new ErrNotFound();
|
||||
}
|
||||
|
||||
const profile = user.profiles.find(({ id }) => id === decoded.email);
|
||||
if (!profile) {
|
||||
throw errors.ErrNotFound;
|
||||
throw new ErrNotFound();
|
||||
}
|
||||
|
||||
if (profile.metadata && profile.metadata.confirmed_at !== null) {
|
||||
throw errors.ErrEmailAlreadyVerified;
|
||||
throw new ErrEmailAlreadyVerified();
|
||||
}
|
||||
|
||||
return decoded;
|
||||
@@ -943,7 +956,7 @@ class UsersService {
|
||||
|
||||
const users = await UsersService.findByIdArray(usersToIgnore);
|
||||
if (some(users, user => user.isStaff())) {
|
||||
throw errors.ErrCannotIgnoreStaff;
|
||||
throw new ErrCannotIgnoreStaff();
|
||||
}
|
||||
|
||||
return UserModel.update(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
const debug = require('debug')('talk:services:wordlist');
|
||||
const _ = require('lodash');
|
||||
const SettingsService = require('./settings');
|
||||
const Errors = require('../errors');
|
||||
const { ErrContainsProfanity } = require('../errors');
|
||||
const memoize = require('lodash/memoize');
|
||||
const { escapeRegExp } = require('./regex');
|
||||
|
||||
@@ -96,7 +96,7 @@ class Wordlist {
|
||||
`the field "${fieldName}" contained a phrase "${phrase}" which contained a banned word/phrase`
|
||||
);
|
||||
|
||||
errors.banned = Errors.ErrContainsProfanity;
|
||||
errors.banned = new ErrContainsProfanity(phrase);
|
||||
|
||||
// Stop looping through the fields now, we discovered the worst possible
|
||||
// situation (a banned word).
|
||||
@@ -109,7 +109,7 @@ class Wordlist {
|
||||
`the field "${fieldName}" contained a phrase "${phrase}" which contained a suspected word/phrase`
|
||||
);
|
||||
|
||||
errors.suspect = Errors.ErrContainsProfanity;
|
||||
errors.suspect = new ErrContainsProfanity(phrase);
|
||||
|
||||
// Continue looping through the fields now, we discovered a possible bad
|
||||
// word (suspect).
|
||||
@@ -167,7 +167,7 @@ class Wordlist {
|
||||
|
||||
return wl.load().then(() => {
|
||||
if (wl.regexp.banned.test(username)) {
|
||||
return Errors.ErrContainsProfanity;
|
||||
throw new ErrContainsProfanity(username);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const User = require('../../../models/user');
|
||||
const Context = require('../../../graph/context');
|
||||
const errors = require('../../../errors');
|
||||
const { ErrNotAuthorized } = require('../../../errors');
|
||||
const SettingsService = require('../../../services/settings');
|
||||
|
||||
const { expect } = require('chai');
|
||||
@@ -54,7 +54,7 @@ describe('graph.Context', () => {
|
||||
throw new Error('should not reach this point');
|
||||
})
|
||||
.catch(err => {
|
||||
expect(err).to.be.equal(errors.ErrNotAuthorized);
|
||||
expect(err).to.be.an.instanceof(ErrNotAuthorized);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const Errors = require('../../../errors');
|
||||
const { ErrContainsProfanity } = require('../../../errors');
|
||||
const Wordlist = require('../../../services/wordlist');
|
||||
const SettingsService = require('../../../services/settings');
|
||||
|
||||
@@ -103,7 +103,8 @@ describe('services.Wordlist', () => {
|
||||
'content'
|
||||
);
|
||||
|
||||
expect(errors).to.have.property('banned', Errors.ErrContainsProfanity);
|
||||
expect(errors).to.have.property('banned');
|
||||
expect(errors.banned).to.be.an.instanceof(ErrContainsProfanity);
|
||||
});
|
||||
|
||||
it('does not match on bodies not containing bad words', () => {
|
||||
|
||||
@@ -1684,6 +1684,13 @@ builtin-status-codes@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8"
|
||||
|
||||
bunyan-debug-stream@^1.0.8:
|
||||
version "1.0.8"
|
||||
resolved "https://registry.yarnpkg.com/bunyan-debug-stream/-/bunyan-debug-stream-1.0.8.tgz#df612852d5d0b6d6df3f30214d8a7e4ee925106d"
|
||||
dependencies:
|
||||
colors "^1.0.3"
|
||||
exception-formatter "^1.0.4"
|
||||
|
||||
bunyan@^1.8.12:
|
||||
version "1.8.12"
|
||||
resolved "https://registry.yarnpkg.com/bunyan/-/bunyan-1.8.12.tgz#f150f0f6748abdd72aeae84f04403be2ef113797"
|
||||
@@ -2174,6 +2181,10 @@ colors@1.0.3, colors@1.0.x:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/colors/-/colors-1.0.3.tgz#0433f44d809680fdeb60ed260f1b0c262e82a40b"
|
||||
|
||||
colors@^1.0.3:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/colors/-/colors-1.2.1.tgz#f4a3d302976aaf042356ba1ade3b1a2c62d9d794"
|
||||
|
||||
colors@^1.1.2, colors@~1.1.2:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/colors/-/colors-1.1.2.tgz#168a4701756b6a7f51a12ce0c97bfa28c084ed63"
|
||||
@@ -3577,6 +3588,12 @@ evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3:
|
||||
md5.js "^1.3.4"
|
||||
safe-buffer "^5.1.1"
|
||||
|
||||
exception-formatter@^1.0.4:
|
||||
version "1.0.5"
|
||||
resolved "https://registry.yarnpkg.com/exception-formatter/-/exception-formatter-1.0.5.tgz#bda957319789cbabdf36848fb5288c59634b73a5"
|
||||
dependencies:
|
||||
colors "^1.0.3"
|
||||
|
||||
exec-sh@^0.2.0:
|
||||
version "0.2.1"
|
||||
resolved "https://registry.yarnpkg.com/exec-sh/-/exec-sh-0.2.1.tgz#163b98a6e89e6b65b47c2a28d215bc1f63989c38"
|
||||
@@ -7480,7 +7497,7 @@ moo-server@*, moo-server@1.3.x:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/moo-server/-/moo-server-1.3.0.tgz#5dc79569565a10d6efed5439491e69d2392e58f1"
|
||||
|
||||
morgan@^1.6.1, morgan@^1.9.0:
|
||||
morgan@^1.6.1:
|
||||
version "1.9.0"
|
||||
resolved "https://registry.yarnpkg.com/morgan/-/morgan-1.9.0.tgz#d01fa6c65859b76fcf31b3cb53a3821a311d8051"
|
||||
dependencies:
|
||||
|
||||
Reference in New Issue
Block a user