[CORL-810] Custom Moderation Phases (#2901)

* feat: initial implementation

* feat: renamed fields from mutations

* fix: more renaming to streamline {Key,Secret}->SigningSecret

* feat: introduced WrappedInternalError

* feat: enhanced extern payload, more fetch options

- Added tenant.{id,domain} to extern payload
- Added site.id to the extern payload
- Added response size limit to fetch
- Added new SCRAPE_MAX_RESPONSE_SIZE env var for managing the size of
  responses for scraping

* fix: fixed bug with scrape invocation

* feat: added more queries + mutations

- Added Query.externalModerationPhase
- Added Mutation.createExternalModerationPhase
- Added Mutation.updateExternalModerationPhase
- Added Mutation.enableExternalModerationPhase
- Added Mutation.disableExternalModerationPhase
- Added Mutation.deleteExternalModerationPhase
- Added Mutation.rotateExternalModerationPhaseSigningSecret

* feat: added secret management

* fix: linting

* fix: merge conflict fix

* feat: added UI

* fix: linting

* fix: linting

* fix: updated snapshots

* fix: improved docs

* fix: improved docs

* fix: added locales

* review: improve naming

* review: some review changes

- Switched /moderation/phase to /moderation/phases
- Fixed scrolling
- Fixed redirection

* fix: added scroll timeout for webhooks
This commit is contained in:
Wyatt Johnson
2020-05-14 19:20:35 +00:00
committed by GitHub
parent ceb96dba75
commit ed92f4916d
154 changed files with 5445 additions and 1694 deletions
+1 -1
View File
@@ -19,7 +19,7 @@ import { WebhookQueue } from "coral-server/queue/tasks/webhook";
import { I18n } from "coral-server/services/i18n";
import { JWTSigningConfig } from "coral-server/services/jwt";
import { AugmentedRedis } from "coral-server/services/redis";
import TenantCache from "coral-server/services/tenant/cache";
import { TenantCache } from "coral-server/services/tenant/cache";
import { Request } from "coral-server/types/express";
import loaders from "./loaders";
@@ -6,7 +6,7 @@ import { merge } from "lodash";
import {
CoralError,
InternalDevelopmentError,
InternalError,
WrappedInternalError,
} from "coral-server/errors";
import GraphContext from "coral-server/graph/context";
import { getOriginalError } from "./helpers";
@@ -71,7 +71,7 @@ function getWrappedOriginalError(
);
}
return new InternalError(originalError, "wrapped internal error");
return new WrappedInternalError(originalError, "wrapped internal error");
}
/**
+3 -3
View File
@@ -1,7 +1,7 @@
import DataLoader from "dataloader";
import GraphContext from "coral-server/graph/context";
import { retrieveLastUsedAtTenantSSOKeys } from "coral-server/models/tenant";
import { retrieveLastUsedAtTenantSSOSigningSecrets } from "coral-server/models/tenant";
import { discoverOIDCConfiguration } from "coral-server/services/tenant";
import { GQLDiscoveredOIDCConfiguration } from "coral-server/graph/schema/__generated__/types";
@@ -19,7 +19,7 @@ export default (ctx: GraphContext) => ({
cache: !ctx.disableCaching,
}
),
retrieveSSOKeyLastUsedAt: new DataLoader((kids: string[]) =>
retrieveLastUsedAtTenantSSOKeys(ctx.redis, ctx.tenant.id, kids)
retrieveSSOSigningSecretLastUsedAt: new DataLoader((kids: string[]) =>
retrieveLastUsedAtTenantSSOSigningSecrets(ctx.redis, ctx.tenant.id, kids)
),
});
+6 -5
View File
@@ -211,12 +211,13 @@ export default (ctx: GraphContext) => ({
// This typecast is needed because the custom `ms` format does not return
// the desired `number` type even though that's the only type it can
// output.
scraper.scrape(
scraper.scrape({
url,
(ctx.config.get("scrape_timeout") as unknown) as number,
ctx.tenant.stories.scraping.customUserAgent,
ctx.tenant.stories.scraping.proxyURL
)
timeout: (ctx.config.get("scrape_timeout") as unknown) as number,
size: ctx.config.get("scrape_response_max_size"),
customUserAgent: ctx.tenant.stories.scraping.customUserAgent,
proxyURL: ctx.tenant.stories.scraping.proxyURL,
})
),
{
// Disable caching for the DataLoader if the Context is designed to be
+80 -18
View File
@@ -2,34 +2,46 @@ import GraphContext from "coral-server/graph/context";
import { Tenant } from "coral-server/models/tenant";
import {
createAnnouncement,
createExternalModerationPhase,
createWebhookEndpoint,
deactivateSSOKey,
deactivateSSOSigningSecret,
deleteAnnouncement,
deleteSSOKey,
deleteExternalModerationPhase,
deleteSSOSigningSecret,
deleteWebhookEndpoint,
disableExternalModerationPhase,
disableFeatureFlag,
disableWebhookEndpoint,
enableExternalModerationPhase,
enableFeatureFlag,
enableWebhookEndpoint,
regenerateSSOKey,
rotateSSOKey,
rotateWebhookEndpointSecret,
rotateExternalModerationPhaseSigningSecret,
rotateSSOSigningSecret,
rotateWebhookEndpointSigningSecret,
sendSMTPTest,
update,
updateExternalModerationPhase,
updateWebhookEndpoint,
} from "coral-server/services/tenant";
import {
GQLCreateAnnouncementInput,
GQLCreateExternalModerationPhaseInput,
GQLCreateWebhookEndpointInput,
GQLDeactivateSSOKeyInput,
GQLDeleteSSOKeyInput,
GQLDeactivateSSOSigningSecretInput,
GQLDeleteExternalModerationPhaseInput,
GQLDeleteSSOSigningSecretInput,
GQLDeleteWebhookEndpointInput,
GQLDisableExternalModerationPhaseInput,
GQLDisableWebhookEndpointInput,
GQLEnableExternalModerationPhaseInput,
GQLEnableWebhookEndpointInput,
GQLFEATURE_FLAG,
GQLRotateSSOKeyInput,
GQLRotateWebhookEndpointSecretInput,
GQLRotateExternalModerationPhaseSigningSecretInput,
GQLRotateSSOSigningSecretInput,
GQLRotateWebhookEndpointSigningSecretInput,
GQLUpdateExternalModerationPhaseInput,
GQLUpdateSettingsInput,
GQLUpdateWebhookEndpointInput,
} from "coral-server/graph/schema/__generated__/types";
@@ -50,20 +62,21 @@ export const Settings = ({
input: WithoutMutationID<GQLUpdateSettingsInput>
): Promise<Tenant | null> =>
update(mongo, redis, tenantCache, config, tenant, input.settings),
// DEPRECATED: deprecated in favour of `rotateSSOSigningSecret`, remove in 6.2.0.
regenerateSSOKey: (): Promise<Tenant | null> =>
regenerateSSOKey(mongo, redis, tenantCache, tenant, now),
rotateSSOKey: ({ inactiveIn }: GQLRotateSSOKeyInput) =>
rotateSSOKey(mongo, redis, tenantCache, tenant, inactiveIn, now),
deactivateSSOKey: ({ kid }: GQLDeactivateSSOKeyInput) =>
deactivateSSOKey(mongo, redis, tenantCache, tenant, kid, now),
deleteSSOKey: ({ kid }: GQLDeleteSSOKeyInput) =>
deleteSSOKey(mongo, redis, tenantCache, tenant, kid),
rotateSSOSigningSecret: ({ inactiveIn }: GQLRotateSSOSigningSecretInput) =>
rotateSSOSigningSecret(mongo, redis, tenantCache, tenant, inactiveIn, now),
deleteSSOSigningSecret: ({ kid }: GQLDeleteSSOSigningSecretInput) =>
deleteSSOSigningSecret(mongo, redis, tenantCache, tenant, kid),
deactivateSSOSigningSecret: ({ kid }: GQLDeactivateSSOSigningSecretInput) =>
deactivateSSOSigningSecret(mongo, redis, tenantCache, tenant, kid, now),
enableFeatureFlag: (flag: GQLFEATURE_FLAG) =>
enableFeatureFlag(mongo, redis, tenantCache, tenant, flag),
disableFeatureFlag: (flag: GQLFEATURE_FLAG) =>
disableFeatureFlag(mongo, redis, tenantCache, tenant, flag),
createAnnouncement: (input: GQLCreateAnnouncementInput) =>
createAnnouncement(mongo, redis, tenantCache, tenant, input, now),
createAnnouncement(mongo, redis, tenantCache, tenant, input),
deleteAnnouncement: () =>
deleteAnnouncement(mongo, redis, tenantCache, tenant),
createWebhookEndpoint: (
@@ -92,10 +105,59 @@ export const Settings = ({
deleteWebhookEndpoint: (
input: WithoutMutationID<GQLDeleteWebhookEndpointInput>
) => deleteWebhookEndpoint(mongo, redis, tenantCache, tenant, input.id),
rotateWebhookEndpointSecret: (
input: WithoutMutationID<GQLRotateWebhookEndpointSecretInput>
rotateWebhookEndpointSigningSecret: (
input: WithoutMutationID<GQLRotateWebhookEndpointSigningSecretInput>
) =>
rotateWebhookEndpointSecret(
rotateWebhookEndpointSigningSecret(
mongo,
redis,
tenantCache,
tenant,
input.id,
input.inactiveIn,
now
),
createExternalModerationPhase: (
input: WithoutMutationID<GQLCreateExternalModerationPhaseInput>
) =>
createExternalModerationPhase(
mongo,
redis,
config,
tenantCache,
tenant,
input,
now
),
updateExternalModerationPhase: ({
id,
...input
}: WithoutMutationID<GQLUpdateExternalModerationPhaseInput>) =>
updateExternalModerationPhase(
mongo,
redis,
config,
tenantCache,
tenant,
id,
input
),
enableExternalModerationPhase: (
input: WithoutMutationID<GQLEnableExternalModerationPhaseInput>
) =>
enableExternalModerationPhase(mongo, redis, tenantCache, tenant, input.id),
disableExternalModerationPhase: (
input: WithoutMutationID<GQLDisableExternalModerationPhaseInput>
) =>
disableExternalModerationPhase(mongo, redis, tenantCache, tenant, input.id),
deleteExternalModerationPhase: (
input: WithoutMutationID<GQLDeleteExternalModerationPhaseInput>
) =>
deleteExternalModerationPhase(mongo, redis, tenantCache, tenant, input.id),
rotateExternalModerationPhaseSigningSecret: (
input: WithoutMutationID<GQLRotateExternalModerationPhaseSigningSecretInput>
) =>
rotateExternalModerationPhaseSigningSecret(
mongo,
redis,
tenantCache,
+4 -3
View File
@@ -9,6 +9,7 @@ import {
} from "coral-server/models/action/comment";
import * as comment from "coral-server/models/comment";
import {
getDepth,
getLatestRevision,
hasAncestors,
hasPublishedStatus,
@@ -52,9 +53,9 @@ export const Comment: GQLCommentTypeResolver<comment.Comment> = {
c.revisions.length > 0
? { revision: getLatestRevision(c), comment: c }
: null,
deleted: ({ deletedAt }) => !!deletedAt,
revisionHistory: (c) =>
c.revisions.map((revision) => ({ revision, comment: c })),
deleted: ({ deletedAt }) => !!deletedAt,
editing: ({ revisions, createdAt }, input, ctx) => ({
// When there is more than one body history, then the comment has been
// edited.
@@ -98,8 +99,8 @@ export const Comment: GQLCommentTypeResolver<comment.Comment> = {
}),
viewerActionPresence: (c, input, ctx) =>
ctx.user ? ctx.loaders.Comments.retrieveMyActionPresence.load(c.id) : null,
parentCount: (c) => (hasAncestors(c) ? c.ancestorIDs.length : 0),
depth: (c) => (hasAncestors(c) ? c.ancestorIDs.length : 0),
parentCount: (c) => getDepth(c),
depth: (c) => getDepth(c),
rootParent: (c, input, ctx, info) =>
hasAncestors(c)
? maybeLoadOnlyID(ctx, info, c.ancestorIDs[c.ancestorIDs.length - 1])
@@ -0,0 +1,8 @@
import * as settings from "coral-server/models/settings";
import { GQLExternalModerationPhaseTypeResolver } from "coral-server/graph/schema/__generated__/types";
export const ExternalModerationPhase: GQLExternalModerationPhaseTypeResolver<settings.ExternalModerationPhase> = {
signingSecret: ({ signingSecrets }) =>
signingSecrets[signingSecrets.length - 1],
};
+61 -8
View File
@@ -75,20 +75,21 @@ export const Mutation: Required<GQLMutationTypeResolver<void>> = {
comment: await ctx.mutators.Comments.unfeature(input),
clientMutationId,
}),
// DEPRECATED: deprecated in favour of `rotateSSOSigningSecret`, remove in 6.2.0.
regenerateSSOKey: async (source, { input }, ctx) => ({
settings: await ctx.mutators.Settings.regenerateSSOKey(),
clientMutationId: input.clientMutationId,
}),
rotateSSOKey: async (source, { input }, ctx) => ({
settings: await ctx.mutators.Settings.rotateSSOKey(input),
rotateSSOSigningSecret: async (source, { input }, ctx) => ({
settings: await ctx.mutators.Settings.rotateSSOSigningSecret(input),
clientMutationId: input.clientMutationId,
}),
deactivateSSOKey: async (source, { input }, ctx) => ({
settings: await ctx.mutators.Settings.deactivateSSOKey(input),
deactivateSSOSigningSecret: async (source, { input }, ctx) => ({
settings: await ctx.mutators.Settings.deactivateSSOSigningSecret(input),
clientMutationId: input.clientMutationId,
}),
deleteSSOKey: async (source, { input }, ctx) => ({
settings: await ctx.mutators.Settings.deleteSSOKey(input),
deleteSSOSigningSecret: async (source, { input }, ctx) => ({
settings: await ctx.mutators.Settings.deleteSSOSigningSecret(input),
clientMutationId: input.clientMutationId,
}),
createStory: async (source, { input }, ctx) => ({
@@ -319,12 +320,64 @@ export const Mutation: Required<GQLMutationTypeResolver<void>> = {
endpoint: await ctx.mutators.Settings.deleteWebhookEndpoint(input),
clientMutationId,
}),
rotateWebhookEndpointSecret: async (
rotateWebhookEndpointSigningSecret: async (
source,
{ input: { clientMutationId, ...input } },
ctx
) => ({
endpoint: await ctx.mutators.Settings.rotateWebhookEndpointSecret(input),
endpoint: await ctx.mutators.Settings.rotateWebhookEndpointSigningSecret(
input
),
clientMutationId,
}),
createExternalModerationPhase: async (
source,
{ input: { clientMutationId, ...input } },
ctx
) => ({
...(await ctx.mutators.Settings.createExternalModerationPhase(input)),
clientMutationId,
}),
updateExternalModerationPhase: async (
source,
{ input: { clientMutationId, ...input } },
ctx
) => ({
phase: await ctx.mutators.Settings.updateExternalModerationPhase(input),
clientMutationId,
}),
disableExternalModerationPhase: async (
source,
{ input: { clientMutationId, ...input } },
ctx
) => ({
phase: await ctx.mutators.Settings.disableExternalModerationPhase(input),
clientMutationId,
}),
enableExternalModerationPhase: async (
source,
{ input: { clientMutationId, ...input } },
ctx
) => ({
phase: await ctx.mutators.Settings.enableExternalModerationPhase(input),
clientMutationId,
}),
deleteExternalModerationPhase: async (
source,
{ input: { clientMutationId, ...input } },
ctx
) => ({
phase: await ctx.mutators.Settings.deleteExternalModerationPhase(input),
clientMutationId,
}),
rotateExternalModerationPhaseSigningSecret: async (
source,
{ input: { clientMutationId, ...input } },
ctx
) => ({
phase: await ctx.mutators.Settings.rotateExternalModerationPhaseSigningSecret(
input
),
clientMutationId,
}),
testSMTP: async (source, { input: { clientMutationId } }, ctx) => {
+5
View File
@@ -1,3 +1,4 @@
import { getExternalModerationPhase } from "coral-server/models/settings";
import { getWebhookEndpoint } from "coral-server/models/tenant";
import { GQLQueryTypeResolver } from "coral-server/graph/schema/__generated__/types";
@@ -29,4 +30,8 @@ export const Query: Required<GQLQueryTypeResolver<void>> = {
site: (source, { id }, ctx) => (id ? ctx.loaders.Sites.site.load(id) : null),
webhookEndpoint: (source, { id }, ctx) => getWebhookEndpoint(ctx.tenant, id),
queues: () => ({}),
externalModerationPhase: (source, { id }, ctx) =>
ctx.tenant.integrations.external
? getExternalModerationPhase(ctx.tenant.integrations.external, id)
: null,
};
@@ -1,25 +1,25 @@
import * as settings from "coral-server/models/settings";
import { GQLSSOAuthIntegrationTypeResolver } from "coral-server/graph/schema/__generated__/types";
import { filterFreshSigningSecrets } from "coral-server/models/settings";
function getActiveSSOKey(keys: settings.Secret[]) {
// Any key that has been rotated cannot be the active key.
return keys.find((key) => !key.rotatedAt);
function getActiveSSOSigningSecret(keys: settings.SigningSecret[]) {
return keys.find(filterFreshSigningSecrets());
}
export const SSOAuthIntegration: GQLSSOAuthIntegrationTypeResolver<settings.SSOAuthIntegration> = {
key: ({ keys }) => {
const key = getActiveSSOKey(keys);
if (key) {
return key.secret;
key: ({ signingSecrets }) => {
const signingSecret = getActiveSSOSigningSecret(signingSecrets);
if (signingSecret) {
return signingSecret.secret;
}
return null;
},
keyGeneratedAt: ({ keys }) => {
const key = getActiveSSOKey(keys);
if (key) {
return key.createdAt;
keyGeneratedAt: ({ signingSecrets }) => {
const signingSecret = getActiveSSOSigningSecret(signingSecrets);
if (signingSecret) {
return signingSecret.createdAt;
}
return null;
@@ -1,8 +0,0 @@
import * as settings from "coral-server/models/settings";
import { GQLSecretTypeResolver } from "coral-server/graph/schema/__generated__/types";
export const Secret: GQLSecretTypeResolver<settings.Secret> = {
lastUsedAt: async ({ kid }, args, ctx) =>
ctx.loaders.Auth.retrieveSSOKeyLastUsedAt.load(kid),
};
@@ -0,0 +1,8 @@
import * as settings from "coral-server/models/settings";
import { GQLSigningSecretTypeResolver } from "coral-server/graph/schema/__generated__/types";
export const SigningSecret: GQLSigningSecretTypeResolver<settings.SigningSecret> = {
lastUsedAt: async ({ kid }, args, ctx) =>
ctx.loaders.Auth.retrieveSSOSigningSecretLastUsedAt.load(kid),
};
+4 -2
View File
@@ -20,6 +20,7 @@ import { CommentReplyCreatedPayload } from "./CommentReplyCreatedPayload";
import { CommentRevision } from "./CommentRevision";
import { CommentStatusUpdatedPayload } from "./CommentStatusUpdatedPayload";
import { DisableCommenting } from "./DisableCommenting";
import { ExternalModerationPhase } from "./ExternalModerationPhase";
import { FacebookAuthIntegration } from "./FacebookAuthIntegration";
import { FeatureCommentPayload } from "./FeatureCommentPayload";
import { Flag } from "./Flag";
@@ -39,8 +40,8 @@ import { Queue } from "./Queue";
import { Queues } from "./Queues";
import { RecentCommentHistory } from "./RecentCommentHistory";
import { RejectCommentPayload } from "./RejectCommentPayload";
import { Secret } from "./Secret";
import { Settings } from "./Settings";
import { SigningSecret } from "./SigningSecret";
import { SlackConfiguration } from "./SlackConfiguration";
import { SSOAuthIntegration } from "./SSOAuthIntegration";
import { Story } from "./Story";
@@ -73,6 +74,7 @@ const Resolvers: GQLResolver = {
CommentStatusUpdatedPayload,
Cursor,
DisableCommenting,
ExternalModerationPhase,
FacebookAuthIntegration,
FeatureCommentPayload,
Flag,
@@ -92,7 +94,7 @@ const Resolvers: GQLResolver = {
RecentCommentHistory,
RejectCommentPayload,
SSOAuthIntegration,
Secret,
SigningSecret,
Story,
StorySettings,
Subscription,
+394 -46
View File
@@ -495,7 +495,7 @@ type LocalAuthIntegration {
## SSOAuthIntegration
##########################
type Secret {
type SigningSecret {
"""
kid is the identifier for the key used when verifying tokens issued by the
provider.
@@ -551,23 +551,23 @@ type SSOAuthIntegration {
targetFilter: AuthenticationTargetFilter!
"""
keys are the different SSOKey's used by this Tenant.
signingSecrets are the different SigningSecret's used by this Tenant.
"""
keys: [Secret!]! @auth(roles: [ADMIN])
signingSecrets: [SigningSecret!]! @auth(roles: [ADMIN])
"""
key is the secret that is used to sign tokens.
"""
key: String
@auth(roles: [ADMIN])
@deprecated(reason: "field is deprecated in favour of `keys`")
@deprecated(reason: "field is deprecated in favour of `signingSecrets`")
"""
keyGeneratedAt is the Time that the key was effective from.
"""
keyGeneratedAt: Time
@auth(roles: [ADMIN])
@deprecated(reason: "field is deprecated in favour of `keys`")
@deprecated(reason: "field is deprecated in favour of `signingSecrets`")
}
##########################
@@ -908,6 +908,73 @@ type PerspectiveExternalIntegration {
sendFeedback: Boolean @auth(roles: [ADMIN])
}
"""
COMMENT_BODY_FORMAT describes the various formats that a comment body can be
provided in.
"""
enum COMMENT_BODY_FORMAT {
"""
HTML describes the format of the comment body using HTML.
"""
HTML
"""
PLAIN_TEXT describes the format of the comment body with the HTML stripped.
"""
PLAIN_TEXT
}
"""
ExternalModerationPhase describes a phase use in the moderation pipeline that
calls out to an external resource as defined by the provided URL.
"""
type ExternalModerationPhase {
"""
id identifies this particular External Moderation Phase.
"""
id: ID!
"""
name is the name assigned to this ExternalModerationPhase for identification
purposes.
"""
name: String!
"""
enabled when true, will use this phase in the moderation pipeline.
"""
enabled: Boolean!
"""
url is the actual URL that should be called.
"""
url: String!
"""
format is the format of the comment body sent.
"""
format: COMMENT_BODY_FORMAT!
"""
timeout is the number of milliseconds that this moderation is maximum expected
to take before it is skipped.
"""
timeout: Int!
"""
signingSecret is the secret used to sign outgoing requests to the url during
the moderation pipeline.
"""
signingSecret: SigningSecret!
}
type CustomExternalIntegration {
"""
phases is all the external moderation phases for this Tenant.
"""
phases: [ExternalModerationPhase!]!
}
type ExternalIntegrations {
"""
akismet provides integration with the Akismet Spam detection service.
@@ -919,6 +986,12 @@ type ExternalIntegrations {
platform.
"""
perspective: PerspectiveExternalIntegration!
"""
external provides integration details for external moderation phases that can be
used in the moderation pipeline.
"""
external: CustomExternalIntegration
}
################################################################################
@@ -1318,7 +1391,7 @@ type WebhookEndpoint {
"""
signingSecret is the current secret used to sign the events sent out.
"""
signingSecret: Secret!
signingSecret: SigningSecret!
"""
deliveries store the deliveries for each event sent for the last 50 events.
@@ -3183,6 +3256,13 @@ type Query {
queues returns information on queues used in Coral to manage
"""
queues: Queues! @auth(roles: [ADMIN])
"""
externalModerationPhase will return a specific ExternalModerationPhase if it
exists.
"""
externalModerationPhase(id: ID!): ExternalModerationPhase
@auth(roles: [ADMIN])
}
################################################################################
@@ -4369,7 +4449,7 @@ input RegenerateSSOKeyInput {
type RegenerateSSOKeyPayload {
"""
settings is the Settings that the SSO key was regenerated on.
settings is the Settings that the SSO secret was regenerated on.
"""
settings: Settings
@@ -5158,10 +5238,10 @@ type UpdateWebhookEndpointPayload {
}
##################
# rotateWebhookEndpointSecret
# rotateWebhookEndpointSigningSecret
##################
input RotateWebhookEndpointSecretInput {
input RotateWebhookEndpointSigningSecretInput {
"""
clientMutationId is required for Relay support.
"""
@@ -5179,7 +5259,7 @@ input RotateWebhookEndpointSecretInput {
inactiveIn: Int!
}
type RotateWebhookEndpointSecretPayload {
type RotateWebhookEndpointSigningSecretPayload {
"""
clientMutationId is required for Relay support.
"""
@@ -5188,7 +5268,7 @@ type RotateWebhookEndpointSecretPayload {
"""
endpoint is the endpoint that we just updated.
"""
endpoint: WebhookEndpoint
endpoint: WebhookEndpoint!
}
##################
@@ -5216,7 +5296,7 @@ type DisableWebhookEndpointPayload {
"""
endpoint is the endpoint that we just disabled.
"""
endpoint: WebhookEndpoint
endpoint: WebhookEndpoint!
}
##################
@@ -5244,7 +5324,7 @@ type EnableWebhookEndpointPayload {
"""
endpoint is the endpoint that we just enabled.
"""
endpoint: WebhookEndpoint
endpoint: WebhookEndpoint!
}
##################
@@ -5272,7 +5352,225 @@ type DeleteWebhookEndpointPayload {
"""
endpoint is the endpoint that we just deleted.
"""
endpoint: WebhookEndpoint
endpoint: WebhookEndpoint!
}
#################
# createExternalModerationPhase
##################
input CreateExternalModerationPhaseInput {
"""
clientMutationId is required for Relay support.
"""
clientMutationId: String!
"""
name is the name assigned to this ExternalModerationPhase for identification
purposes.
"""
name: String!
"""
url is the URL that Coral will POST moderation queries to.
"""
url: String!
"""
format is the format of the comment body sent.
"""
format: COMMENT_BODY_FORMAT!
"""
timeout is the number of milliseconds that this moderation is maximum expected
to take before it is skipped.
"""
timeout: Int!
}
type CreateExternalModerationPhasePayload {
"""
clientMutationId is required for Relay support.
"""
clientMutationId: String!
"""
phase is the ExternalModerationPhase that we just created.
"""
phase: ExternalModerationPhase!
"""
settings is the updated settings also containing the new phase.
"""
settings: Settings!
}
##################
# updateExternalModerationPhase
##################
input UpdateExternalModerationPhaseInput {
"""
clientMutationId is required for Relay support.
"""
clientMutationId: String!
"""
id is the ID of the ExternalModerationPhase being updated.
"""
id: ID!
"""
name is the name assigned to this ExternalModerationPhase for identification
purposes.
"""
name: String
"""
url is the URL that Coral will POST moderation queries to.
"""
url: String
"""
format is the format of the comment body sent.
"""
format: COMMENT_BODY_FORMAT
"""
timeout is the number of milliseconds that this moderation is maximum expected
to take before it is skipped.
"""
timeout: Int
}
type UpdateExternalModerationPhasePayload {
"""
clientMutationId is required for Relay support.
"""
clientMutationId: String!
"""
phase is the ExternalModerationPhase that we just updated.
"""
phase: ExternalModerationPhase!
}
##################
# deleteExternalModerationPhase
##################
input DeleteExternalModerationPhaseInput {
"""
clientMutationId is required for Relay support.
"""
clientMutationId: String!
"""
id is the ID of the ExternalModerationPhase being deleted.
"""
id: ID!
}
type DeleteExternalModerationPhasePayload {
"""
clientMutationId is required for Relay support.
"""
clientMutationId: String!
"""
phase is the ExternalModerationPhase that we just deleted.
"""
phase: ExternalModerationPhase!
}
##################
# disableExternalModerationPhase
##################
input DisableExternalModerationPhaseInput {
"""
clientMutationId is required for Relay support.
"""
clientMutationId: String!
"""
id is the ID of the ExternalModerationPhase being disabled.
"""
id: ID!
}
type DisableExternalModerationPhasePayload {
"""
clientMutationId is required for Relay support.
"""
clientMutationId: String!
"""
phase is the ExternalModerationPhase that we just disabled.
"""
phase: ExternalModerationPhase!
}
##################
# enableExternalModerationPhase
##################
input EnableExternalModerationPhaseInput {
"""
clientMutationId is required for Relay support.
"""
clientMutationId: String!
"""
id is the ID of the ExternalModerationPhase being enabled.
"""
id: ID!
}
type EnableExternalModerationPhasePayload {
"""
clientMutationId is required for Relay support.
"""
clientMutationId: String!
"""
phase is the ExternalModerationPhase that we just enabled.
"""
phase: ExternalModerationPhase!
}
##################
# rotateExternalModerationPhaseSigningSecret
##################
input RotateExternalModerationPhaseSigningSecretInput {
"""
clientMutationId is required for Relay support.
"""
clientMutationId: String!
"""
id is the ID of the ExternalModerationPhase being updated.
"""
id: ID!
"""
inactiveIn is the number of seconds that the current active Secret should be
kept active.
"""
inactiveIn: Int!
}
type RotateExternalModerationPhaseSigningSecretPayload {
"""
clientMutationId is required for Relay support.
"""
clientMutationId: String!
"""
phase is the ExternalModerationPhase that we just updated.
"""
phase: ExternalModerationPhase!
}
##################
@@ -6001,87 +6299,87 @@ type EnableFeatureFlagPayload {
}
#########################
## rotateSSOKey
## rotateSSOSigningSecret
#########################
input RotateSSOKeyInput {
input RotateSSOSigningSecretInput {
"""
clientMutationId is required for Relay support.
"""
clientMutationId: String!
"""
inactiveIn is the number of seconds that the current active SSOKey should be
kept active (allow signed tokens signed with this secret) before rejecting
them.
inactiveIn is the number of seconds that the current active SigningSecret
should be kept active (allow signed tokens signed with this secret) before
rejecting them.
"""
inactiveIn: Int!
}
type RotateSSOKeyPayload {
type RotateSSOSigningSecretPayload {
"""
clientMutationId is required for Relay support.
"""
clientMutationId: String!
"""
settings is the Settings that the SSO key was regenerated on.
settings is the Settings that the SSO secret was regenerated on.
"""
settings: Settings
}
#########################
## deactivateSSOKey
## deactivateSSOSigningSecret
#########################
input DeactivateSSOKeyInput {
input DeactivateSSOSigningSecretInput {
"""
clientMutationId is required for Relay support.
"""
clientMutationId: String!
"""
kid is the ID of the SSOKey being deactivated.
kid is the ID of the SigningSecret being deactivated.
"""
kid: ID!
}
type DeactivateSSOKeyPayload {
type DeactivateSSOSigningSecretPayload {
"""
clientMutationId is required for Relay support.
"""
clientMutationId: String!
"""
settings is the Settings that the SSO key was regenerated on.
settings is the Settings that the SSO secret was regenerated on.
"""
settings: Settings
}
#########################
## deleteSSOKey
## deleteSSOSigningSecret
#########################
input DeleteSSOKeyInput {
input DeleteSSOSigningSecretInput {
"""
clientMutationId is required for Relay support.
"""
clientMutationId: String!
"""
kid is the ID of the SSOKey being deleted.
kid is the ID of the SigningSecret being deleted.
"""
kid: ID!
}
type DeleteSSOKeyPayload {
type DeleteSSOSigningSecretPayload {
"""
clientMutationId is required for Relay support.
"""
clientMutationId: String!
"""
settings is the Settings that the SSO key was regenerated on.
settings is the Settings that the SSO secret was regenerated on.
"""
settings: Settings
}
@@ -6349,30 +6647,35 @@ type Mutation {
@auth(roles: [ADMIN])
"""
regenerateSSOKey will regenerate the SSO key used to sign secrets. This will
regenerateSSOKey will regenerate the SSO secret used to sign secrets. This will
invalidate any existing user sessions.
DEPRECATED: deprecated in favour of `rotateSSOSigningSecret`, remove in 6.2.0.
"""
regenerateSSOKey(input: RegenerateSSOKeyInput!): RegenerateSSOKeyPayload!
@auth(roles: [ADMIN])
@deprecated(reason: "deprecated in favour of `rotateSSOKey`")
@deprecated(reason: "deprecated in favour of `rotateSSOSigningSecret`")
"""
rotateSSOKey can be used to rotate a given active SSOKey.
rotateSSOSigningSecret can be used to rotate a given active SigningSecret.
"""
rotateSSOKey(input: RotateSSOKeyInput!): RotateSSOKeyPayload!
@auth(roles: [ADMIN])
rotateSSOSigningSecret(
input: RotateSSOSigningSecretInput!
): RotateSSOSigningSecretPayload! @auth(roles: [ADMIN])
"""
deactivateSSOKey will deactivate a given deactivated SSOKey.
deactivateSSOSigningSecret will deactivate a given deactivated SigningSecret.
"""
deactivateSSOKey(input: DeactivateSSOKeyInput!): DeactivateSSOKeyPayload!
@auth(roles: [ADMIN])
deactivateSSOSigningSecret(
input: DeactivateSSOSigningSecretInput!
): DeactivateSSOSigningSecretPayload! @auth(roles: [ADMIN])
"""
deleteSSOKey will delete a given inactive SSOKey.
deleteSSOSigningSecret will delete a given inactive SigningSecret.
"""
deleteSSOKey(input: DeleteSSOKeyInput!): DeleteSSOKeyPayload!
@auth(roles: [ADMIN])
deleteSSOSigningSecret(
input: DeleteSSOSigningSecretInput!
): DeleteSSOSigningSecretPayload! @auth(roles: [ADMIN])
"""
createCommentReaction will create a Reaction authored by the current logged in
@@ -6762,11 +7065,56 @@ type Mutation {
): DeleteWebhookEndpointPayload! @auth(roles: [ADMIN])
"""
rotateWebhookEndpointSecret will roll the current active secret to a new key.
rotateWebhookEndpointSigningSecret will roll the current active secret to a new key.
"""
rotateWebhookEndpointSecret(
input: RotateWebhookEndpointSecretInput!
): RotateWebhookEndpointSecretPayload! @auth(roles: [ADMIN])
rotateWebhookEndpointSigningSecret(
input: RotateWebhookEndpointSigningSecretInput!
): RotateWebhookEndpointSigningSecretPayload! @auth(roles: [ADMIN])
"""
createExternalModerationPhase will create a new ExternalModerationPhase.
"""
createExternalModerationPhase(
input: CreateExternalModerationPhaseInput!
): CreateExternalModerationPhasePayload! @auth(roles: [ADMIN])
"""
updateExternalModerationPhase will update a ExternalModerationPhase.
"""
updateExternalModerationPhase(
input: UpdateExternalModerationPhaseInput!
): UpdateExternalModerationPhasePayload! @auth(roles: [ADMIN])
"""
enableExternalModerationPhase will enable a ExternalModerationPhase to recieve
new comments.
"""
enableExternalModerationPhase(
input: EnableExternalModerationPhaseInput!
): EnableExternalModerationPhasePayload! @auth(roles: [ADMIN])
"""
disableExternalModerationPhase will disable a ExternalModerationPhase from
recieving new comments.
"""
disableExternalModerationPhase(
input: DisableExternalModerationPhaseInput!
): DisableExternalModerationPhasePayload! @auth(roles: [ADMIN])
"""
deleteExternalModerationPhase will delete a ExternalModerationPhase.
"""
deleteExternalModerationPhase(
input: DeleteExternalModerationPhaseInput!
): DeleteExternalModerationPhasePayload! @auth(roles: [ADMIN])
"""
rotateExternalModerationPhaseSigningSecret will roll the current active secret
to a new key.
"""
rotateExternalModerationPhaseSigningSecret(
input: RotateExternalModerationPhaseSigningSecretInput!
): RotateExternalModerationPhaseSigningSecretPayload! @auth(roles: [ADMIN])
"""
updateStoryMode will set the story mode.
@@ -24,10 +24,10 @@ import {
} from "coral-server/app/middleware/passport/strategies/jwt";
import {
CoralError,
InternalError,
LiveUpdatesDisabled,
RawQueryNotAuthorized,
TenantNotFoundError,
WrappedInternalError,
} from "coral-server/errors";
import { enrichError, logError, logQuery } from "coral-server/graph/extensions";
import { getOperationMetadata } from "coral-server/graph/extensions/helpers";
@@ -151,7 +151,10 @@ export function onConnect(options: OnConnectOptions): OnConnectFn {
if (!(err instanceof CoralError)) {
// eslint-disable-next-line no-ex-assign
err = new InternalError(err, "could not setup websocket connection");
err = new WrappedInternalError(
err,
"could not setup websocket connection"
);
}
const { message } = err.serializeExtensions(
options.i18n.getDefaultBundle()