[CORL-409] Prevent users from ignoring staff members (#2355)

* Throw error if user tries to ignore a staff member

Throws a UserCannotBeIgnoredError if a user tries to ignore
a user who is a staff member. A staff member in this case is
considered anyone who has a role of staff, moderator, or admin.

CORL-409

* Prevent users from ignoring staff in the user info popover

Creates the staff roles in a constant next to the user model.
Uses this to add a computed property to the user resolver.

CORL-409

* Remove unnecessary async declaration from userIsStaff helper function

CORL-409

* Specify ignoreable on users in client test fixtures

Allows the tests to pass for the required computed property
of ignoreable that is computed by whether a user is a staff
member or not.

CORL-409

* Update more fixtures with ignoreable property on mocked users/commenters

CORL-409

* Consolidate ignore-able calculation into re-usable helper methods

Re-use the logic for whether a role is a staff member to
clearly define when a user is ignore-able or not across the
business logic.

CORL-409

* Set the ignoreable optimisticResponse on comment mutations

We have set the ignoreable value in the graphQL schema, so now
the optimisticResponses are looking for a default value to use
until the data result arrives. Put to false since the ignoreable
value is set on our author, we likely don't want to ignore ourselves.

CORL-409
This commit is contained in:
Nick Funk
2019-06-13 11:24:50 -06:00
committed by Wyatt Johnson
parent 3947b143cb
commit 662f5ce314
14 changed files with 78 additions and 6 deletions
+7
View File
@@ -289,6 +289,7 @@ export const users = {
username: "Markus",
email: "markus@test.com",
role: GQLUSER_ROLE.ADMIN,
ignoreable: false,
},
],
baseUser
@@ -300,6 +301,7 @@ export const users = {
username: "Lukas",
email: "lukas@test.com",
role: GQLUSER_ROLE.MODERATOR,
ignoreable: false,
},
],
baseUser
@@ -311,6 +313,7 @@ export const users = {
username: "Huy",
email: "huy@test.com",
role: GQLUSER_ROLE.STAFF,
ignoreable: false,
},
],
baseUser
@@ -322,18 +325,21 @@ export const users = {
username: "Isabelle",
email: "isabelle@test.com",
role: GQLUSER_ROLE.COMMENTER,
ignoreable: true,
},
{
id: "user-commenter-1",
username: "Ngoc",
email: "ngoc@test.com",
role: GQLUSER_ROLE.COMMENTER,
ignoreable: true,
},
{
id: "user-commenter-2",
username: "Max",
email: "max@test.com",
role: GQLUSER_ROLE.COMMENTER,
ignoreable: true,
},
],
baseUser
@@ -344,6 +350,7 @@ export const users = {
username: "Ingrid",
email: "ingrid@test.com",
role: GQLUSER_ROLE.COMMENTER,
ignoreable: true,
status: {
current: [GQLUSER_STATUS.BANNED],
ban: { active: true },
@@ -178,6 +178,7 @@ function commit(
id: viewer.id,
username: viewer.username,
createdAt: viewer.createdAt,
ignoreable: false,
},
body: input.body,
revision: {
@@ -30,7 +30,8 @@ export const UserPopoverOverviewContainer: FunctionComponent<Props> = ({
const canIgnore =
viewer &&
viewer.id !== user.id &&
viewer.ignoredUsers.every(u => u.id !== user.id);
viewer.ignoredUsers.every(u => u.id !== user.id) &&
user.ignoreable;
return (
<HorizontalGutter spacing={3} className={styles.root}>
<HorizontalGutter spacing={2}>
@@ -73,6 +74,7 @@ const enhanced = withFragmentContainer<Props>({
id
username
createdAt
ignoreable
}
`,
})(UserPopoverOverviewContainer);
@@ -148,6 +148,7 @@ function commit(
id: viewer.id,
username: viewer.username,
createdAt: viewer.createdAt,
ignoreable: false,
},
revision: {
id: uuidGenerator(),
+5
View File
@@ -104,21 +104,25 @@ export const commenters = createFixtures<GQLUser>(
id: "user-0",
username: "Markus",
role: GQLUSER_ROLE.COMMENTER,
ignoreable: true,
},
{
id: "user-1",
username: "Lukas",
role: GQLUSER_ROLE.COMMENTER,
ignoreable: true,
},
{
id: "user-2",
username: "Isabelle",
role: GQLUSER_ROLE.COMMENTER,
ignoreable: true,
},
{
id: "user-3",
username: "Markus",
role: GQLUSER_ROLE.COMMENTER,
ignoreable: true,
},
],
baseUser
@@ -354,6 +358,7 @@ export const moderators = createFixtures<GQLUser>(
id: "me-as-moderator",
username: "Moderator",
role: GQLUSER_ROLE.MODERATOR,
ignoreable: false,
},
],
baseUser
+7
View File
@@ -231,6 +231,13 @@ export enum ERROR_CODES {
*/
USER_BANNED = "USER_BANNED",
/**
* USER_CANNOT_BE_IGNORED is returned when the user attempts to ignore
* a user that is not allowed to be ignored. This is usually because the
* user is staff member.
*/
USER_CANNOT_BE_IGNORED = "USER_CANNOT_BE_IGNORED",
/**
* INTEGRATION_DISABLED is returned when an operation is attempted against an
* integration that has been disabled.
+9
View File
@@ -573,6 +573,15 @@ export class UserSuspended extends CoralError {
}
}
export class UserCannotBeIgnoredError extends CoralError {
constructor(userID: string) {
super({
code: ERROR_CODES.USER_CANNOT_BE_IGNORED,
context: { pub: { userID } },
});
}
}
export class PasswordResetTokenExpired extends CoralError {
constructor(reason: string, cause?: Error) {
super({
+1
View File
@@ -27,6 +27,7 @@ export const ERROR_TRANSLATIONS: Record<ERROR_CODES, string> = {
TOKEN_NOT_FOUND: "error-tokenNotFound",
USER_NOT_ENTITLED: "error-userNotEntitled",
USER_NOT_FOUND: "error-userNotFound",
USER_CANNOT_BE_IGNORED: "error-userCannotBeIgnored",
USERNAME_ALREADY_SET: "error-usernameAlreadySet",
USERNAME_CONTAINS_INVALID_CHARACTERS:
"error-usernameContainsInvalidCharacters",
@@ -6,6 +6,7 @@ import {
GQLUserTypeResolver,
} from "coral-server/graph/tenant/schema/__generated__/types";
import * as user from "coral-server/models/user";
import { roleIsStaff } from "coral-server/models/user/helpers";
import { UserStatusInput } from "./UserStatus";
import { getRequestedFields } from "./util";
@@ -41,4 +42,5 @@ export const User: GQLUserTypeResolver<user.User> = {
}),
ignoredUsers: ({ ignoredUsers }, input, ctx, info) =>
maybeLoadOnlyIgnoredUserID(ctx, info, ignoredUsers),
ignoreable: ({ role }) => !roleIsStaff(role),
};
@@ -1456,6 +1456,13 @@ type User {
permit: [SUSPENDED, BANNED]
)
"""
ignoreable is a computed property based on the
user's role. Typically, users with elevated privileges
aren't allowed to be ignored.
"""
ignoreable: Boolean!
"""
comments are the comments written by the User.
"""
+7
View File
@@ -0,0 +1,7 @@
import { GQLUSER_ROLE } from "coral-server/graph/tenant/schema/__generated__/types";
export const STAFF_ROLES = [
GQLUSER_ROLE.ADMIN,
GQLUSER_ROLE.MODERATOR,
GQLUSER_ROLE.STAFF,
];
+16
View File
@@ -0,0 +1,16 @@
import { GQLUSER_ROLE } from "coral-server/graph/tenant/schema/__generated__/types";
import { STAFF_ROLES } from "coral-server/models/user/constants";
import { User } from ".";
export function roleIsStaff(role: GQLUSER_ROLE) {
if (STAFF_ROLES.includes(role)) {
return true;
}
return false;
}
export function userIsStaff(user: User) {
return roleIsStaff(user.role);
}
@@ -25,17 +25,17 @@ import {
} from "coral-server/graph/tenant/schema/__generated__/types";
import { getLocalProfile, hasLocalProfile } from "coral-server/helpers/users";
import logger from "coral-server/logger";
import {
Connection,
ConnectionInput,
resolveConnection,
} from "coral-server/models/helpers/connection";
import {
createConnectionOrderVariants,
createIndexFactory,
} from "coral-server/models/helpers/indexing";
import Query from "coral-server/models/helpers/query";
import { TenantResource } from "coral-server/models/tenant";
import {
Connection,
ConnectionInput,
resolveConnection,
} from "./helpers/connection";
function collection(mongo: Db) {
return mongo.collection<Readonly<User>>("users");
+7
View File
@@ -9,6 +9,7 @@ import {
TokenNotFoundError,
UserAlreadyBannedError,
UserAlreadySuspendedError,
UserCannotBeIgnoredError,
UsernameAlreadySetError,
UserNotFoundError,
} from "coral-server/errors";
@@ -39,6 +40,7 @@ import {
updateUserUsername,
User,
} from "coral-server/models/user";
import { userIsStaff } from "coral-server/models/user/helpers";
import { MailerQueue } from "coral-server/queue/tasks/mailer";
import { JWTSigningConfig, signPATString } from "coral-server/services/jwt";
@@ -585,6 +587,11 @@ export async function ignore(
throw new UserNotFoundError(userID);
}
const userToBeIgnoredIsStaff = userIsStaff(targetUser);
if (userToBeIgnoredIsStaff) {
throw new UserCannotBeIgnoredError(userID);
}
// TODO: extract function
if (user.ignoredUsers && user.ignoredUsers.some(u => u.id === userID)) {
// TODO: improve error