[CORL-1249] Add "warn" status (#3100)

* schema and backend for warning users

* allow warning and warning acknowledgement

* include warning actions in user history drawer

* fix fixture

* add copy to warn modal

* style warning on stream side

* localize strings

* update warning form

* fix fixtures

* copy updates

* clean up error messaging

* userefetch for users

* ensure user status is refreshed on warn error

* add details about warning to user status details

* update copy for unacknowledged warning

* fix merge conflict

* update copy

* remove unused code

* clean up warning css

* sort imports

* fix copy in comments

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
This commit is contained in:
Tessa Thornton
2020-08-14 15:44:35 +00:00
committed by GitHub
co-authored by kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
parent d001fcd456
commit abfcdcc933
45 changed files with 1651 additions and 87 deletions
+16
View File
@@ -5,11 +5,13 @@ import {
UserBanned,
UserForbiddenError,
UserSuspended,
UserWarned,
} from "coral-server/errors";
import GraphContext from "coral-server/graph/context";
import {
consolidateUserStatus,
consolidateUserSuspensionStatus,
consolidateUserWarningStatus,
User,
} from "coral-server/models/user";
import { canModerateUnscoped } from "coral-server/models/user/helpers";
@@ -59,6 +61,10 @@ function calculateAuthConditions(
conditions.push(GQLUSER_AUTH_CONDITIONS.PENDING_DELETION);
}
if (status.warning && status.warning.active) {
conditions.push(GQLUSER_AUTH_CONDITIONS.WARNED);
}
return conditions.sort();
}
@@ -92,6 +98,16 @@ const auth: DirectiveResolverFn<
throw new UserBanned(user.id, resource, info.operation.operation);
}
if (conditions.includes(GQLUSER_AUTH_CONDITIONS.WARNED)) {
const warning = consolidateUserWarningStatus(user.status.warning);
throw new UserWarned(
user.id,
warning.message,
resource,
info.operation.operation
);
}
if (conditions.includes(GQLUSER_AUTH_CONDITIONS.SUSPENDED)) {
const status = consolidateUserSuspensionStatus(
user.status.suspension,
+18
View File
@@ -3,6 +3,7 @@ import GraphContext from "coral-server/graph/context";
import { mapFieldsetToErrorCodes } from "coral-server/graph/errors";
import { User } from "coral-server/models/user";
import {
acknowledgeWarning,
addModeratorNote,
ban,
cancelAccountDeletion,
@@ -15,6 +16,7 @@ import {
removeIgnore,
removePremod,
removeSuspension,
removeWarning,
requestAccountDeletion,
requestCommentsDownload,
requestUserCommentsDownload,
@@ -32,6 +34,7 @@ import {
updateRole,
updateUsername,
updateUsernameByID,
warn,
} from "coral-server/services/users";
import { invite } from "coral-server/services/users/auth/invite";
import { deleteUser } from "coral-server/services/users/delete";
@@ -51,6 +54,7 @@ import {
GQLRemoveUserBanInput,
GQLRemoveUserIgnoreInput,
GQLRemoveUserSuspensionInput,
GQLRemoveUserWarningInput,
GQLRequestAccountDeletionInput,
GQLRequestCommentsDownloadInput,
GQLRequestUserCommentsDownloadInput,
@@ -68,6 +72,7 @@ import {
GQLUpdateUsernameInput,
GQLUpdateUserRoleInput,
GQLUpdateUserUsernameInput,
GQLWarnUserInput,
} from "coral-server/graph/schema/__generated__/types";
import { WithoutMutationID } from "./util";
@@ -258,6 +263,19 @@ export const Users = (ctx: GraphContext) => ({
rejectExistingComments,
ctx.now
),
warn: async (input: GQLWarnUserInput) =>
warn(
ctx.mongo,
ctx.tenant,
ctx.user!,
input.userID,
input.message,
ctx.now
),
removeWarning: async (input: GQLRemoveUserWarningInput) =>
removeWarning(ctx.mongo, ctx.tenant, ctx.user!, input.userID, ctx.now),
acknowledgeWarning: async () =>
acknowledgeWarning(ctx.mongo, ctx.tenant, ctx.user!.id, ctx.now),
premodUser: async (input: GQLPremodUserInput) =>
premod(ctx.mongo, ctx.tenant, ctx.user!, input.userID, ctx.now),
suspend: async (input: GQLSuspendUserInput) =>
@@ -199,6 +199,18 @@ export const Mutation: Required<GQLMutationTypeResolver<void>> = {
user: await ctx.mutators.Users.removeBan(input),
clientMutationId: input.clientMutationId,
}),
warnUser: async (source, { input }, ctx) => ({
user: await ctx.mutators.Users.warn(input),
clientMutationId: input.clientMutationId,
}),
removeUserWarning: async (source, { input }, ctx) => ({
user: await ctx.mutators.Users.removeWarning(input),
clientMutationId: input.clientMutationId,
}),
acknowledgeWarning: async (source, { input }, ctx) => ({
user: await ctx.mutators.Users.acknowledgeWarning(),
clientMutationId: input.clientMutationId,
}),
suspendUser: async (source, { input }, ctx) => ({
user: await ctx.mutators.Users.suspend(input),
clientMutationId: input.clientMutationId,
@@ -9,6 +9,7 @@ import { BanStatusInput } from "./BanStatus";
import { PremodStatusInput } from "./PremodStatus";
import { SuspensionStatusInput } from "./SuspensionStatus";
import { UsernameStatusInput } from "./UsernameStatus";
import { WarningStatusInput } from "./WarningStatus";
export type UserStatusInput = user.UserStatus & {
userID: string;
@@ -36,6 +37,11 @@ export const UserStatus: Required<GQLUserStatusTypeResolver<
statuses.push(GQLUSER_STATUS.PREMOD);
}
// If they have a warning, then mark it.
if (consolidatedStatus.warning.active) {
statuses.push(GQLUSER_STATUS.WARNED);
}
// If no other statuses were applied, then apply the active status.
if (statuses.length === 0) {
statuses.push(GQLUSER_STATUS.ACTIVE);
@@ -59,4 +65,8 @@ export const UserStatus: Required<GQLUserStatusTypeResolver<
...user.consolidateUserPremodStatus(premod),
userID,
}),
warning: ({ warning, userID }): WarningStatusInput => ({
...user.consolidateUserWarningStatus(warning),
userID,
}),
};
@@ -0,0 +1,16 @@
import * as user from "coral-server/models/user";
import { GQLWarningStatusTypeResolver } from "coral-server/graph/schema/__generated__/types";
export type WarningStatusInput = user.ConsolidatedWarningStatus & {
userID: string;
};
export const WarningStatus: Required<GQLWarningStatusTypeResolver<
WarningStatusInput
>> = {
active: ({ active }) => active,
message: ({ message }) => message,
history: ({ history, userID }) =>
history.map((status) => ({ ...status, userID })),
};
@@ -0,0 +1,15 @@
import * as user from "coral-server/models/user";
import { GQLWarningStatusHistoryTypeResolver } from "coral-server/graph/schema/__generated__/types";
export const WarningStatusHistory: Required<GQLWarningStatusHistoryTypeResolver<
user.WarningStatusHistory
>> = {
active: ({ active }) => active,
acknowledgedAt: ({ acknowledgedAt }) => acknowledgedAt,
createdBy: ({ createdBy }, input, ctx) => {
return ctx.loaders.Users.user.load(createdBy);
},
createdAt: ({ createdAt }) => createdAt,
message: ({ message }) => message,
};
+4
View File
@@ -60,6 +60,8 @@ import { UserModerationScopes } from "./UserModerationScopes";
import { UsernameHistory } from "./UsernameHistory";
import { UsernameStatus } from "./UsernameStatus";
import { UserStatus } from "./UserStatus";
import { WarningStatus } from "./WarningStatus";
import { WarningStatusHistory } from "./WarningStatusHistory";
import { WebhookEndpoint } from "./WebhookEndpoint";
import { YouTubeMediaConfiguration } from "./YouTubeMediaConfiguration";
@@ -123,6 +125,8 @@ const Resolvers: GQLResolver = {
UsernameHistory,
UsernameStatus,
UserStatus,
WarningStatus,
WarningStatusHistory,
WebhookEndpoint,
YouTubeMediaConfiguration,
};
+228 -31
View File
@@ -34,6 +34,11 @@ enum USER_AUTH_CONDITIONS {
remain after being deleted.
"""
PENDING_DELETION
"""
WARNED
"""
WARNED
}
"""
@@ -1921,7 +1926,7 @@ type BanStatus {
@auth(
roles: [ADMIN, MODERATOR]
userIDField: "userID"
permit: [SUSPENDED, BANNED, PENDING_DELETION]
permit: [SUSPENDED, BANNED, PENDING_DELETION, WARNED]
)
"""
@@ -2001,7 +2006,7 @@ type SuspensionStatus {
@auth(
roles: [ADMIN, MODERATOR]
userIDField: "userID"
permit: [SUSPENDED, BANNED, PENDING_DELETION]
permit: [SUSPENDED, BANNED, PENDING_DELETION, WARNED]
)
"""
@@ -2011,7 +2016,7 @@ type SuspensionStatus {
@auth(
roles: [ADMIN, MODERATOR]
userIDField: "userID"
permit: [SUSPENDED, BANNED, PENDING_DELETION]
permit: [SUSPENDED, BANNED, PENDING_DELETION, WARNED]
)
"""
@@ -2020,6 +2025,50 @@ type SuspensionStatus {
history: [SuspensionStatusHistory!]! @auth(roles: [ADMIN, MODERATOR])
}
type WarningStatusHistory {
"""
active when true, indicates that the given user has been warned but has not acknowledged the warning.
"""
active: Boolean!
"""
createdBy is the user that warned the commenter
"""
createdBy: User!
"""
createdAt is the time the user was warned
"""
createdAt: Time!
"""
acknowledgedAt is the time the commenter acknowledged the warning. if `null` then the warning
has not been acknowledged
"""
acknowledgedAt: Time
"""
message is the custom message sent to the commenter
"""
message: String!
}
type WarningStatus {
"""
active when true, indicates that the given user has been warned but has not acknowledged it.
"""
active: Boolean!
@auth(
roles: [ADMIN, MODERATOR]
userIDField: "userID"
permit: [SUSPENDED, BANNED, PENDING_DELETION, WARNED]
)
message: String
history: [WarningStatusHistory!]! @auth(roles: [ADMIN, MODERATOR])
}
type PremodStatusHistory {
"""
active when true, indicates that the given user is premodded.
@@ -2057,7 +2106,7 @@ type UsernameHistory {
@auth(
roles: [ADMIN, MODERATOR]
userIDField: "userID"
permit: [SUSPENDED, BANNED, PENDING_DELETION]
permit: [SUSPENDED, BANNED, PENDING_DELETION, WARNED]
)
"""
@@ -2072,7 +2121,7 @@ type UsernameHistory {
@auth(
roles: [ADMIN, MODERATOR]
userIDField: "userID"
permit: [SUSPENDED, BANNED, PENDING_DELETION]
permit: [SUSPENDED, BANNED, PENDING_DELETION, WARNED]
)
}
@@ -2094,7 +2143,7 @@ type UserStatus {
@auth(
roles: [ADMIN, MODERATOR]
userIDField: "userID"
permit: [SUSPENDED, BANNED, PENDING_DELETION]
permit: [SUSPENDED, BANNED, PENDING_DELETION, WARNED]
)
"""
@@ -2117,6 +2166,11 @@ type UserStatus {
premod stores the user premod status as well as the history of changes.
"""
premod: PremodStatus! @auth(roles: [ADMIN, MODERATOR])
"""
warning stores the user warning status as well as the history of warnings
"""
warning: WarningStatus!
}
"""
@@ -2143,6 +2197,11 @@ enum USER_STATUS {
PREMOD is used when a User is currently set to require pre-moderation.
"""
PREMOD
"""
WARNED is used when a user has been warned about behaviour and has not acknowledged
"""
WARNED
}
type ModeratorNote {
@@ -2281,7 +2340,14 @@ type User {
@auth(
roles: [ADMIN, MODERATOR]
userIDField: "id"
permit: [MISSING_NAME, MISSING_EMAIL, SUSPENDED, BANNED, PENDING_DELETION]
permit: [
MISSING_NAME
MISSING_EMAIL
SUSPENDED
BANNED
PENDING_DELETION
WARNED
]
)
"""
@@ -2299,7 +2365,7 @@ type User {
@auth(
roles: [ADMIN, MODERATOR]
userIDField: "id"
permit: [SUSPENDED, BANNED, PENDING_DELETION]
permit: [SUSPENDED, BANNED, PENDING_DELETION, WARNED]
)
"""
@@ -2309,7 +2375,14 @@ type User {
@auth(
roles: [ADMIN, MODERATOR]
userIDField: "id"
permit: [MISSING_NAME, MISSING_EMAIL, SUSPENDED, BANNED, PENDING_DELETION]
permit: [
MISSING_NAME
MISSING_EMAIL
SUSPENDED
BANNED
PENDING_DELETION
WARNED
]
)
"""
@@ -2319,7 +2392,14 @@ type User {
@auth(
roles: [ADMIN, MODERATOR]
userIDField: "id"
permit: [MISSING_NAME, MISSING_EMAIL, SUSPENDED, BANNED, PENDING_DELETION]
permit: [
MISSING_NAME
MISSING_EMAIL
SUSPENDED
BANNED
PENDING_DELETION
WARNED
]
)
"""
@@ -2345,7 +2425,7 @@ type User {
@auth(
roles: [ADMIN, MODERATOR]
userIDField: "id"
permit: [SUSPENDED, BANNED, PENDING_DELETION]
permit: [SUSPENDED, BANNED, PENDING_DELETION, WARNED]
)
"""
@@ -2369,7 +2449,10 @@ type User {
sorted by their last comment date.
"""
ongoingDiscussions(limit: Int = 5 @constraint(max: 5)): [Story!]!
@auth(userIDField: "id", permit: [SUSPENDED, BANNED, PENDING_DELETION])
@auth(
userIDField: "id"
permit: [SUSPENDED, BANNED, PENDING_DELETION, WARNED]
)
"""
recentCommentHistory returns recent commenting history by the User.
@@ -2397,7 +2480,7 @@ type User {
@auth(
roles: [ADMIN]
userIDField: "id"
permit: [SUSPENDED, BANNED, PENDING_DELETION]
permit: [SUSPENDED, BANNED, PENDING_DELETION, WARNED]
)
"""
@@ -2407,14 +2490,17 @@ type User {
@auth(
roles: [ADMIN, MODERATOR]
userIDField: "id"
permit: [SUSPENDED, BANNED, PENDING_DELETION]
permit: [SUSPENDED, BANNED, PENDING_DELETION, WARNED]
)
"""
notifications stores the notification settings for the given User.
"""
notifications: UserNotificationSettings!
@auth(userIDField: "id", permit: [SUSPENDED, BANNED, PENDING_DELETION])
@auth(
userIDField: "id"
permit: [SUSPENDED, BANNED, PENDING_DELETION, WARNED]
)
"""
createdAt is the time that the User was created at.
@@ -2429,7 +2515,7 @@ type User {
@auth(
userIDField: "id"
roles: [ADMIN, MODERATOR]
permit: [SUSPENDED, BANNED, PENDING_DELETION]
permit: [SUSPENDED, BANNED, PENDING_DELETION, WARNED]
)
"""
@@ -2440,7 +2526,7 @@ type User {
@auth(
userIDField: "id"
roles: [ADMIN, MODERATOR]
permit: [SUSPENDED, BANNED, PENDING_DELETION]
permit: [SUSPENDED, BANNED, PENDING_DELETION, WARNED]
)
"""
@@ -2450,7 +2536,7 @@ type User {
@auth(
userIDField: "id"
roles: [ADMIN, MODERATOR]
permit: [SUSPENDED, BANNED, PENDING_DELETION]
permit: [SUSPENDED, BANNED, PENDING_DELETION, WARNED]
)
"""
@@ -2461,7 +2547,7 @@ type User {
@auth(
userIDField: "id"
roles: [ADMIN, MODERATOR]
permit: [SUSPENDED, BANNED, PENDING_DELETION]
permit: [SUSPENDED, BANNED, PENDING_DELETION, WARNED]
)
"""
@@ -2474,6 +2560,10 @@ type User {
mediaSettings are the user's preferences around media stream behaviour.
"""
mediaSettings: UserMediaSettings
@auth(
userIDField: "id"
permit: [SUSPENDED, BANNED, PENDING_DELETION, WARNED]
)
}
"""
@@ -2896,7 +2986,7 @@ type Comment {
@auth(
roles: [MODERATOR, ADMIN]
userIDField: "author_id"
permit: [SUSPENDED, BANNED, PENDING_DELETION]
permit: [SUSPENDED, BANNED, PENDING_DELETION, WARNED]
)
"""
@@ -6452,15 +6542,89 @@ input UpdateUserRoleInput {
}
type UpdateUserRolePayload {
"""
clientMutationId is required for Relay support.
"""
clientMutationId: String!
"""
user is the possibly modified User.
"""
user: User!
}
##################
# warnUser
##################
input WarnUserInput {
"""
userID is the ID of the User that should have their account banned.
"""
userID: ID!
"""
clientMutationId is required for Relay support.
"""
clientMutationId: String!
"""
message is displayed to the user in the stream
"""
message: String!
}
type WarnUserPayload {
"""
clientMutationId is required for Relay support.
"""
clientMutationId: String!
"""
user is the possibly modified User.
"""
user: User!
}
input RemoveUserWarningInput {
"""
userID is the ID of the User that should be warned.
"""
userID: ID!
"""
clientMutationId is required for Relay support.
"""
clientMutationId: String!
}
type RemoveUserWarningPayload {
"""
clientMutationId is required for Relay support.
"""
clientMutationId: String!
"""
user is the possibly modified User.
"""
user: User!
}
input AcknowledgeWarningInput {
"""
clientMutationId is required for Relay support.
"""
clientMutationId: String!
}
type AcknowledgeWarningPayload {
"""
clientMutationId is required for Relay support.
"""
clientMutationId: String!
"""
user is the possibly modified User.
"""
user: User!
}
##################
@@ -7279,14 +7443,21 @@ type Mutation {
"""
setUsername(input: SetUsernameInput!): SetUsernamePayload!
@auth(
permit: [MISSING_NAME, MISSING_EMAIL, SUSPENDED, BANNED, PENDING_DELETION]
permit: [
MISSING_NAME
MISSING_EMAIL
SUSPENDED
BANNED
PENDING_DELETION
WARNED
]
)
"""
updateUsername will update the users username.
"""
updateUsername(input: UpdateUsernameInput!): UpdateUsernamePayload!
@auth(permit: [SUSPENDED, BANNED, PENDING_DELETION])
@auth(permit: [SUSPENDED, BANNED, PENDING_DELETION, WARNED])
@rate(seconds: 10)
"""
@@ -7295,7 +7466,14 @@ type Mutation {
"""
setEmail(input: SetEmailInput!): SetEmailPayload!
@auth(
permit: [MISSING_NAME, MISSING_EMAIL, SUSPENDED, BANNED, PENDING_DELETION]
permit: [
MISSING_NAME
MISSING_EMAIL
SUSPENDED
BANNED
PENDING_DELETION
WARNED
]
)
"""
@@ -7309,7 +7487,7 @@ type Mutation {
they already have one associated with them.
"""
updatePassword(input: UpdatePasswordInput!): UpdatePasswordPayload!
@auth(permit: [SUSPENDED, BANNED, PENDING_DELETION])
@auth(permit: [SUSPENDED, BANNED, PENDING_DELETION, WARNED])
@rate(seconds: 10)
"""
@@ -7319,7 +7497,7 @@ type Mutation {
requestAccountDeletion(
input: RequestAccountDeletionInput!
): RequestAccountDeletionPayload!
@auth(permit: [SUSPENDED, BANNED])
@auth(permit: [SUSPENDED, BANNED, WARNED])
@rate(seconds: 10)
"""
@@ -7335,7 +7513,7 @@ type Mutation {
cancelAccountDeletion(
input: CancelAccountDeletionInput!
): CancelAccountDeletionPayload!
@auth(permit: [SUSPENDED, BANNED, PENDING_DELETION])
@auth(permit: [SUSPENDED, BANNED, PENDING_DELETION, WARNED])
"""
createToken allows an administrator to create a Token based on the current
@@ -7363,7 +7541,7 @@ type Mutation {
updateEmail will update the current users email address.
"""
updateEmail(input: UpdateEmailInput!): UpdateEmailPayload!
@auth(permit: [SUSPENDED, BANNED, PENDING_DELETION])
@auth(permit: [SUSPENDED, BANNED, PENDING_DELETION, WARNED])
@rate(seconds: 10)
"""
@@ -7373,7 +7551,7 @@ type Mutation {
updateNotificationSettings(
input: UpdateNotificationSettingsInput!
): UpdateNotificationSettingsPayload!
@auth(permit: [SUSPENDED, BANNED, PENDING_DELETION])
@auth(permit: [SUSPENDED, BANNED, PENDING_DELETION, WARNED])
"""
updateUserMediaSettings can be used to update the media preferences for the
@@ -7430,6 +7608,25 @@ type Mutation {
suspendUser(input: SuspendUserInput!): SuspendUserPayload!
@auth(roles: [ADMIN, MODERATOR])
"""
warnUser will warn a user and prevent them from commenting until they acknowledge
"""
warnUser(input: WarnUserInput!): WarnUserPayload!
@auth(roles: [ADMIN, MODERATOR])
"""
removeUserWarning will remove a user warning
"""
removeUserWarning(input: RemoveUserWarningInput!): RemoveUserWarningPayload!
@auth(roles: [ADMIN, MODERATOR])
"""
acknowledgWarning will remove a warning
"""
acknowledgeWarning(
input: AcknowledgeWarningInput!
): AcknowledgeWarningPayload @auth(permit: [WARNED])
"""
removeUserSuspension will remove an active suspension from a User if they have
one.
@@ -7442,14 +7639,14 @@ type Mutation {
ignoreUser will mark the given User as ignored by the current logged in User.
"""
ignoreUser(input: IgnoreUserInput!): IgnoreUserPayload!
@auth(permit: [SUSPENDED, BANNED, PENDING_DELETION])
@auth(permit: [SUSPENDED, BANNED, PENDING_DELETION, WARNED])
"""
removeUserIgnore will remove the given User from the ignored user list from
the current logged in User.
"""
removeUserIgnore(input: RemoveUserIgnoreInput!): RemoveUserIgnorePayload!
@auth(permit: [SUSPENDED, BANNED, PENDING_DELETION])
@auth(permit: [SUSPENDED, BANNED, PENDING_DELETION, WARNED])
"""
requestCommentsDownload allows a user to request to download their comments.
@@ -7457,7 +7654,7 @@ type Mutation {
requestCommentsDownload(
input: RequestCommentsDownloadInput!
): RequestCommentsDownloadPayload!
@auth(permit: [SUSPENDED, BANNED, PENDING_DELETION])
@auth(permit: [SUSPENDED, BANNED, PENDING_DELETION, WARNED])
"""
requestUserCommentsDownload allows a user to request to download their comments.