Merge branch 'master' into bugfix-flagged-actions

This commit is contained in:
Gabriela Rodríguez Berón
2017-01-13 15:05:54 -08:00
committed by GitHub
12 changed files with 134 additions and 64 deletions
+14 -15
View File
@@ -17,30 +17,29 @@ To launch a Talk server of your own from your browser without any need to muck a
### Configuration
The Talk application requires specific configuration options to be available
inside the environment in order to run, those variables are listed here:
The Talk application looks for the following configuration values either as environment variables:
- `TALK_MONGO_URL` (*required*) - the database connection string for the MongoDB database.
- `TALK_REDIS_URL` (*required*) - the database connection string for the Redis database.
- `TALK_SESSION_SECRET` (*required*) - a random string which will be used to
secure cookies.
- `TALK_FACEBOOK_APP_ID` (*required*) - the Facebook app id for your Facebook
Login enabled app.
- `TALK_FACEBOOK_APP_SECRET` (*required*) - the Facebook app secret for your
Facebook Login enabled app.
- `TALK_ROOT_URL` (*required*) - root url of the installed application externally
available in the format: `<scheme>://<host>` without the path.
- `TALK_SMTP_EMAIL` (*required*) - the address to send emails from using the
- `TALK_FACEBOOK_APP_ID` (*required for login via fb*) - the Facebook app id for your Facebook
Login enabled app.
- `TALK_FACEBOOK_APP_SECRET` (*required for login via fb*) - the Facebook app secret for your
Facebook Login enabled app.
- `TALK_SMTP_EMAIL` (*required for email*) - the address to send emails from using the
SMTP provider.
- `TALK_SMTP_USERNAME` (*required*) - username of the SMTP provider you are using.
- `TALK_SMTP_PASSWORD` (*required*) - password for the SMTP provider you are using.
- `TALK_SMTP_HOST` (*required*) - SMTP host url with format `smtp.domain.com`.
- `TALK_SMTP_PORT` (*required*) - SMTP port.
- `TALK_SMTP_USERNAME` (*required for email*) - username of the SMTP provider you are using.
- `TALK_SMTP_PASSWORD` (*required for email*) - password for the SMTP provider you are using.
- `TALK_SMTP_HOST` (*required for email*) - SMTP host url with format `smtp.domain.com`.
- `TALK_SMTP_PORT` (*required for email*) - SMTP port.
### Install from Source
If you want to run Talk in development mode from source (without docker) you can read the [INSTALL file](INSTALL.md).
Refer to the wiki page on [Configuration Loading](https://github.com/coralproject/talk/wiki/Configuration-Loading) for
alternative methods of loading configuration during development.
### License
+3 -3
View File
@@ -55,9 +55,9 @@ export const fetchFlaggedQueue = () => {
dispatch({type: commentTypes.COMMENTS_MODERATION_QUEUE_FETCH_REQUEST});
return coralApi('/queue/comments/flagged')
.then(comments => {
comments.forEach(comment => comment.flagged = true);
return comments;
.then(results => {
results.comments.forEach(comment => comment.flagged = true);
return results;
})
.then(addUsersCommentsActions.bind(this, dispatch));
};
@@ -22,49 +22,61 @@ export default ({onTabClick, ...props}) => (
className={`mdl-tabs__tab ${styles.tab}`}>{lang.t('modqueue.flagged')}</a>
</div>
<div className={`mdl-tabs__panel is-active ${styles.listContainer}`} id='pending'>
<CommentList
suspectWords={props.settings.settings.wordlist.suspect}
isActive={props.activeTab === 'pending'}
singleView={props.singleView}
commentIds={props.premodIds}
comments={props.comments.byId}
users={props.users.byId}
onClickAction={props.updateStatus}
onClickShowBanDialog={props.showBanUserDialog}
modActions={['reject', 'approve', 'ban']}
loading={props.comments.loading}/>
<BanUserDialog
open={props.comments.showBanUserDialog}
handleClose={props.hideBanUserDialog}
onClickBanUser={props.banUser}
user={props.comments.banUser}
/>
{
props.activeTab === 'pending'
? <div>
<CommentList
suspectWords={props.settings.settings.wordlist.suspect}
isActive={props.activeTab === 'pending'}
singleView={props.singleView}
commentIds={props.premodIds}
comments={props.comments.byId}
users={props.users.byId}
onClickAction={props.updateStatus}
onClickShowBanDialog={props.showBanUserDialog}
modActions={['reject', 'approve', 'ban']}
loading={props.comments.loading} />
<BanUserDialog
open={props.comments.showBanUserDialog}
handleClose={props.hideBanUserDialog}
onClickBanUser={props.banUser}
user={props.comments.banUser} />
</div>
: null
}
</div>
<div className={`mdl-tabs__panel ${styles.listContainer}`} id='rejected'>
<CommentList
suspectWords={props.settings.settings.wordlist.suspect}
isActive={props.activeTab === 'rejected'}
singleView={props.singleView}
commentIds={props.rejectedIds}
comments={props.comments.byId}
users={props.users.byId}
onClickAction={props.updateStatus}
modActions={['approve']}
loading={props.comments.loading}
/>
{
props.activeTab === 'rejected'
? <CommentList
suspectWords={props.settings.settings.wordlist.suspect}
isActive={props.activeTab === 'rejected'}
singleView={props.singleView}
commentIds={props.rejectedIds}
comments={props.comments.byId}
users={props.users.byId}
onClickAction={props.updateStatus}
modActions={['approve']}
loading={props.comments.loading} />
: null
}
</div>
<div className={`mdl-tabs__panel ${styles.listContainer}`} id='flagged'>
<CommentList
suspectWords={props.settings.settings.wordlist.suspect}
isActive={props.activeTab === 'flagged'}
singleView={props.singleView}
commentIds={props.flaggedIds}
comments={props.comments.byId}
users={props.users.byId}
onClickAction={props.updateStatus}
modActions={['reject', 'approve']}
loading={props.comments.loading}/>
</div>
{
props.activeTab === 'flagged'
? <CommentList
suspectWords={props.settings.settings.wordlist.suspect}
isActive={props.activeTab === 'flagged'}
singleView={props.singleView}
commentIds={props.flaggedIds}
comments={props.comments.byId}
users={props.users.byId}
onClickAction={props.updateStatus}
modActions={['reject', 'approve']}
loading={props.comments.loading} />
: null
}
</div>
<ModerationKeysModal open={props.modalOpen} onClose={props.closeModal} />
</div>
</div>
+4
View File
@@ -14,6 +14,8 @@
"PASSWORD_REQUIRED": "Must input a password",
"PASSWORD_LENGTH": "Password is too short",
"EMAIL_IN_USE": "Email address already in use",
"EMAIL_DISPLAY_NAME_IN_USE": "Email address or display name already in use",
"DISPLAYNAME_IN_USE": "Display name already in use",
"DISPLAY_NAME_REQUIRED": "Must input a display name",
"NO_SPECIAL_CHARACTERS": "Display names can contain letters, numbers and _ only",
"PROFANITY_ERROR": "Display names must not contain profanity. Please contact the administrator if you believe this to be in error."
@@ -34,6 +36,8 @@
"PASSWORD_REQUIRED": "Debe ingresar una contraseña",
"PASSWORD_LENGTH": "La contraseña es muy corta",
"EMAIL_IN_USE": "La dirección de correo electrónico se encuentra en uso",
"EMAIL_DISPLAY_NAME_IN_USE": "Correo o Nombre en uso.",
"DISPLAYNAME_IN_USE": "Nombre en uso.",
"DISPLAY_NAME_REQUIRED": "Debe ingresar un nombre",
"NO_SPECIAL_CHARACTERS": "Los nombres pueden contener letras, números y _",
"PROFANITY_ERROR": "Los nombres no pueden contener blasfemias. Por favor contacte al administrador si cree que esto es un error"
+2
View File
@@ -19,6 +19,7 @@ export default {
alreadyHaveAnAccount: 'Already have an account?',
recoverPassword: 'Recover password',
emailInUse: 'Email address already in use',
emailORusernameInUse: 'Email address or Username already in use',
requiredField: 'This field is required',
passwordsDontMatch: 'Passwords don\'t match.',
specialCharacters: 'Display names can contain letters, numbers and _ only',
@@ -45,6 +46,7 @@ export default {
alreadyHaveAnAccount: 'Ya tienes una cuenta?',
recoverPassword: 'Recuperar contraseña',
emailInUse: 'Este email se encuentra en uso',
emailORusernameInUse: 'Este email ó nombre se encuentran en uso',
requiredField: 'Este campo es requerido',
passwordsDontMatch: 'Las contraseñas no coinciden',
specialCharacters: 'Los nombres pueden contener letras, números y _',
+6
View File
@@ -54,6 +54,11 @@ const ErrEmailTaken = new APIError('Email address already in use', {
status: 400
});
const ErrDisplayTaken = new APIError('Display name already in use', {
translation_key: 'DISPLAYNAME_IN_USE',
status: 400
});
const ErrSpecialChars = new APIError('No special characters are allowed in a display name', {
translation_key: 'NO_SPECIAL_CHARACTERS',
status: 400
@@ -116,6 +121,7 @@ module.exports = {
ErrSpecialChars,
ErrMissingDisplay,
ErrContainsProfanity,
ErrDisplayTaken,
ErrAssetCommentingClosed,
ErrNotFound,
ErrInvalidAssetURL,
+10 -2
View File
@@ -282,8 +282,16 @@ CommentSchema.statics.all = () => Comment.find();
* probably to be paginated at some point in the future
* @return {Promise} array resolves to an array of comments by that user
*/
CommentSchema.statics.findByUserId = function (author_id) {
return Comment.find({author_id});
CommentSchema.statics.findByUserId = function (author_id, admin = false) {
// do not return un-published comments for non-admins
let query = {author_id};
if (!admin) {
query.$nor = [{status: 'premod'}, {status: 'rejected'}];
}
return Comment.find(query);
};
// Comment model.
+8 -1
View File
@@ -81,7 +81,11 @@ const UserSchema = new mongoose.Schema({
// This is sourced from the social provider or set manually during user setup
// and simply provides a name to display for the given user.
displayName: String,
displayName: {
type: String,
unique: true,
required: true
},
// This is true when the user account is disabled, no action should be
// acknowledged when they are disabled. Logins are also prevented.
@@ -379,6 +383,9 @@ UserService.createLocalUser = (email, password, displayName) => {
user.save((err) => {
if (err) {
if (err.code === 11000) {
if (err.message.match('displayName')) {
return reject(errors.ErrDisplayTaken);
}
return reject(errors.ErrEmailTaken);
}
return reject(err);
+1 -1
View File
@@ -48,7 +48,7 @@ router.get('/', (req, res, next) => {
// otherwise this will be a vulnerability if you pass user_id and something else,
// the app will return admin-level data without the proper checks
if (user_id) {
query = Comment.findByUserId(user_id);
query = Comment.findByUserId(user_id, authorization.has(req.user, 'admin'));
} else if (status) {
query = assetIDWrap(Comment.findByStatus(status === 'new' ? null : status));
} else if (action_type) {
+17
View File
@@ -209,6 +209,23 @@ describe('models.Comment', () => {
});
});
describe('#findByUserId', () => {
it('should return all comments if admin', () => {
return Comment.findByUserId('456', true)
.then(comments => {
expect(comments).to.have.length(4);
});
});
it('should not return premod and rejected comments if not admin', () => {
return Comment.findByUserId('456')
.then(comments => {
expect(comments).to.have.length(1);
});
});
});
describe('#changeStatus', () => {
it('should change the status of a comment from no status', () => {
+16
View File
@@ -85,6 +85,22 @@ describe('models.User', () => {
});
describe('#createLocalUser', () => {
it('should not create a user with duplicate display name', () => {
return User.createLocalUsers([{
email: 'otrostampi@gmail.com',
displayName: 'Stampi',
password: '1Coralito!'
}])
.then((user) => {
expect(user).to.be.null;
})
.catch((error) => {
expect(error).to.not.be.null;
});
});
});
describe('#createEmailConfirmToken', () => {
it('should create a token for a valid user', () => {
+2 -3
View File
@@ -83,15 +83,14 @@ describe('/api/v1/comments', () => {
]);
});
it('should return only the owners comments if the user is not an admin', () => {
it('should return only the owners published comments if the user is not an admin', () => {
return chai.request(app)
.get('/api/v1/comments?user_id=456')
.set(passport.inject({id: '456', roles: []}))
.then(res => {
expect(res).to.have.status(200);
expect(res.body.comments).to.have.length(2);
expect(res.body.comments).to.have.length(1);
expect(res.body.comments[0]).to.have.property('author_id', '456');
expect(res.body.comments[1]).to.have.property('author_id', '456');
});
});