mirror of
https://github.com/wassname/talk.git
synced 2026-07-27 11:28:12 +08:00
Merge branch 'master' into feature/talk-plugin-toxic-comments
Conflicts: graph/resolvers/root_mutation.js graph/typeDefs.graphql
This commit is contained in:
@@ -8,7 +8,7 @@ const StorySearch = (props) => {
|
||||
|
||||
const {
|
||||
root: {
|
||||
assets = []
|
||||
assets,
|
||||
},
|
||||
data: {loading}
|
||||
} = props;
|
||||
@@ -62,7 +62,7 @@ const StorySearch = (props) => {
|
||||
{
|
||||
loading
|
||||
? <Spinner />
|
||||
: assets.map((story, i) => {
|
||||
: assets.nodes.map((story, i) => {
|
||||
const storyOpen = story.closedAt === null || new Date(story.closedAt) > new Date();
|
||||
|
||||
return <Story
|
||||
@@ -77,7 +77,7 @@ const StorySearch = (props) => {
|
||||
})
|
||||
}
|
||||
|
||||
{assets.length === 0 && <div className={styles.noResults}>No results</div>}
|
||||
{assets.nodes.length === 0 && <div className={styles.noResults}>No results</div>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
`, {
|
||||
|
||||
@@ -302,7 +302,7 @@ const fragments = {
|
||||
questionBoxEnable
|
||||
questionBoxContent
|
||||
questionBoxIcon
|
||||
closeTimeout
|
||||
closedTimeout
|
||||
closedMessage
|
||||
charCountEnable
|
||||
charCount
|
||||
|
||||
+28
-3
@@ -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({}))
|
||||
|
||||
@@ -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())
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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),
|
||||
}),
|
||||
|
||||
@@ -11,7 +11,7 @@ const RootQuery = {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Assets.search(query);
|
||||
return Assets.getByQuery(query);
|
||||
},
|
||||
asset(_, query, {loaders: {Assets}}) {
|
||||
if (query.id) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
+26
-1
@@ -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<String> 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,
|
||||
};
|
||||
|
||||
+254
-10
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
+2
-5
@@ -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.`);
|
||||
}
|
||||
|
||||
+10
-17
@@ -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:
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -29,12 +29,13 @@ export default class OffTopicCheckbox extends React.Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const checked = this.props.tags.indexOf(this.label) >= 0;
|
||||
return (
|
||||
<div className={styles.offTopic}>
|
||||
{
|
||||
!this.props.isReply ? (
|
||||
<label className={styles.offTopicLabel}>
|
||||
<input type="checkbox" onChange={this.handleChange}/>
|
||||
<input type="checkbox" onChange={this.handleChange} checked={checked}/>
|
||||
{t('off_topic')}
|
||||
</label>
|
||||
) : null
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
|
||||
const scraper = require('../../../services/scraper');
|
||||
const errors = require('../../../errors');
|
||||
const AssetsService = require('../../../services/assets');
|
||||
|
||||
@@ -88,25 +87,6 @@ router.get('/:asset_id', async (req, res, next) => {
|
||||
}
|
||||
});
|
||||
|
||||
// Adds the asset id to the queue to be scraped.
|
||||
router.post('/:asset_id/scrape', async (req, res, next) => {
|
||||
try {
|
||||
|
||||
// Send back the asset.
|
||||
let asset = await AssetsService.findById(req.params.asset_id);
|
||||
if (!asset) {
|
||||
return next(errors.ErrNotFound);
|
||||
}
|
||||
|
||||
let job = await scraper.create(asset);
|
||||
|
||||
// Send the job back for monitoring.
|
||||
res.status(201).json(job);
|
||||
} catch (e) {
|
||||
return next(e);
|
||||
}
|
||||
});
|
||||
|
||||
router.put('/:asset_id/settings', async (req, res, next) => {
|
||||
try {
|
||||
await AssetsService.overrideSettings(req.params.asset_id, req.body);
|
||||
|
||||
+51
-14
@@ -108,19 +108,57 @@ module.exports = class AssetsService {
|
||||
* @param {String} value string to search by.
|
||||
* @return {Promise}
|
||||
*/
|
||||
static search({value, skip, limit} = {}) {
|
||||
if (!value) {
|
||||
return AssetsService.all(skip, limit);
|
||||
} else {
|
||||
return AssetModel
|
||||
.find({
|
||||
$text: {
|
||||
$search: value
|
||||
}
|
||||
})
|
||||
.skip(skip)
|
||||
.limit(limit);
|
||||
static search({value, limit, open, sortOrder, cursor} = {}) {
|
||||
let assets = AssetModel.find({});
|
||||
|
||||
if (value) {
|
||||
assets.merge({
|
||||
$text: {
|
||||
$search: value
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (open != null) {
|
||||
if (open) {
|
||||
assets.merge({
|
||||
$or: [
|
||||
{
|
||||
closedAt: null
|
||||
},
|
||||
{
|
||||
closedAt: {
|
||||
$gt: Date.now()
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
} else {
|
||||
assets.merge({
|
||||
closedAt: {
|
||||
$lt: Date.now()
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (cursor) {
|
||||
if (sortOrder === 'DESC') {
|
||||
assets.merge({
|
||||
created_at: {
|
||||
$lt: cursor,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
assets.merge({
|
||||
created_at: {
|
||||
$gt: cursor,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return assets.sort({created_at: sortOrder === 'DESC' ? -1 : 1}).limit(limit);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -185,10 +223,9 @@ module.exports = class AssetsService {
|
||||
// That's it!
|
||||
}
|
||||
|
||||
static all(skip = null, limit = null) {
|
||||
static all(limit = undefined) {
|
||||
return AssetModel
|
||||
.find({})
|
||||
.skip(skip)
|
||||
.limit(limit);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -42,6 +42,19 @@ module.exports = class SettingsService {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* updateWordlist will update the wordlists.
|
||||
*
|
||||
* @param {Object} wordlist the Wordlist object
|
||||
*/
|
||||
static updateWordlist(wordlist) {
|
||||
return SettingModel.findOneAndUpdate(selector, {
|
||||
$set: {
|
||||
wordlist,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* This is run once when the app starts to ensure settings are populated.
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
const {graphql} = require('graphql');
|
||||
|
||||
const schema = require('../../../../graph/schema');
|
||||
const Context = require('../../../../graph/context');
|
||||
const UserModel = require('../../../../models/user');
|
||||
const SettingsService = require('../../../../services/settings');
|
||||
const AssetModel = require('../../../../models/asset');
|
||||
|
||||
const {expect} = require('chai');
|
||||
|
||||
describe('graph.mutations.updateAssetSettings', () => {
|
||||
let asset;
|
||||
beforeEach(async () => {
|
||||
await SettingsService.init();
|
||||
asset = await AssetModel.create({url: 'http://new.test.com/'});
|
||||
});
|
||||
|
||||
const QUERY = `
|
||||
mutation UpdateAssetStatus($id: ID!, $settings: AssetSettingsInput!) {
|
||||
updateAssetSettings(id: $id, input: $settings) {
|
||||
errors {
|
||||
translation_key
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
describe('context with different user roles', () => {
|
||||
|
||||
[
|
||||
{error: 'NOT_AUTHORIZED'},
|
||||
{roles: ['ADMIN', 'MODERATOR']},
|
||||
{roles: ['MODERATOR']},
|
||||
].forEach(({roles, error}) => {
|
||||
it(roles ? roles.join(', ') : '<None>', async () => {
|
||||
const user = new UserModel({roles});
|
||||
const ctx = new Context({user});
|
||||
|
||||
const settings = {
|
||||
premodLinksEnable: false,
|
||||
moderation: 'POST',
|
||||
questionBoxEnable: true,
|
||||
questionBoxContent: 'Question?',
|
||||
questionBoxIcon: '<Icon>',
|
||||
};
|
||||
|
||||
const res = await graphql(schema, QUERY, {}, ctx, {
|
||||
id: asset.id,
|
||||
settings,
|
||||
});
|
||||
if (res.errors) {
|
||||
console.error(res.errors);
|
||||
}
|
||||
expect(res.errors).to.be.empty;
|
||||
|
||||
if (error) {
|
||||
expect(res.data.updateAssetSettings.errors).to.not.be.empty;
|
||||
expect(res.data.updateAssetSettings.errors[0]).to.have.property('translation_key', error);
|
||||
} else {
|
||||
if (res.data.updateAssetSettings && res.data.updateAssetSettings.errors) {
|
||||
console.error(res.data.updateAssetSettings.errors);
|
||||
}
|
||||
expect(res.data.updateAssetSettings).to.be.null;
|
||||
|
||||
const retrievedAsset = await AssetModel.findOne({id: asset.id});
|
||||
Object.keys(settings).forEach((key) => {
|
||||
expect(retrievedAsset.settings).to.have.property(key, settings[key]);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,70 @@
|
||||
const {graphql} = require('graphql');
|
||||
|
||||
const schema = require('../../../../graph/schema');
|
||||
const Context = require('../../../../graph/context');
|
||||
const UserModel = require('../../../../models/user');
|
||||
const SettingsService = require('../../../../services/settings');
|
||||
const AssetModel = require('../../../../models/asset');
|
||||
|
||||
const {expect} = require('chai');
|
||||
|
||||
describe('graph.mutations.updateAssetStatus', () => {
|
||||
let asset;
|
||||
beforeEach(async () => {
|
||||
await SettingsService.init();
|
||||
asset = await AssetModel.create({url: 'http://new.test.com/'});
|
||||
});
|
||||
|
||||
const QUERY = `
|
||||
mutation UpdateAssetStatus($id: ID!, $status: UpdateAssetStatusInput!) {
|
||||
updateAssetStatus(id: $id, input: $status) {
|
||||
errors {
|
||||
translation_key
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
describe('context with different user roles', () => {
|
||||
|
||||
[
|
||||
{error: 'NOT_AUTHORIZED'},
|
||||
{roles: ['ADMIN', 'MODERATOR']},
|
||||
{roles: ['MODERATOR']},
|
||||
].forEach(({roles, error}) => {
|
||||
it(roles ? roles.join(', ') : '<None>', async () => {
|
||||
const user = new UserModel({roles});
|
||||
const ctx = new Context({user});
|
||||
|
||||
const closedAt = (new Date()).toISOString();
|
||||
const closedMessage = 'my closed message!';
|
||||
|
||||
const res = await graphql(schema, QUERY, {}, ctx, {
|
||||
id: asset.id,
|
||||
status: {
|
||||
closedAt,
|
||||
closedMessage,
|
||||
},
|
||||
});
|
||||
if (res.errors) {
|
||||
console.error(res.errors);
|
||||
}
|
||||
expect(res.errors).to.be.empty;
|
||||
|
||||
if (error) {
|
||||
expect(res.data.updateAssetStatus.errors).to.not.be.empty;
|
||||
expect(res.data.updateAssetStatus.errors[0]).to.have.property('translation_key', error);
|
||||
} else {
|
||||
if (res.data.updateAssetStatus && res.data.updateAssetStatus.errors) {
|
||||
console.error(res.data.updateAssetStatus.errors);
|
||||
}
|
||||
expect(res.data.updateAssetStatus).to.be.null;
|
||||
|
||||
const retrievedAsset = await AssetModel.findOne({id: asset.id});
|
||||
expect(retrievedAsset.closedAt).to.not.be.null;
|
||||
expect(retrievedAsset).to.have.property('closedMessage', closedMessage);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,74 @@
|
||||
const {graphql} = require('graphql');
|
||||
|
||||
const schema = require('../../../../graph/schema');
|
||||
const Context = require('../../../../graph/context');
|
||||
const UserModel = require('../../../../models/user');
|
||||
const SettingsService = require('../../../../services/settings');
|
||||
|
||||
const {expect} = require('chai');
|
||||
|
||||
describe('graph.mutations.updateSettings', () => {
|
||||
beforeEach(async () => {
|
||||
await SettingsService.init();
|
||||
});
|
||||
|
||||
const QUERY = `
|
||||
mutation UpdateSettings($settings: UpdateSettingsInput!) {
|
||||
updateSettings(input: $settings) {
|
||||
errors {
|
||||
translation_key
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
describe('context with different user roles', () => {
|
||||
|
||||
[
|
||||
{error: 'NOT_AUTHORIZED'},
|
||||
{error: 'NOT_AUTHORIZED', roles: []},
|
||||
{roles: ['ADMIN']},
|
||||
{roles: ['ADMIN', 'MODERATOR']},
|
||||
{roles: ['MODERATOR']},
|
||||
].forEach(({roles, error}) => {
|
||||
it(roles ? roles.join(', ') : '<None>', async () => {
|
||||
let user;
|
||||
if (roles != null) {
|
||||
user = new UserModel({roles});
|
||||
}
|
||||
const ctx = new Context({user});
|
||||
|
||||
const newSettings = {
|
||||
premodLinksEnable: false,
|
||||
moderation: 'POST',
|
||||
questionBoxEnable: true,
|
||||
questionBoxContent: 'Question?',
|
||||
questionBoxIcon: '<Icon>',
|
||||
};
|
||||
|
||||
const res = await graphql(schema, QUERY, {}, ctx, {
|
||||
settings: newSettings,
|
||||
});
|
||||
if (res.errors) {
|
||||
console.error(res.errors);
|
||||
}
|
||||
expect(res.errors).to.be.empty;
|
||||
|
||||
if (error) {
|
||||
expect(res.data.updateSettings.errors).to.not.be.empty;
|
||||
expect(res.data.updateSettings.errors[0]).to.have.property('translation_key', error);
|
||||
} else {
|
||||
if (res.data.updateSettings && res.data.updateSettings.errors) {
|
||||
console.error(res.data.updateSettings.errors);
|
||||
}
|
||||
expect(res.data.updateSettings).to.be.null;
|
||||
|
||||
const retrievedSettings = await SettingsService.retrieve();
|
||||
Object.keys(newSettings).forEach((key) => {
|
||||
expect(retrievedSettings).to.have.property(key, newSettings[key]);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,82 @@
|
||||
const {graphql} = require('graphql');
|
||||
|
||||
const schema = require('../../../../graph/schema');
|
||||
const Context = require('../../../../graph/context');
|
||||
const UserModel = require('../../../../models/user');
|
||||
const SettingsService = require('../../../../services/settings');
|
||||
|
||||
const {expect} = require('chai');
|
||||
|
||||
describe('graph.mutations.updateWordlist', () => {
|
||||
beforeEach(async () => {
|
||||
await SettingsService.init();
|
||||
});
|
||||
|
||||
const QUERY = `
|
||||
mutation UpdateWordlist($wordlist: UpdateWordlistInput!) {
|
||||
updateWordlist(input: $wordlist) {
|
||||
errors {
|
||||
translation_key
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
describe('context with different user roles', () => {
|
||||
|
||||
[
|
||||
{error: 'NOT_AUTHORIZED'},
|
||||
{error: 'NOT_AUTHORIZED', roles: []},
|
||||
{roles: ['ADMIN']},
|
||||
{roles: ['ADMIN', 'MODERATOR']},
|
||||
{roles: ['MODERATOR']},
|
||||
].forEach(({roles, error}) => {
|
||||
it(roles && roles.length > 0 ? roles.join(', ') : '<None>', async () => {
|
||||
let user;
|
||||
if (roles != null) {
|
||||
user = new UserModel({roles});
|
||||
}
|
||||
const ctx = new Context({user});
|
||||
|
||||
const wordlist = {
|
||||
banned: [
|
||||
'happy',
|
||||
],
|
||||
suspect: [
|
||||
'sad',
|
||||
],
|
||||
};
|
||||
|
||||
const res = await graphql(schema, QUERY, {}, ctx, {
|
||||
wordlist,
|
||||
});
|
||||
if (res.errors) {
|
||||
console.error(res.errors);
|
||||
}
|
||||
expect(res.errors).to.be.empty;
|
||||
|
||||
if (error) {
|
||||
expect(res.data.updateWordlist.errors).to.not.be.empty;
|
||||
expect(res.data.updateWordlist.errors[0]).to.have.property('translation_key', error);
|
||||
|
||||
const {wordlist: retrievedWordlist} = await SettingsService.retrieve();
|
||||
expect(retrievedWordlist).to.have.property('banned');
|
||||
expect(retrievedWordlist.banned).to.have.members([]);
|
||||
expect(retrievedWordlist).to.have.property('suspect');
|
||||
expect(retrievedWordlist.suspect).to.have.members([]);
|
||||
} else {
|
||||
if (res.data.updateWordlist && res.data.updateWordlist.errors) {
|
||||
console.error(res.data.updateWordlist.errors);
|
||||
}
|
||||
expect(res.data.updateWordlist).to.be.null;
|
||||
|
||||
const {wordlist: retrievedWordlist} = await SettingsService.retrieve();
|
||||
expect(retrievedWordlist).to.have.property('banned');
|
||||
expect(retrievedWordlist.banned).to.have.members(wordlist.banned);
|
||||
expect(retrievedWordlist).to.have.property('suspect');
|
||||
expect(retrievedWordlist.suspect).to.have.members(wordlist.suspect);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -74,12 +74,10 @@ describe('graph.queries.asset', () => {
|
||||
|
||||
expect(asset.nodes).to.have.length(2);
|
||||
expect(asset.hasNextPage).to.be.false;
|
||||
expect(asset.nodes[0]).to.have.property('id', comments[1].id);
|
||||
expect(asset.nodes[1]).to.have.property('id', comments[0].id);
|
||||
expect(asset.nodes.map(({id}) => id)).to.have.members(comments.slice(0, 2).map(({id}) => id));
|
||||
expect(otherAsset.nodes).to.have.length(2);
|
||||
expect(otherAsset.hasNextPage).to.be.false;
|
||||
expect(otherAsset.nodes[0]).to.have.property('id', comments[3].id);
|
||||
expect(otherAsset.nodes[1]).to.have.property('id', comments[2].id);
|
||||
expect(otherAsset.nodes.map(({id}) => id)).to.have.members(comments.slice(2, 4).map(({id}) => id));
|
||||
|
||||
for (let node of asset.nodes) {
|
||||
for (let otherNode of otherAsset.nodes) {
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
const {graphql} = require('graphql');
|
||||
|
||||
const schema = require('../../../../graph/schema');
|
||||
const Context = require('../../../../graph/context');
|
||||
const SettingsService = require('../../../../services/settings');
|
||||
const UserModel = require('../../../../models/user');
|
||||
|
||||
const {expect} = require('chai');
|
||||
|
||||
const defaultSettings = {
|
||||
organizationName: 'The Coral Project'
|
||||
};
|
||||
|
||||
describe('graph.queries.settings', () => {
|
||||
let settings;
|
||||
beforeEach(async () => {
|
||||
settings = await SettingsService.init(defaultSettings);
|
||||
});
|
||||
|
||||
const QUERY = `
|
||||
{
|
||||
settings {
|
||||
moderation
|
||||
requireEmailConfirmation
|
||||
infoBoxEnable
|
||||
infoBoxContent
|
||||
questionBoxEnable
|
||||
questionBoxContent
|
||||
premodLinksEnable
|
||||
questionBoxIcon
|
||||
autoCloseStream
|
||||
customCssUrl
|
||||
closedTimeout
|
||||
closedMessage
|
||||
charCountEnable
|
||||
charCount
|
||||
organizationName
|
||||
wordlist {
|
||||
banned
|
||||
suspect
|
||||
}
|
||||
domains {
|
||||
whitelist
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
describe('context with different user roles', () => {
|
||||
|
||||
const BLACKLISTED_PROPERTIES = [
|
||||
'premodLinksEnable',
|
||||
'autoCloseStream',
|
||||
'wordlist',
|
||||
'domains',
|
||||
];
|
||||
|
||||
[
|
||||
{bl: true},
|
||||
{bl: true, roles: []},
|
||||
{bl: false, roles: ['ADMIN']},
|
||||
{bl: false, roles: ['ADMIN', 'MODERATOR']},
|
||||
{bl: false, roles: ['MODERATOR']},
|
||||
].forEach(({bl, roles}) => {
|
||||
it(roles && roles.length > 0 ? roles.join(', ') : '<None>', async () => {
|
||||
let user;
|
||||
if (roles != null) {
|
||||
user = new UserModel({roles});
|
||||
}
|
||||
|
||||
const ctx = new Context({user});
|
||||
|
||||
const res = await graphql(schema, QUERY, {}, ctx);
|
||||
if (res.errors) {
|
||||
console.error(res.errors);
|
||||
}
|
||||
|
||||
expect(res.errors).to.be.empty;
|
||||
expect(res.data.settings).to.be.object;
|
||||
Object.keys(res.data.settings).forEach((key) => {
|
||||
if (bl && BLACKLISTED_PROPERTIES.includes(key)) {
|
||||
expect(res.data.settings).to.have.property(key, null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof settings[key] !== 'object') {
|
||||
expect(res.data.settings).to.have.property(key, settings[key]);
|
||||
} else {
|
||||
expect(res.data.settings).to.have.property(key);
|
||||
expect(res.data.settings[key]).to.not.be.null;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
@@ -3,7 +3,7 @@ const passport = require('../../../passport');
|
||||
const app = require('../../../../../app');
|
||||
|
||||
const chai = require('chai');
|
||||
chai.should();
|
||||
chai.use(require('chai-as-promised'));
|
||||
chai.use(require('chai-http'));
|
||||
const expect = chai.expect;
|
||||
|
||||
@@ -13,139 +13,125 @@ const SettingsService = require('../../../../../services/settings');
|
||||
|
||||
describe('/api/v1/assets', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
const settings = {id: '1', moderation: 'PRE', domains: {whitelist: ['test.com']}};
|
||||
|
||||
return SettingsService.init(settings).then(() => {
|
||||
return AssetModel.create([
|
||||
{
|
||||
url: 'https://coralproject.net/news/asset1',
|
||||
title: 'Asset 1',
|
||||
description: 'term1',
|
||||
closedAt: Date.now()
|
||||
},
|
||||
{
|
||||
url: 'https://coralproject.net/news/asset2',
|
||||
title: 'Asset 2',
|
||||
description: 'term2',
|
||||
closedAt: null
|
||||
}
|
||||
]);
|
||||
});
|
||||
await SettingsService.init(settings);
|
||||
|
||||
await AssetModel.create([
|
||||
{
|
||||
url: 'https://coralproject.net/news/asset1',
|
||||
title: 'Asset 1',
|
||||
description: 'term1',
|
||||
closedAt: Date.now()
|
||||
},
|
||||
{
|
||||
url: 'https://coralproject.net/news/asset2',
|
||||
title: 'Asset 2',
|
||||
description: 'term2',
|
||||
closedAt: null
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
describe('#get', () => {
|
||||
|
||||
it('should return all assets without a search query', () => {
|
||||
return chai.request(app)
|
||||
it('should return all assets without a search query', async () => {
|
||||
const res = await chai.request(app)
|
||||
.get('/api/v1/assets')
|
||||
.set(passport.inject({roles: ['ADMIN']}))
|
||||
.then((res) => {
|
||||
const body = res.body;
|
||||
.set(passport.inject({roles: ['ADMIN']}));
|
||||
|
||||
expect(body).to.have.property('count', 2);
|
||||
expect(body).to.have.property('result');
|
||||
const body = res.body;
|
||||
|
||||
const assets = body.result;
|
||||
expect(body).to.have.property('count', 2);
|
||||
expect(body).to.have.property('result');
|
||||
|
||||
expect(assets).to.have.length(2);
|
||||
});
|
||||
const assets = body.result;
|
||||
|
||||
expect(assets).to.have.length(2);
|
||||
});
|
||||
|
||||
it('should return assets that we search for', () => {
|
||||
return chai.request(app)
|
||||
it('should return assets that we search for', async () => {
|
||||
const res = await chai.request(app)
|
||||
.get('/api/v1/assets?search=term2')
|
||||
.set(passport.inject({roles: ['ADMIN']}))
|
||||
.then((res) => {
|
||||
const body = res.body;
|
||||
.set(passport.inject({roles: ['ADMIN']}));
|
||||
|
||||
expect(body).to.have.property('count', 1);
|
||||
expect(body).to.have.property('result');
|
||||
const body = res.body;
|
||||
|
||||
const assets = body.result;
|
||||
expect(body).to.have.property('count', 1);
|
||||
expect(body).to.have.property('result');
|
||||
|
||||
expect(assets).to.have.length(1);
|
||||
const assets = body.result;
|
||||
|
||||
const asset = assets[0];
|
||||
expect(assets).to.have.length(1);
|
||||
|
||||
expect(asset).to.have.property('url', 'https://coralproject.net/news/asset2');
|
||||
expect(asset).to.have.property('title', 'Asset 2');
|
||||
});
|
||||
const asset = assets[0];
|
||||
|
||||
expect(asset).to.have.property('url', 'https://coralproject.net/news/asset2');
|
||||
expect(asset).to.have.property('title', 'Asset 2');
|
||||
});
|
||||
|
||||
it('should not return assets that we do not search for', () => {
|
||||
return chai.request(app)
|
||||
it('should not return assets that we do not search for', async () => {
|
||||
const res = await chai.request(app)
|
||||
.get('/api/v1/assets?search=term3')
|
||||
.set(passport.inject({roles: ['ADMIN']}))
|
||||
.then((res) => {
|
||||
const body = res.body;
|
||||
.set(passport.inject({roles: ['ADMIN']}));
|
||||
const body = res.body;
|
||||
|
||||
expect(body).to.have.property('count', 0);
|
||||
expect(body).to.have.property('result');
|
||||
expect(body).to.have.property('count', 0);
|
||||
expect(body).to.have.property('result');
|
||||
|
||||
expect(body.result).to.be.empty;
|
||||
});
|
||||
expect(body.result).to.be.empty;
|
||||
});
|
||||
|
||||
it('should return only closed assets', () => {
|
||||
return chai.request(app)
|
||||
it('should return only closed assets', async () => {
|
||||
const res = await chai.request(app)
|
||||
.get('/api/v1/assets?filter=closed')
|
||||
.set(passport.inject({roles: ['ADMIN']}))
|
||||
.then((res) => {
|
||||
const body = res.body;
|
||||
.set(passport.inject({roles: ['ADMIN']}));
|
||||
const body = res.body;
|
||||
|
||||
expect(body).to.have.property('count', 1);
|
||||
expect(body).to.have.property('result');
|
||||
expect(body).to.have.property('count', 1);
|
||||
expect(body).to.have.property('result');
|
||||
|
||||
const assets = body.result;
|
||||
const assets = body.result;
|
||||
|
||||
expect(assets[0]).to.have.property('title', 'Asset 1');
|
||||
});
|
||||
expect(assets[0]).to.have.property('title', 'Asset 1');
|
||||
});
|
||||
|
||||
it('should return only opened assets', () => {
|
||||
return chai.request(app)
|
||||
it('should return only opened assets', async () => {
|
||||
const res = await chai.request(app)
|
||||
.get('/api/v1/assets?filter=open')
|
||||
.set(passport.inject({roles: ['ADMIN']}))
|
||||
.then((res) => {
|
||||
const body = res.body;
|
||||
.set(passport.inject({roles: ['ADMIN']}));
|
||||
const body = res.body;
|
||||
|
||||
expect(body).to.have.property('count', 1);
|
||||
expect(body).to.have.property('result');
|
||||
expect(body).to.have.property('count', 1);
|
||||
expect(body).to.have.property('result');
|
||||
|
||||
const assets = body.result;
|
||||
const assets = body.result;
|
||||
|
||||
expect(assets[0]).to.have.property('title', 'Asset 2');
|
||||
});
|
||||
expect(assets[0]).to.have.property('title', 'Asset 2');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('#put', () => {
|
||||
it('should close the asset', function() {
|
||||
it('should close the asset', async () => {
|
||||
|
||||
const today = Date.now();
|
||||
|
||||
return AssetsService.findOrCreateByUrl('http://test.com')
|
||||
.then((asset) => {
|
||||
expect(asset).to.have.property('isClosed', false);
|
||||
expect(asset).to.have.property('closedAt', null);
|
||||
const asset = await AssetsService.findOrCreateByUrl('http://test.com');
|
||||
expect(asset).to.have.property('isClosed', false);
|
||||
expect(asset).to.have.property('closedAt', null);
|
||||
|
||||
return chai.request(app)
|
||||
.put(`/api/v1/assets/${asset.id}/status`)
|
||||
.set(passport.inject({roles: ['ADMIN']}))
|
||||
.send({closedAt: today});
|
||||
})
|
||||
.then((res) => {
|
||||
const res = await chai.request(app)
|
||||
.put(`/api/v1/assets/${asset.id}/status`)
|
||||
.set(passport.inject({roles: ['ADMIN']}))
|
||||
.send({closedAt: today});
|
||||
|
||||
expect(res).to.have.status(204);
|
||||
expect(res).to.have.status(204);
|
||||
|
||||
return AssetsService.findByUrl('http://test.com');
|
||||
})
|
||||
.then((asset) => {
|
||||
expect(asset).to.have.property('isClosed', true);
|
||||
expect(asset).to.have.property('closedAt').and.to.not.equal(null);
|
||||
});
|
||||
const closedAsset = await AssetsService.findByUrl('http://test.com');
|
||||
expect(closedAsset).to.have.property('isClosed', true);
|
||||
expect(closedAsset).to.have.property('closedAt').and.to.not.equal(null);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user