}
diff --git a/client/coral-admin/src/routes/Moderation/containers/StorySearch.js b/client/coral-admin/src/routes/Moderation/containers/StorySearch.js
index 04472d219..04aab2d84 100644
--- a/client/coral-admin/src/routes/Moderation/containers/StorySearch.js
+++ b/client/coral-admin/src/routes/Moderation/containers/StorySearch.js
@@ -89,12 +89,14 @@ class StorySearchContainer extends React.Component {
export const withAssetSearchQuery = withQuery(gql`
query SearchStories($value: String = "") {
assets(query: {value: $value, limit: 10}) {
- id
- title
- url
- created_at
- closedAt
- author
+ nodes {
+ id
+ title
+ url
+ created_at
+ closedAt
+ author
+ }
}
}
`, {
diff --git a/client/coral-embed-stream/src/containers/Stream.js b/client/coral-embed-stream/src/containers/Stream.js
index d3279896c..273d39589 100644
--- a/client/coral-embed-stream/src/containers/Stream.js
+++ b/client/coral-embed-stream/src/containers/Stream.js
@@ -302,7 +302,7 @@ const fragments = {
questionBoxEnable
questionBoxContent
questionBoxIcon
- closeTimeout
+ closedTimeout
closedMessage
charCountEnable
charCount
diff --git a/graph/loaders/assets.js b/graph/loaders/assets.js
index 956d573ef..b2e315ec5 100644
--- a/graph/loaders/assets.js
+++ b/graph/loaders/assets.js
@@ -25,8 +25,33 @@ const genAssetsByID = (context, ids) => AssetModel.find({
* @param {Object} query the query
* @return {Promise} resolves the assets
*/
-const getAssetsByQuery = (context, query) => {
- return AssetsService.search(query);
+const getAssetsByQuery = async (context, query) => {
+
+ // If we are requesting based on a limit, ask for one more than we want.
+ const limit = query.limit;
+ if (limit) {
+ query.limit += 1;
+ }
+
+ const nodes = await AssetsService.search(query);
+
+ // The hasNextPage is always handled the same (ask for one more than we need,
+ // if there is one more, than there is more).
+ let hasNextPage = false;
+ if (limit && nodes.length > limit) {
+
+ // There was one more than we expected! Set hasNextPage = true and remove
+ // the last item from the array that we requested.
+ hasNextPage = true;
+ nodes.splice(limit, 1);
+ }
+
+ return {
+ startCursor: nodes && nodes.length > 0 ? nodes[0].created_at : null,
+ endCursor: nodes && nodes.length > 0 ? nodes[nodes.length - 1].created_at : null,
+ hasNextPage,
+ nodes,
+ };
};
/**
@@ -84,7 +109,7 @@ module.exports = (context) => ({
getByURL: (url) => findOrCreateAssetByURL(context, url),
findByUrl: (url) => findByUrl(context, url),
- search: (query) => getAssetsByQuery(context, query),
+ getByQuery: (query) => getAssetsByQuery(context, query),
getByID: new DataLoader((ids) => genAssetsByID(context, ids)),
getForMetrics: () => getAssetsForMetrics(context),
getAll: new util.SingletonResolver(() => AssetModel.find({}))
diff --git a/graph/loaders/settings.js b/graph/loaders/settings.js
index 83545821d..2d6189b7d 100644
--- a/graph/loaders/settings.js
+++ b/graph/loaders/settings.js
@@ -1,5 +1,5 @@
const SettingsService = require('../../services/settings');
-const util = require('./util');
+const {SingletonResolver} = require('./util');
/**
* Creates a set of loaders based on a GraphQL context.
@@ -7,5 +7,5 @@ const util = require('./util');
* @return {Object} object of loaders
*/
module.exports = () => ({
- Settings: new util.SingletonResolver(() => SettingsService.retrieve())
+ Settings: new SingletonResolver(() => SettingsService.retrieve())
});
diff --git a/graph/mutators/asset.js b/graph/mutators/asset.js
new file mode 100644
index 000000000..462607f5f
--- /dev/null
+++ b/graph/mutators/asset.js
@@ -0,0 +1,55 @@
+const errors = require('../../errors');
+const {
+ UPDATE_ASSET_SETTINGS,
+ UPDATE_ASSET_STATUS,
+} = require('../../perms/constants');
+
+const AssetsService = require('../../services/assets');
+const AssetModel = require('../../models/asset');
+
+/**
+ * updateSettings will update the settings on an asset.
+ *
+ * @param {Object} ctx graphql context
+ * @param {String} id the asset's id to update
+ * @param {Object} settings the settings to update on the asset.
+ */
+const updateSettings = async (ctx, id, settings) => AssetsService.overrideSettings(id, settings);
+
+/**
+ * updateStatus will update the status of an asset.
+ *
+ * @param {Object} ctx graphql context
+ * @param {String} id the asset's id to update
+ * @param {Object} status the status to change on the asset relating to it's
+ * current state.
+ */
+const updateStatus = async (ctx, id, {closedAt, closedMessage}) => AssetModel.update({
+ id,
+}, {
+ $set: {
+ closedAt,
+ closedMessage
+ }
+});
+
+module.exports = (ctx) => {
+ let mutators = {
+ Asset: {
+ updateSettings: () => Promise.reject(errors.ErrNotAuthorized),
+ updateStatus: () => Promise.reject(errors.ErrNotAuthorized)
+ }
+ };
+
+ if (ctx.user) {
+ if (ctx.user.can(UPDATE_ASSET_SETTINGS)) {
+ mutators.Asset.updateSettings = (id, settings) => updateSettings(ctx, id, settings);
+ }
+
+ if (ctx.user.can(UPDATE_ASSET_STATUS)) {
+ mutators.Asset.updateStatus = (id, status) => updateStatus(ctx, id, status);
+ }
+ }
+
+ return mutators;
+};
diff --git a/graph/mutators/index.js b/graph/mutators/index.js
index e7e5df9e7..0f290c69a 100644
--- a/graph/mutators/index.js
+++ b/graph/mutators/index.js
@@ -3,6 +3,8 @@ const debug = require('debug')('talk:graph:mutators');
const Comment = require('./comment');
const Action = require('./action');
+const Asset = require('./asset');
+const Settings = require('./settings');
const Tag = require('./tag');
const Token = require('./token');
const User = require('./user');
@@ -14,6 +16,8 @@ let mutators = [
// Load in the core mutators.
Comment,
Action,
+ Asset,
+ Settings,
Tag,
Token,
User,
diff --git a/graph/mutators/settings.js b/graph/mutators/settings.js
new file mode 100644
index 000000000..f725d791d
--- /dev/null
+++ b/graph/mutators/settings.js
@@ -0,0 +1,33 @@
+const errors = require('../../errors');
+
+const {
+ UPDATE_SETTINGS,
+ UPDATE_WORDLIST,
+} = require('../../perms/constants');
+
+const SettingsService = require('../../services/settings');
+
+const update = async (ctx, settings) => SettingsService.update(settings);
+
+const updateWordlist = async (ctx, wordlist) => SettingsService.updateWordlist(wordlist);
+
+module.exports = (ctx) => {
+ let mutators = {
+ Settings: {
+ update: () => Promise.reject(errors.ErrNotAuthorized),
+ updateWordlist: () => Promise.reject(errors.ErrNotAuthorized)
+ }
+ };
+
+ if (ctx.user) {
+ if (ctx.user.can(UPDATE_SETTINGS)) {
+ mutators.Settings.update = (id, settings) => update(ctx, id, settings);
+ }
+
+ if (ctx.user.can(UPDATE_WORDLIST)) {
+ mutators.Settings.updateWordlist = (id, status) => updateWordlist(ctx, id, status);
+ }
+ }
+
+ return mutators;
+};
diff --git a/graph/resolvers/root_mutation.js b/graph/resolvers/root_mutation.js
index 21e6c4787..df67b8e8a 100644
--- a/graph/resolvers/root_mutation.js
+++ b/graph/resolvers/root_mutation.js
@@ -29,6 +29,12 @@ const RootMutation = {
stopIgnoringUser: async (_, {id}, {mutators: {User}}) => {
await User.stopIgnoringUser({id});
},
+ updateAssetSettings: async (_, {id, input: settings}, {mutators: {Asset}}) => {
+ await Asset.updateSettings(id, settings);
+ },
+ updateAssetStatus: async (_, {id, input: status}, {mutators: {Asset}}) => {
+ await Asset.updateStatus(id, status);
+ },
setCommentStatus: async (_, {id, status}, {mutators: {Comment}, pubsub}) => {
const comment = await Comment.setStatus({id, status});
if (status === 'ACCEPTED') {
@@ -47,6 +53,12 @@ const RootMutation = {
removeTag: async (_, {tag}, {mutators: {Tag}}) => {
await Tag.remove(tag);
},
+ updateSettings: async (_, {input: settings}, {mutators: {Settings}}) => {
+ await Settings.update(settings);
+ },
+ updateWordlist: async (_, {input: wordlist}, {mutators: {Settings}}) => {
+ await Settings.updateWordlist(wordlist);
+ },
createToken: async (_, {input}, {mutators: {Token}}) => ({
token: await Token.create(input),
}),
diff --git a/graph/resolvers/root_query.js b/graph/resolvers/root_query.js
index 239b052c0..13e41a312 100644
--- a/graph/resolvers/root_query.js
+++ b/graph/resolvers/root_query.js
@@ -11,7 +11,7 @@ const RootQuery = {
return null;
}
- return Assets.search(query);
+ return Assets.getByQuery(query);
},
asset(_, query, {loaders: {Assets}}) {
if (query.id) {
diff --git a/graph/resolvers/settings.js b/graph/resolvers/settings.js
index 72608a9c4..30e614c38 100644
--- a/graph/resolvers/settings.js
+++ b/graph/resolvers/settings.js
@@ -1,3 +1,21 @@
+const {
+ VIEW_PROTECTED_SETTINGS,
+} = require('../../perms/constants');
+
+const {decorateWithPermissionCheck} = require('./util');
+
const Settings = {};
+// PROTECTED_SETTINGS are the settings keys that must be protected for only some
+// eyes.
+const PROTECTED_SETTINGS = {
+ 'premodLinksEnable': [VIEW_PROTECTED_SETTINGS],
+ 'autoCloseStream': [VIEW_PROTECTED_SETTINGS],
+ 'wordlist': [VIEW_PROTECTED_SETTINGS],
+ 'domains': [VIEW_PROTECTED_SETTINGS],
+};
+
+// decorate the fields on the settings resolver with a permission check.
+decorateWithPermissionCheck(Settings, PROTECTED_SETTINGS);
+
module.exports = Settings;
diff --git a/graph/resolvers/util.js b/graph/resolvers/util.js
index ef3c70865..cf3162679 100644
--- a/graph/resolvers/util.js
+++ b/graph/resolvers/util.js
@@ -13,6 +13,31 @@ const decorateWithTags = (typeResolver) => {
};
};
+/**
+ * decorateWithPermissionCheck will decorate the field resolver with
+ * permission checks.
+ *
+ * @param {Object} typeResolver the type resolver
+ * @param {Object} protect the object with field -> Array of permissions
+ */
+const decorateWithPermissionCheck = (typeResolver, protect) => {
+ for (const [field, permissions] of Object.entries(protect)) {
+ let fieldResolver = (obj) => obj[field];
+ if (field in typeResolver) {
+ fieldResolver = typeResolver[field];
+ }
+
+ typeResolver[field] = (obj, args, ctx, info) => {
+ if (!ctx.user || !ctx.user.can(...permissions)) {
+ return null;
+ }
+
+ return fieldResolver(obj, args, ctx, info);
+ };
+ }
+};
+
module.exports = {
- decorateWithTags
+ decorateWithTags,
+ decorateWithPermissionCheck,
};
diff --git a/graph/typeDefs.graphql b/graph/typeDefs.graphql
index c3cbc2e57..916c0ade5 100644
--- a/graph/typeDefs.graphql
+++ b/graph/typeDefs.graphql
@@ -164,7 +164,19 @@ input AssetsQuery {
# Limit the number of results to be returned
limit: Int = 10
+
+ # open filters assets that are open/closed/all. Not providing this parameter
+ # will return all the assets, true will return assets that are open, and false
+ # will return assets that are closed.
+ open: Boolean
+
+ # sortOrder specifies the order of the sort for the returned Assets.
+ sortOrder: SORT_ORDER = DESC
+
+ # Skip results from the last created_at timestamp.
+ cursor: Cursor
}
+
################################################################################
## Tags
################################################################################
@@ -572,27 +584,85 @@ enum MODERATION_MODE {
POST
}
-# Site wide global settings.
+# Wordlist describes all the available wordlists.
+type Wordlist {
+
+ # banned words will by default reject the comment if it is found.
+ banned: [String!]!
+
+ # suspect words will simply flag the comment.
+ suspect: [String!]!
+}
+
+# Domains describes all the available lists of domains.
+type Domains {
+
+ # whitelist is the list of domains that the embed is allowed to render on.
+ whitelist: [String!]!
+}
+
+# Settings stores the global settings for a given installation.
type Settings {
- # Moderation mode for the site.
+ # moderation is the moderation mode for all Asset's on the site.
moderation: MODERATION_MODE!
# Enables a requirement for email confirmation before a user can login.
requireEmailConfirmation: Boolean
+ # infoBoxEnable will enable the Info Box content visible above the question
+ # box.
infoBoxEnable: Boolean
+
+ # infoBoxContent is the content of the Info Box.
infoBoxContent: String
- premodLinksEnable: Boolean
+
+ # questionBoxEnable will enable the Question Box's content to be visible above
+ # the comment box.
questionBoxEnable: Boolean
+
+ # questionBoxContent is the content of the Question Box.
questionBoxContent: String
+
+ # premodLinksEnable will put all comments that contain links into premod.
+ premodLinksEnable: Boolean
+
+ # questionBoxIcon is the icon for the Question Box.
questionBoxIcon: String
- closeTimeout: Int
+
+ # autoCloseStream when true will auto close the stream when the `closeTimeout`
+ # amount of seconds have been reached.
+ autoCloseStream: Boolean
+
+ # customCssUrl is the URL of the custom CSS used to display on the frontend.
+ customCssUrl: String
+
+ # closedTimeout is the amount of seconds from the created_at timestamp that a
+ # given asset will be considered closed.
+ closedTimeout: Int
+
+ # closedMessage is the message shown to the user when the given Asset is
+ # closed.
closedMessage: String
+
+ # editCommentWindowLength is the length of time (in milliseconds) after a
+ # comment is posted that it can still be edited by the author.
+ editCommentWindowLength: Int
+
+ # charCountEnable is true when the character count restriction is enabled.
charCountEnable: Boolean
+
+ # charCount is the maximum number of characters a comment may be.
charCount: Int
+ # organizationName is the name of the organization.
organizationName: String
+
+ # wordlist will return a given list of words.
+ wordlist: Wordlist
+
+ # domains will return a given list of domains.
+ domains: Domains
}
################################################################################
@@ -648,6 +718,22 @@ type Asset {
author: String
}
+# AssetConnection represents a paginable subset of a asset list.
+type AssetConnection {
+
+ # Indicates that there are more assets after this subset.
+ hasNextPage: Boolean!
+
+ # Cursor of first asset in subset.
+ startCursor: Cursor
+
+ # Cursor of last asset in subset.
+ endCursor: Cursor
+
+ # Subset of assets.
+ nodes: [Asset!]!
+}
+
################################################################################
## Errors
################################################################################
@@ -735,7 +821,7 @@ type RootQuery {
comment(id: ID!): Comment
# All assets. Requires the `ADMIN` role.
- assets(query: AssetsQuery): [Asset]
+ assets(query: AssetsQuery): AssetConnection
# Find or create an asset by url, or just find with the ID.
asset(id: ID, url: String): Asset
@@ -905,6 +991,56 @@ input RejectUsernameInput {
message: String!
}
+# Configurable settings that can be overridden for the Asset. You must specify
+# all fields that should be updated.
+input AssetSettingsInput {
+
+ # premodLinksEnable will put all comments that contain links into premod.
+ premodLinksEnable: Boolean
+
+ # moderation is the moderation mode for the asset.
+ moderation: MODERATION_MODE
+
+ # questionBoxEnable will enable the Question Boxs' content to be visable above
+ # the comment box.
+ questionBoxEnable: Boolean
+
+ # questionBoxContent is the content of the Question Box.
+ questionBoxContent: String
+
+ # questionBoxIcon is the icon for the Question Box.
+ questionBoxIcon: String
+}
+
+# UpdateAssetStatusInput contains the input to change the status of a comment as
+# it relates to being open/closed for commenting.
+input UpdateAssetStatusInput {
+
+ # closedAt is the time that the asset will be closed for commenting. If this
+ # is null or in the future, it will be open for commenting.
+ closedAt: Date
+
+ # closedMessage is the message to be set on the asset when it is closed. If it
+ # is null, then the message will default to the globally set `closedMessage`.
+ closedMessage: String
+}
+
+# UpdateAssetStatusResponse is the response returned with possibly some errors
+# relating to the update status attempt.
+type UpdateAssetStatusResponse implements Response {
+
+ # An array of errors relating to the mutation that occurred.
+ errors: [UserError!]
+}
+
+# UpdateAssetSettingsResponse is the response returned with possibly some errors
+# relating to the update settings attempt.
+type UpdateAssetSettingsResponse implements Response {
+
+ # An array of errors relating to the mutation that occurred.
+ errors: [UserError!]
+}
+
# DeleteActionResponse is the response returned with possibly some errors
# relating to the delete action attempt.
type DeleteActionResponse implements Response {
@@ -998,6 +1134,91 @@ type EditCommentResponse implements Response {
errors: [UserError!]
}
+# UpdateSettingsInput is the input used to input the global site settings. This
+# will override the existing settings, so all fields must be included.
+input UpdateSettingsInput {
+
+ # moderation is the moderation mode for all Asset's on the site.
+ moderation: MODERATION_MODE
+
+ # Enables a requirement for email confirmation before a user can login.
+ requireEmailConfirmation: Boolean
+
+ # infoBoxEnable will enable the Info Box content visible above the question
+ # box.
+ infoBoxEnable: Boolean
+
+ # infoBoxContent is the content of the Info Box.
+ infoBoxContent: String
+
+ # questionBoxEnable will enable the Question Box's content to be visible above
+ # the comment box.
+ questionBoxEnable: Boolean
+
+ # questionBoxContent is the content of the Question Box.
+ questionBoxContent: String
+
+ # premodLinksEnable will put all comments that contain links into premod.
+ premodLinksEnable: Boolean
+
+ # questionBoxIcon is the icon for the Question Box.
+ questionBoxIcon: String
+
+ # autoCloseStream when true will auto close the stream when the `closeTimeout`
+ # amount of seconds have been reached.
+ autoCloseStream: Boolean
+
+ # customCssUrl is the URL of the custom CSS used to display on the frontend.
+ customCssUrl: String
+
+ # closedTimeout is the amount of seconds from the created_at timestamp that a
+ # given asset will be considered closed.
+ closedTimeout: Int
+
+ # closedMessage is the message shown to the user when the given Asset is
+ # closed.
+ closedMessage: String
+
+ # charCountEnable is true when the character count restriction is enabled.
+ charCountEnable: Boolean
+
+ # charCount is the maximum number of characters a comment may be.
+ charCount: Int
+
+ # organizationName is the name of the organization.
+ organizationName: String
+
+ # editCommentWindowLength is the length of time (in milliseconds) after a
+ # comment is posted that it can still be edited by the author.
+ editCommentWindowLength: Int
+}
+
+# UpdateSettingsResponse contains any errors that were rendered as a result
+# of the mutation.
+type UpdateSettingsResponse implements Response {
+
+ # An array of errors relating to the mutation that occurred.
+ errors: [UserError!]
+}
+
+# UpdateWordlistInput is the list of words that composes the Wordlist.
+input UpdateWordlistInput {
+
+ # banned words will by default reject the comment if it is found.
+ banned: [String!]!
+
+ # suspect words will simply flag the comment.
+ suspect: [String!]!
+}
+
+# UpdateWordlistResponse contains any errors that were rendered as a result
+# of the mutation.
+type UpdateWordlistResponse implements Response {
+
+ # An array of errors relating to the mutation that occurred.
+ errors: [UserError!]
+}
+
# CreateTokenInput contains the input to create the token.
input CreateTokenInput {
@@ -1033,30 +1254,34 @@ type RevokeTokenResponse implements Response {
type RootMutation {
# Creates a comment on the asset.
- createComment(input: CreateCommentInput!): CreateCommentResponse
+ createComment(input: CreateCommentInput!): CreateCommentResponse!
# Creates a flag on an entity.
- createFlag(flag: CreateFlagInput!): CreateFlagResponse
+ createFlag(flag: CreateFlagInput!): CreateFlagResponse!
# Creates a don't agree action on an entity.
- createDontAgree(dontagree: CreateDontAgreeInput!): CreateDontAgreeResponse
+ createDontAgree(dontagree: CreateDontAgreeInput!): CreateDontAgreeResponse!
# Delete an action based on the action id.
deleteAction(id: ID!): DeleteActionResponse
# Edit a comment
- editComment(id: ID!, asset_id: ID!, edit: EditCommentInput): EditCommentResponse
+ editComment(id: ID!, asset_id: ID!, edit: EditCommentInput): EditCommentResponse!
# Sets User status. Requires the `ADMIN` role.
+ # Mutation is restricted.
setUserStatus(id: ID!, status: USER_STATUS!): SetUserStatusResponse
# Suspends a user. Requires the `ADMIN` role.
+ # Mutation is restricted.
suspendUser(input: SuspendUserInput!): SuspendUserResponse
# Reject a username. Requires the `ADMIN` role.
+ # Mutation is restricted.
rejectUsername(input: RejectUsernameInput!): RejectUsernameResponse
# Sets Comment status. Requires the `ADMIN` role.
+ # Mutation is restricted.
setCommentStatus(id: ID!, status: COMMENT_STATUS!): SetCommentStatusResponse
# Add a tag.
@@ -1065,16 +1290,35 @@ type RootMutation {
# Removes a tag.
removeTag(tag: ModifyTagInput!): ModifyTagResponse
+ # Updates settings on a given asset.
+ # Mutation is restricted.
+ updateAssetSettings(id: ID!, input: AssetSettingsInput!): UpdateAssetSettingsResponse
+
+ # Updates the status of an asset allowing you to close/reopen an asset for
+ # commenting.
+ # Mutation is restricted.
+ updateAssetStatus(id: ID!, input: UpdateAssetStatusInput!): UpdateAssetStatusResponse
+
+ # updateSettings will update the global settings.
+ # Mutation is restricted.
+ updateSettings(input: UpdateSettingsInput!): UpdateSettingsResponse
+
+ # updateWordlist will update the given Wordlist.
+ # Mutation is restricted.
+ updateWordlist(input: UpdateWordlistInput!): UpdateWordlistResponse
+
# Ignore comments by another user
ignoreUser(id: ID!): IgnoreUserResponse
# CreateToken will create a token that is attached to the current user.
+ # Mutation is restricted.
createToken(input: CreateTokenInput!): CreateTokenResponse!
# RevokeToken will revoke an existing token.
+ # Mutation is restricted.
revokeToken(input: RevokeTokenInput!): RevokeTokenResponse
- # Stop Ignoring comments by another user
+ # Stop Ignoring comments by another user.
stopIgnoringUser(id: ID!): StopIgnoringUserResponse
}
diff --git a/perms/constants.js b/perms/constants.js
index 383f36daf..070fe80bf 100644
--- a/perms/constants.js
+++ b/perms/constants.js
@@ -16,6 +16,10 @@ module.exports = {
UPDATE_CONFIG: 'UPDATE_CONFIG',
CREATE_TOKEN: 'CREATE_TOKEN',
REVOKE_TOKEN: 'REVOKE_TOKEN',
+ UPDATE_ASSET_SETTINGS: 'UPDATE_ASSET_SETTINGS',
+ UPDATE_ASSET_STATUS: 'UPDATE_ASSET_STATUS',
+ UPDATE_SETTINGS: 'UPDATE_SETTINGS',
+ UPDATE_WORDLIST: 'UPDATE_WORDLIST',
// queries
SEARCH_ASSETS: 'SEARCH_ASSETS',
@@ -27,6 +31,7 @@ module.exports = {
LIST_OWN_TOKENS: 'LIST_OWN_TOKENS',
SEARCH_COMMENT_STATUS_HISTORY: 'SEARCH_COMMENT_STATUS_HISTORY',
VIEW_SUSPENSION_INFO: 'VIEW_SUSPENSION_INFO',
+ VIEW_PROTECTED_SETTINGS: 'VIEW_PROTECTED_SETTINGS',
// subscriptions
SUBSCRIBE_COMMENT_ACCEPTED: 'SUBSCRIBE_COMMENT_ACCEPTED',
diff --git a/perms/index.js b/perms/index.js
index be6e7a2cd..a6b459efe 100644
--- a/perms/index.js
+++ b/perms/index.js
@@ -41,11 +41,8 @@ const findGrant = (user, perms) => {
*/
module.exports = (user, ...perms) => {
- // make sure all the passed permissions are not typos
- const missingPerms = perms.filter((perm) => {
- return allPermissions.indexOf(perm) === -1;
- });
-
+ // Make sure all the passed permissions are not typos.
+ const missingPerms = perms.filter((perm) => !allPermissions.includes(perm));
if (missingPerms.length > 0) {
throw new Error(`${missingPerms.join(' ')} are not valid permissions.`);
}
diff --git a/perms/mutationReducer.js b/perms/mutationReducer.js
index 897217348..2e8718ed1 100644
--- a/perms/mutationReducer.js
+++ b/perms/mutationReducer.js
@@ -4,33 +4,26 @@ const types = require('./constants');
module.exports = (user, perm) => {
switch (perm) {
case types.CREATE_COMMENT:
- return true;
case types.CREATE_ACTION:
- return true;
case types.DELETE_ACTION:
- return true;
case types.EDIT_NAME:
- return true;
case types.EDIT_COMMENT:
return true;
- case types.UPDATE_USER_ROLES:
- return check(user, ['ADMIN']);
- case types.REJECT_USERNAME:
- return check(user, ['ADMIN', 'MODERATOR']);
- case types.SET_USER_STATUS:
- return check(user, ['ADMIN', 'MODERATOR']);
- case types.SUSPEND_USER:
- return check(user, ['ADMIN', 'MODERATOR']);
- case types.SET_COMMENT_STATUS:
- return check(user, ['ADMIN', 'MODERATOR']);
case types.ADD_COMMENT_TAG:
- return check(user, ['ADMIN', 'MODERATOR']);
case types.REMOVE_COMMENT_TAG:
- return check(user, ['ADMIN', 'MODERATOR']);
+ return check(user, ['ADMIN', 'MODERATOR', 'STAFF']);
+ case types.UPDATE_USER_ROLES:
+ case types.REJECT_USERNAME:
+ case types.SET_USER_STATUS:
+ case types.SUSPEND_USER:
+ case types.SET_COMMENT_STATUS:
case types.UPDATE_CONFIG:
+ case types.UPDATE_SETTINGS:
+ case types.UPDATE_WORDLIST:
+ case types.UPDATE_ASSET_SETTINGS:
+ case types.UPDATE_ASSET_STATUS:
return check(user, ['ADMIN', 'MODERATOR']);
case types.CREATE_TOKEN:
- return check(user, ['ADMIN']);
case types.REVOKE_TOKEN:
return check(user, ['ADMIN']);
default:
diff --git a/perms/queryReducer.js b/perms/queryReducer.js
index 0b76c225a..b71c3d964 100644
--- a/perms/queryReducer.js
+++ b/perms/queryReducer.js
@@ -21,6 +21,8 @@ module.exports = (user, perm) => {
return check(user, ['ADMIN', 'MODERATOR']);
case types.VIEW_SUSPENSION_INFO:
return check(user, ['ADMIN', 'MODERATOR']);
+ case types.VIEW_PROTECTED_SETTINGS:
+ return check(user, ['ADMIN', 'MODERATOR']);
default:
break;
}
diff --git a/plugins/talk-plugin-offtopic/client/components/OffTopicCheckbox.js b/plugins/talk-plugin-offtopic/client/components/OffTopicCheckbox.js
index 7b59d87d6..7f4ec3503 100644
--- a/plugins/talk-plugin-offtopic/client/components/OffTopicCheckbox.js
+++ b/plugins/talk-plugin-offtopic/client/components/OffTopicCheckbox.js
@@ -29,12 +29,13 @@ export default class OffTopicCheckbox extends React.Component {
}
render() {
+ const checked = this.props.tags.indexOf(this.label) >= 0;
return (