mirror of
https://github.com/wassname/talk.git
synced 2026-06-27 16:47:24 +08:00
[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:
@@ -0,0 +1,240 @@
|
||||
# External Moderation Phases Guide
|
||||
|
||||
This document is in reference to external moderation phases emitted by Coral.
|
||||
You can configure external moderation phases on your installation of Coral by
|
||||
visiting `/admin/configure/moderation/phases`.
|
||||
|
||||
Once you've configured a external moderation phase in Coral, you will start to
|
||||
receive moderation requests in the form of a
|
||||
[External Moderation Requests](#external-moderation-request) at the provided
|
||||
callback URL. These will be in the form of `POST` requests with a `JSON`
|
||||
payload.
|
||||
|
||||
When a comment is created or edited, it will be processed by moderation phases in
|
||||
a predefined order. Any external moderation phase is run last, and only if all
|
||||
other moderation phases before it do not return a status. The current set of
|
||||
moderation phases is listed in order [here](https://github.com/coralproject/talk/blob/master/src/core/server/services/comments/pipeline/phases/index.ts).
|
||||
|
||||
Once you have received a moderation request, you must respond within the
|
||||
provided timeout else the phase will be skipped and it will continue. It is
|
||||
strongly recommended to [verify the request signature](#request-signing).
|
||||
|
||||
The external moderation phase must respond with one of the following:
|
||||
|
||||
1. Do not moderate the comment, and return a 204 without a body.
|
||||
2. Perform a moderation action and return a 200 with a [External Moderation Response](#external-moderation-response)
|
||||
as a `JSON` encoded body containing the operations you want to perform on the
|
||||
comment.
|
||||
|
||||
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
|
||||
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
|
||||
## Table of Contents
|
||||
|
||||
- [Request Signing](#request-signing)
|
||||
- [Schema](#schema)
|
||||
- [External Moderation Request](#external-moderation-request)
|
||||
- [External Moderation Response](#external-moderation-response)
|
||||
|
||||
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
||||
|
||||
## Request Signing
|
||||
|
||||
Requests sent by Coral for external moderation phases use the same process as
|
||||
those used by webhooks. Refer to the [webhooks documentation](WEBHOOKS.md#webhook-signing)
|
||||
for instructions on how to verify signatures sent by Coral.
|
||||
|
||||
## Schema
|
||||
|
||||
### External Moderation Request
|
||||
|
||||
```ts
|
||||
interface ExternalModerationRequest {
|
||||
/**
|
||||
* action refers to the specific operation being performed. If `NEW`, this
|
||||
* is referring to a new comment being created. If `EDIT`, then this refers to
|
||||
* an operation involving an edit operation on an existing Comment.
|
||||
*/
|
||||
action: "NEW" | "EDIT";
|
||||
|
||||
/**
|
||||
* comment refers to the actual Comment data for the Comment being
|
||||
* created/edited.
|
||||
*/
|
||||
comment: {
|
||||
/**
|
||||
* body refers to the actual body text of the Comment being created/edited.
|
||||
*/
|
||||
body: string;
|
||||
|
||||
/**
|
||||
* parentID is the identifier for the parent comment (if this Comment is a
|
||||
* reply, null otherwise).
|
||||
*/
|
||||
parentID: string | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* author refers to the User that is creating/editing the Comment.
|
||||
*/
|
||||
author: {
|
||||
/**
|
||||
* id is the identifier for this User.
|
||||
*/
|
||||
id: string;
|
||||
|
||||
/**
|
||||
* role refers to the role of this User.
|
||||
*/
|
||||
role: "COMMENTER" | "STAFF" | "MODERATOR" | "ADMIN";
|
||||
};
|
||||
|
||||
/**
|
||||
* story refers to the Story being commented on.
|
||||
*/
|
||||
story: {
|
||||
/**
|
||||
* id is the identifier for this Story.
|
||||
*/
|
||||
id: string;
|
||||
|
||||
/**
|
||||
* url is the URL for this Story.
|
||||
*/
|
||||
url: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* site refers to the Site that the story being commented on belongs to.
|
||||
*/
|
||||
site: {
|
||||
/**
|
||||
* id is the identifier for this Site.
|
||||
*/
|
||||
id: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* tenantID is the identifer of the Tenant that this Comment is being
|
||||
* created/edited on.
|
||||
*/
|
||||
tenantID: string;
|
||||
|
||||
/**
|
||||
* tenantDomain is the domain that is associated with this Tenant that this
|
||||
* Comment is being created/edited on.
|
||||
*/
|
||||
tenantDomain: string;
|
||||
}
|
||||
```
|
||||
|
||||
#### Example
|
||||
|
||||
New comment on a story:
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "NEW",
|
||||
"comment": {
|
||||
"body": "Here's a comment!",
|
||||
"parentID": null
|
||||
},
|
||||
"author": {
|
||||
"id": "baf4e943-3594-4fcc-b2ba-3e8de7a76352",
|
||||
"role": "COMMENTER"
|
||||
},
|
||||
"story": {
|
||||
"id": "245b3856-b0a0-4d2f-a6bb-58c71f18d6a6",
|
||||
"url": "http://localhost:1313/posts/a-story-url/"
|
||||
},
|
||||
"site": {
|
||||
"id": "a4bede88-2d2c-4424-bc18-4322a9e285a6"
|
||||
},
|
||||
"tenantID": "19ba5794-7eeb-4d46-a81b-c00c61672501",
|
||||
"tenantDomain": "localhost"
|
||||
}
|
||||
```
|
||||
|
||||
New reply on a comment on a story:
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "NEW",
|
||||
"comment": {
|
||||
"body": "Here's a reply!",
|
||||
"parentID": "d79b787f-f406-49a0-a179-72e3652e54be"
|
||||
},
|
||||
"author": {
|
||||
"id": "baf4e943-3594-4fcc-b2ba-3e8de7a76352",
|
||||
"role": "COMMENTER"
|
||||
},
|
||||
"story": {
|
||||
"id": "245b3856-b0a0-4d2f-a6bb-58c71f18d6a6",
|
||||
"url": "http://localhost:1313/posts/a-story-url/"
|
||||
},
|
||||
"site": {
|
||||
"id": "a4bede88-2d2c-4424-bc18-4322a9e285a6"
|
||||
},
|
||||
"tenantID": "19ba5794-7eeb-4d46-a81b-c00c61672501",
|
||||
"tenantDomain": "localhost"
|
||||
}
|
||||
```
|
||||
|
||||
### External Moderation Response
|
||||
|
||||
```ts
|
||||
interface ExternalModerationResponse {
|
||||
/**
|
||||
* actions is an optional list of any flags to be added to this Comment.
|
||||
*/
|
||||
actions?: Array<{
|
||||
actionType: "FLAG";
|
||||
reason: "COMMENT_DETECTED_TOXIC" | "COMMENT_DETECTED_SPAM";
|
||||
}>;
|
||||
|
||||
/**
|
||||
* tags are any listed tags that should be added to the comment.
|
||||
*/
|
||||
tags?: Array<"FEATURED" | "STAFF">;
|
||||
|
||||
/**
|
||||
* status when provided decides and terminates the moderation process by
|
||||
* setting the status of the comment.
|
||||
*/
|
||||
status?: "NONE" | "APPROVED" | "REJECTED" | "PREMOD" | "SYSTEM_WITHHELD";
|
||||
}
|
||||
```
|
||||
|
||||
#### Examples
|
||||
|
||||
Add a flag to a comment and do not set a status:
|
||||
|
||||
```json
|
||||
{
|
||||
"actions": [{ "actionType": "FLAG", "reason": "COMMENT_DETECTED_TOXIC" }]
|
||||
}
|
||||
```
|
||||
|
||||
Reject a comment:
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "REJECTED"
|
||||
}
|
||||
```
|
||||
|
||||
Feature a comment and do not set a status:
|
||||
|
||||
```json
|
||||
{
|
||||
"tags": ["FEATURED"]
|
||||
}
|
||||
```
|
||||
|
||||
Approve a comment and mark it as featured:
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "APPROVED",
|
||||
"tags": ["FEATURED"]
|
||||
}
|
||||
```
|
||||
@@ -1,4 +1,4 @@
|
||||
Copyright 2019 Vox Media, Inc
|
||||
Copyright 2020 Vox Media, Inc
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
||||
+2
-2
@@ -27,7 +27,7 @@
|
||||
"build:server": "gulp server",
|
||||
"migration:create": "ts-node --transpile-only ./scripts/migration/create.ts",
|
||||
"docs:events": "ts-node ./scripts/generateEventDocs.ts ./src/core/client/stream/events.ts ./CLIENT_EVENTS.md",
|
||||
"doctoc": "doctoc --maxlevel=3 --title '## Table of Contents' README.md CLIENT_EVENTS.md CONTRIBUTING.md WEBHOOKS.md",
|
||||
"doctoc": "doctoc --maxlevel=3 --title '## Table of Contents' README.md CLIENT_EVENTS.md CONTRIBUTING.md WEBHOOKS.md EXTERNAL_MODERATION_PHASES.md",
|
||||
"generate": "npm-run-all generate:css-types generate:schema generate:relay",
|
||||
"generate-persist": "npm-run-all generate:css-types generate:schema generate:relay-persist",
|
||||
"generate:css-types": "tcm src/core/client/",
|
||||
@@ -413,7 +413,7 @@
|
||||
"{src/core/client/stream/events.ts,scripts/generateEventDocs.ts,CLIENT_EVENTS.md}": [
|
||||
"npm run docs:events -- --verify"
|
||||
],
|
||||
"{README,CLIENT_EVENTS,CONTRIBUTING,WEBHOOKS}.md": [
|
||||
"{README,CLIENT_EVENTS,CONTRIBUTING,WEBHOOKS,EXTERNAL_MODERATION_PHASES}.md": [
|
||||
"npm run doctoc"
|
||||
]
|
||||
},
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import { urls } from "coral-framework/helpers";
|
||||
|
||||
export default function getExternalModerationPhaseLink(phaseID: string) {
|
||||
return `${urls.admin.configureExternalModerationPhase}/${phaseID}`;
|
||||
}
|
||||
@@ -9,18 +9,22 @@ import { createAuthCheckRoute } from "./routes/AuthCheck";
|
||||
import CommunityRoute from "./routes/Community";
|
||||
import ConfigureRoute from "./routes/Configure";
|
||||
import {
|
||||
AddExternalModerationPhaseRoute,
|
||||
AddWebhookEndpointRoute,
|
||||
AdvancedConfigRoute,
|
||||
AuthConfigRoute,
|
||||
ConfigureExternalModerationPhaseRoute,
|
||||
ConfigureWebhookEndpointRoute,
|
||||
EmailConfigRoute,
|
||||
GeneralConfigRoute,
|
||||
ModerationConfigRoute,
|
||||
ModerationPhasesConfigRoute,
|
||||
OrganizationConfigRoute,
|
||||
SlackConfigRoute,
|
||||
WebhookEndpointsConfigRoute,
|
||||
WordListConfigRoute,
|
||||
} from "./routes/Configure/sections";
|
||||
import ModerationPhasesLayout from "./routes/Configure/sections/ModerationPhases/ModerationPhasesLayout";
|
||||
import { Sites } from "./routes/Configure/sections/Sites";
|
||||
import AddSiteRoute from "./routes/Configure/sections/Sites/AddSiteRoute";
|
||||
import SiteRoute from "./routes/Configure/sections/Sites/SiteRoute";
|
||||
@@ -120,13 +124,28 @@ export default makeRouteConfig(
|
||||
path="organization"
|
||||
{...OrganizationConfigRoute.routeConfig}
|
||||
/>
|
||||
<Route path="moderation" {...ModerationConfigRoute.routeConfig} />
|
||||
<Route
|
||||
exact
|
||||
path="moderation"
|
||||
{...ModerationConfigRoute.routeConfig}
|
||||
/>
|
||||
<Route path="wordList" {...WordListConfigRoute.routeConfig} />
|
||||
<Route path="auth" {...AuthConfigRoute.routeConfig} />
|
||||
<Route path="advanced" {...AdvancedConfigRoute.routeConfig} />
|
||||
<Route path="email" {...EmailConfigRoute.routeConfig} />
|
||||
<Route path="slack" {...SlackConfigRoute.routeConfig} />
|
||||
</Route>
|
||||
<Route
|
||||
path="configure/moderation/phases"
|
||||
Component={ModerationPhasesLayout}
|
||||
>
|
||||
<Route path="/" {...ModerationPhasesConfigRoute.routeConfig} />
|
||||
<Route path="add" Component={AddExternalModerationPhaseRoute} />
|
||||
<Route
|
||||
path=":phaseID"
|
||||
{...ConfigureExternalModerationPhaseRoute.routeConfig}
|
||||
/>
|
||||
</Route>
|
||||
<Route path="configure/webhooks" Component={WebhookEndpointsLayout}>
|
||||
<Route path="/" {...WebhookEndpointsConfigRoute.routeConfig} />
|
||||
<Route path="add" {...AddWebhookEndpointRoute.routeConfig} />
|
||||
|
||||
@@ -14,7 +14,12 @@ const ConfigureLinks: FunctionComponent<{}> = () => {
|
||||
<Link to="/admin/configure/organization">Organization</Link>
|
||||
</Localized>
|
||||
<Localized id="configure-sideBarNavigation-moderation">
|
||||
<Link to="/admin/configure/moderation">Moderation</Link>
|
||||
<Link exact to="/admin/configure/moderation">
|
||||
Moderation
|
||||
</Link>
|
||||
</Localized>
|
||||
<Localized id="configure-sideBarNavigation-moderationPhases">
|
||||
<Link to="/admin/configure/moderation/phases">Moderation Phases</Link>
|
||||
</Localized>
|
||||
<Localized id="configure-sideBarNavigation-bannedAndSuspectWords">
|
||||
<Link to="/admin/configure/wordList">Banned and Suspect Words</Link>
|
||||
|
||||
@@ -7,6 +7,7 @@ interface Props {
|
||||
className?: string;
|
||||
children: React.ReactNode;
|
||||
to: string | LocationDescriptor;
|
||||
exact?: boolean;
|
||||
}
|
||||
|
||||
const Link: FunctionComponent<Props> = (props) => (
|
||||
@@ -15,6 +16,7 @@ const Link: FunctionComponent<Props> = (props) => (
|
||||
to={props.to}
|
||||
className={styles.link}
|
||||
activeClassName={styles.linkActive}
|
||||
exact={props.exact}
|
||||
>
|
||||
{props.children}
|
||||
</FoundLink>
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
import { graphql } from "react-relay";
|
||||
import { Environment } from "relay-runtime";
|
||||
|
||||
import {
|
||||
commitMutationPromiseNormalized,
|
||||
createMutation,
|
||||
} from "coral-framework/lib/relay";
|
||||
|
||||
import { RegenerateSSOKeyMutation as MutationTypes } from "coral-admin/__generated__/RegenerateSSOKeyMutation.graphql";
|
||||
|
||||
let clientMutationId = 0;
|
||||
|
||||
const RegenerateSSOKeyMutation = createMutation(
|
||||
"regenerateSSOKey",
|
||||
(environment: Environment) =>
|
||||
commitMutationPromiseNormalized<MutationTypes>(environment, {
|
||||
mutation: graphql`
|
||||
mutation RegenerateSSOKeyMutation($input: RegenerateSSOKeyInput!) {
|
||||
regenerateSSOKey(input: $input) {
|
||||
settings {
|
||||
auth {
|
||||
integrations {
|
||||
sso {
|
||||
key
|
||||
keyGeneratedAt
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
clientMutationId
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
input: {
|
||||
clientMutationId: (clientMutationId++).toString(),
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
export default RegenerateSSOKeyMutation;
|
||||
@@ -8,7 +8,7 @@ import { FormFieldDescription } from "coral-ui/components/v2";
|
||||
import Header from "../../Header";
|
||||
import ConfigBoxWithToggleField from "./ConfigBoxWithToggleField";
|
||||
import RegistrationField from "./RegistrationField";
|
||||
import SSOKeyRotationQuery from "./SSOKeyRotation/SSOKeyRotationQuery";
|
||||
import SSOSigningSecretRotationQuery from "./SSOSigningSecretRotation/SSOSigningSecretRotationQuery";
|
||||
import TargetFilterField from "./TargetFilterField";
|
||||
|
||||
// eslint-disable-next-line no-unused-expressions
|
||||
@@ -60,7 +60,9 @@ const SSOConfig: FunctionComponent<Props> = ({ disabled }) => (
|
||||
for additional information on single sign on.
|
||||
</FormFieldDescription>
|
||||
</Localized>
|
||||
<SSOKeyRotationQuery disabled={disabledInside}></SSOKeyRotationQuery>
|
||||
<SSOSigningSecretRotationQuery
|
||||
disabled={disabledInside}
|
||||
></SSOSigningSecretRotationQuery>
|
||||
<TargetFilterField
|
||||
label={
|
||||
<Localized id="configure-auth-sso-useLoginOn">
|
||||
|
||||
+7
-7
@@ -3,11 +3,11 @@ import React, { FunctionComponent } from "react";
|
||||
|
||||
import { Flex, Label } from "coral-ui/components/v2";
|
||||
|
||||
import { SSOKeyStatus } from "./StatusField";
|
||||
import { SSOSigningSecretStatus } from "./StatusField";
|
||||
|
||||
import styles from "./DateField.css";
|
||||
|
||||
export interface SSOKeyDates {
|
||||
export interface SSOSigningSecretDates {
|
||||
readonly createdAt: string;
|
||||
readonly lastUsedAt: string | null;
|
||||
readonly rotatedAt: string | null;
|
||||
@@ -15,13 +15,13 @@ export interface SSOKeyDates {
|
||||
}
|
||||
|
||||
interface Props {
|
||||
status: SSOKeyStatus;
|
||||
dates: SSOKeyDates;
|
||||
status: SSOSigningSecretStatus;
|
||||
dates: SSOSigningSecretDates;
|
||||
}
|
||||
|
||||
const DateField: FunctionComponent<Props> = ({ status, dates }) => {
|
||||
switch (status) {
|
||||
case SSOKeyStatus.ACTIVE:
|
||||
case SSOSigningSecretStatus.ACTIVE:
|
||||
return (
|
||||
<>
|
||||
<div className={styles.label}>
|
||||
@@ -37,7 +37,7 @@ const DateField: FunctionComponent<Props> = ({ status, dates }) => {
|
||||
</Localized>
|
||||
</>
|
||||
);
|
||||
case SSOKeyStatus.EXPIRING:
|
||||
case SSOSigningSecretStatus.EXPIRING:
|
||||
return (
|
||||
<>
|
||||
<div className={styles.label}>
|
||||
@@ -63,7 +63,7 @@ const DateField: FunctionComponent<Props> = ({ status, dates }) => {
|
||||
</Flex>
|
||||
</>
|
||||
);
|
||||
case SSOKeyStatus.EXPIRED:
|
||||
case SSOSigningSecretStatus.EXPIRED:
|
||||
return (
|
||||
<>
|
||||
<div className={styles.label}>
|
||||
+9
-7
@@ -7,23 +7,25 @@ import {
|
||||
MutationInput,
|
||||
} from "coral-framework/lib/relay";
|
||||
|
||||
import { RotateSSOKeyMutation as MutationTypes } from "coral-admin/__generated__/RotateSSOKeyMutation.graphql";
|
||||
import { DeactivateSSOSigningSecretMutation as MutationTypes } from "coral-admin/__generated__/DeactivateSSOSigningSecretMutation.graphql";
|
||||
|
||||
const clientMutationId = 0;
|
||||
|
||||
const RotateSSOKeyMutation = createMutation(
|
||||
"rotateSSOKey",
|
||||
const DeactivateSSOSigningSecretMutation = createMutation(
|
||||
"deactivateSSOSigningSecret",
|
||||
(environment: Environment, input: MutationInput<MutationTypes>) => {
|
||||
return commitMutationPromiseNormalized<MutationTypes>(environment, {
|
||||
mutation: graphql`
|
||||
mutation RotateSSOKeyMutation($input: RotateSSOKeyInput!) {
|
||||
rotateSSOKey(input: $input) {
|
||||
mutation DeactivateSSOSigningSecretMutation(
|
||||
$input: DeactivateSSOSigningSecretInput!
|
||||
) {
|
||||
deactivateSSOSigningSecret(input: $input) {
|
||||
settings {
|
||||
auth {
|
||||
integrations {
|
||||
sso {
|
||||
enabled
|
||||
keys {
|
||||
signingSecrets {
|
||||
kid
|
||||
secret
|
||||
createdAt
|
||||
@@ -49,4 +51,4 @@ const RotateSSOKeyMutation = createMutation(
|
||||
}
|
||||
);
|
||||
|
||||
export default RotateSSOKeyMutation;
|
||||
export default DeactivateSSOSigningSecretMutation;
|
||||
+9
-7
@@ -7,23 +7,25 @@ import {
|
||||
MutationInput,
|
||||
} from "coral-framework/lib/relay";
|
||||
|
||||
import { DeactivateSSOKeyMutation as MutationTypes } from "coral-admin/__generated__/DeactivateSSOKeyMutation.graphql";
|
||||
import { DeleteSSOSigningSecretMutation as MutationTypes } from "coral-admin/__generated__/DeleteSSOSigningSecretMutation.graphql";
|
||||
|
||||
const clientMutationId = 0;
|
||||
|
||||
const DeactivateSSOKeyMutation = createMutation(
|
||||
"deactivateSSOKey",
|
||||
const DeleteSSOSigningSecretMutation = createMutation(
|
||||
"deleteSSOSigningSecret",
|
||||
(environment: Environment, input: MutationInput<MutationTypes>) => {
|
||||
return commitMutationPromiseNormalized<MutationTypes>(environment, {
|
||||
mutation: graphql`
|
||||
mutation DeactivateSSOKeyMutation($input: DeactivateSSOKeyInput!) {
|
||||
deactivateSSOKey(input: $input) {
|
||||
mutation DeleteSSOSigningSecretMutation(
|
||||
$input: DeleteSSOSigningSecretInput!
|
||||
) {
|
||||
deleteSSOSigningSecret(input: $input) {
|
||||
settings {
|
||||
auth {
|
||||
integrations {
|
||||
sso {
|
||||
enabled
|
||||
keys {
|
||||
signingSecrets {
|
||||
kid
|
||||
secret
|
||||
createdAt
|
||||
@@ -49,4 +51,4 @@ const DeactivateSSOKeyMutation = createMutation(
|
||||
}
|
||||
);
|
||||
|
||||
export default DeactivateSSOKeyMutation;
|
||||
export default DeleteSSOSigningSecretMutation;
|
||||
+9
-7
@@ -7,23 +7,25 @@ import {
|
||||
MutationInput,
|
||||
} from "coral-framework/lib/relay";
|
||||
|
||||
import { DeleteSSOKeyMutation as MutationTypes } from "coral-admin/__generated__/DeleteSSOKeyMutation.graphql";
|
||||
import { RotateSSOSigningSecretMutation as MutationTypes } from "coral-admin/__generated__/RotateSSOSigningSecretMutation.graphql";
|
||||
|
||||
const clientMutationId = 0;
|
||||
|
||||
const DeleteSSOKeyMutation = createMutation(
|
||||
"deleteSSOKey",
|
||||
const RotateSSOSigningSecretMutation = createMutation(
|
||||
"rotateSSOSigningSecret",
|
||||
(environment: Environment, input: MutationInput<MutationTypes>) => {
|
||||
return commitMutationPromiseNormalized<MutationTypes>(environment, {
|
||||
mutation: graphql`
|
||||
mutation DeleteSSOKeyMutation($input: DeleteSSOKeyInput!) {
|
||||
deleteSSOKey(input: $input) {
|
||||
mutation RotateSSOSigningSecretMutation(
|
||||
$input: RotateSSOSigningSecretInput!
|
||||
) {
|
||||
rotateSSOSigningSecret(input: $input) {
|
||||
settings {
|
||||
auth {
|
||||
integrations {
|
||||
sso {
|
||||
enabled
|
||||
keys {
|
||||
signingSecrets {
|
||||
kid
|
||||
secret
|
||||
createdAt
|
||||
@@ -49,4 +51,4 @@ const DeleteSSOKeyMutation = createMutation(
|
||||
}
|
||||
);
|
||||
|
||||
export default DeleteSSOKeyMutation;
|
||||
export default RotateSSOSigningSecretMutation;
|
||||
+29
-27
@@ -15,16 +15,16 @@ import {
|
||||
} from "coral-ui/components/v2";
|
||||
|
||||
import DateField from "./DateField";
|
||||
import DeactivateSSOKeyMutation from "./DeactivateSSOKeyMutation";
|
||||
import DeleteSSOKeyMutation from "./DeleteSSOKeyMutation";
|
||||
import RotateSSOKeyMutation from "./RotateSSOKeyMutation";
|
||||
import DeactivateSSOSigningSecretMutation from "./DeactivateSSOSigningSecretMutation";
|
||||
import DeleteSSOSigningSecretMutation from "./DeleteSSOSigningSecretMutation";
|
||||
import RotateSSOSigningSecretMutation from "./RotateSSOSigningSecretMutation";
|
||||
import RotationDropDown from "./RotationDropdown";
|
||||
import { RotateOptions } from "./RotationOption";
|
||||
import StatusField, { SSOKeyStatus } from "./StatusField";
|
||||
import StatusField, { SSOSigningSecretStatus } from "./StatusField";
|
||||
|
||||
import styles from "./SSOKeyCard.css";
|
||||
import styles from "./SSOSigningSecretCard.css";
|
||||
|
||||
export interface SSOKeyDates {
|
||||
export interface SSOSigningSecretDates {
|
||||
readonly createdAt: string;
|
||||
readonly lastUsedAt: string | null;
|
||||
readonly rotatedAt: string | null;
|
||||
@@ -34,22 +34,22 @@ export interface SSOKeyDates {
|
||||
interface Props {
|
||||
id: string;
|
||||
secret: string;
|
||||
status: SSOKeyStatus;
|
||||
dates: SSOKeyDates;
|
||||
status: SSOSigningSecretStatus;
|
||||
dates: SSOSigningSecretDates;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
function createActionButton(
|
||||
status: SSOKeyStatus,
|
||||
status: SSOSigningSecretStatus,
|
||||
onRotateKey: (rotation: string) => void,
|
||||
onDeactivateKey: () => void,
|
||||
onDelete: () => void,
|
||||
disabled?: boolean
|
||||
) {
|
||||
switch (status) {
|
||||
case SSOKeyStatus.ACTIVE:
|
||||
case SSOSigningSecretStatus.ACTIVE:
|
||||
return <RotationDropDown onRotateKey={onRotateKey} disabled={disabled} />;
|
||||
case SSOKeyStatus.EXPIRING:
|
||||
case SSOSigningSecretStatus.EXPIRING:
|
||||
return (
|
||||
<Localized id="configure-auth-sso-rotate-deactivateNow">
|
||||
<Button color="alert" onClick={onDeactivateKey} disabled={disabled}>
|
||||
@@ -57,7 +57,7 @@ function createActionButton(
|
||||
</Button>
|
||||
</Localized>
|
||||
);
|
||||
case SSOKeyStatus.EXPIRED:
|
||||
case SSOSigningSecretStatus.EXPIRED:
|
||||
return (
|
||||
<Localized id="configure-auth-sso-rotate-delete">
|
||||
<Button color="alert" onClick={onDelete} disabled={disabled}>
|
||||
@@ -70,48 +70,50 @@ function createActionButton(
|
||||
}
|
||||
}
|
||||
|
||||
const SSOKeyCard: FunctionComponent<Props> = ({
|
||||
const SSOSigningSecretCard: FunctionComponent<Props> = ({
|
||||
id,
|
||||
secret,
|
||||
status,
|
||||
dates,
|
||||
disabled,
|
||||
}) => {
|
||||
const rotateSSOKey = useMutation(RotateSSOKeyMutation);
|
||||
const deactivateSSOKey = useMutation(DeactivateSSOKeyMutation);
|
||||
const deleteSSOKey = useMutation(DeleteSSOKeyMutation);
|
||||
const rotateSSOSigningSecret = useMutation(RotateSSOSigningSecretMutation);
|
||||
const deactivateSSOSigningSecret = useMutation(
|
||||
DeactivateSSOSigningSecretMutation
|
||||
);
|
||||
const deleteSSOSigningSecret = useMutation(DeleteSSOSigningSecretMutation);
|
||||
|
||||
const onRotate = useCallback(
|
||||
(rotation: string) => {
|
||||
switch (rotation) {
|
||||
case RotateOptions.NOW:
|
||||
rotateSSOKey({ inactiveIn: 0 });
|
||||
rotateSSOSigningSecret({ inactiveIn: 0 });
|
||||
break;
|
||||
case RotateOptions.IN1DAY:
|
||||
rotateSSOKey({ inactiveIn: 24 * 60 * 60 });
|
||||
rotateSSOSigningSecret({ inactiveIn: 24 * 60 * 60 });
|
||||
break;
|
||||
case RotateOptions.IN1WEEK:
|
||||
rotateSSOKey({ inactiveIn: 7 * 24 * 60 * 60 });
|
||||
rotateSSOSigningSecret({ inactiveIn: 7 * 24 * 60 * 60 });
|
||||
break;
|
||||
case RotateOptions.IN30DAYS:
|
||||
rotateSSOKey({ inactiveIn: 30 * 24 * 60 * 60 });
|
||||
rotateSSOSigningSecret({ inactiveIn: 30 * 24 * 60 * 60 });
|
||||
break;
|
||||
default:
|
||||
rotateSSOKey({ inactiveIn: 0 });
|
||||
rotateSSOSigningSecret({ inactiveIn: 0 });
|
||||
}
|
||||
},
|
||||
[rotateSSOKey]
|
||||
[rotateSSOSigningSecret]
|
||||
);
|
||||
const onDeactivate = useCallback(() => {
|
||||
deactivateSSOKey({
|
||||
deactivateSSOSigningSecret({
|
||||
kid: id,
|
||||
});
|
||||
}, [deactivateSSOKey, id]);
|
||||
}, [deactivateSSOSigningSecret, id]);
|
||||
const onDelete = useCallback(() => {
|
||||
deleteSSOKey({
|
||||
deleteSSOSigningSecret({
|
||||
kid: id,
|
||||
});
|
||||
}, [deleteSSOKey, id]);
|
||||
}, [deleteSSOSigningSecret, id]);
|
||||
|
||||
return (
|
||||
<Card>
|
||||
@@ -184,4 +186,4 @@ const SSOKeyCard: FunctionComponent<Props> = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default SSOKeyCard;
|
||||
export default SSOSigningSecretCard;
|
||||
+21
-28
@@ -5,59 +5,52 @@ import { graphql } from "react-relay";
|
||||
import { withFragmentContainer } from "coral-framework/lib/relay";
|
||||
import { Label } from "coral-ui/components/v2";
|
||||
|
||||
import { SSOKeyRotationContainer_settings } from "coral-admin/__generated__/SSOKeyRotationContainer_settings.graphql";
|
||||
import { SSOSigningSecretRotationContainer_settings } from "coral-admin/__generated__/SSOSigningSecretRotationContainer_settings.graphql";
|
||||
|
||||
import SSOKeyCard, { SSOKeyDates } from "./SSOKeyCard";
|
||||
import { SSOKeyStatus } from "./StatusField";
|
||||
import SSOSigningSecretCard, {
|
||||
SSOSigningSecretDates,
|
||||
} from "./SSOSigningSecretCard";
|
||||
import { SSOSigningSecretStatus } from "./StatusField";
|
||||
|
||||
interface Props {
|
||||
settings: SSOKeyRotationContainer_settings;
|
||||
settings: SSOSigningSecretRotationContainer_settings;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
interface Key {
|
||||
readonly kid: string;
|
||||
readonly secret: string;
|
||||
readonly createdAt: string;
|
||||
readonly lastUsedAt: string | null;
|
||||
readonly rotatedAt: string | null;
|
||||
readonly inactiveAt: string | null;
|
||||
}
|
||||
|
||||
function getStatus(dates: SSOKeyDates) {
|
||||
function getStatus(dates: SSOSigningSecretDates) {
|
||||
if (
|
||||
dates.inactiveAt &&
|
||||
dates.rotatedAt &&
|
||||
new Date(dates.inactiveAt) > new Date()
|
||||
) {
|
||||
return SSOKeyStatus.EXPIRING;
|
||||
return SSOSigningSecretStatus.EXPIRING;
|
||||
}
|
||||
|
||||
if (dates.inactiveAt && new Date(dates.inactiveAt) <= new Date()) {
|
||||
return SSOKeyStatus.EXPIRED;
|
||||
return SSOSigningSecretStatus.EXPIRED;
|
||||
}
|
||||
|
||||
return SSOKeyStatus.ACTIVE;
|
||||
return SSOSigningSecretStatus.ACTIVE;
|
||||
}
|
||||
|
||||
const SSOKeyRotationContainer: FunctionComponent<Props> = ({
|
||||
const SSOSigningSecretRotationContainer: FunctionComponent<Props> = ({
|
||||
disabled,
|
||||
settings,
|
||||
}) => {
|
||||
const {
|
||||
auth: {
|
||||
integrations: {
|
||||
sso: { keys },
|
||||
sso: { signingSecrets },
|
||||
},
|
||||
},
|
||||
} = settings;
|
||||
|
||||
const sortedKeys = useMemo(
|
||||
const sortedSigningSecrets = useMemo(
|
||||
() =>
|
||||
keys
|
||||
signingSecrets
|
||||
// Copy this map because we don't want to modify the underlying copy.
|
||||
.map((key) => key)
|
||||
.sort((a: Key, b: Key) => {
|
||||
.sort((a, b) => {
|
||||
// Both active, sort on createdAt date.
|
||||
if (!a.inactiveAt && !b.inactiveAt) {
|
||||
return (
|
||||
@@ -84,7 +77,7 @@ const SSOKeyRotationContainer: FunctionComponent<Props> = ({
|
||||
|
||||
return bDate.getTime() - aDate.getTime();
|
||||
}),
|
||||
[keys]
|
||||
[signingSecrets]
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -92,8 +85,8 @@ const SSOKeyRotationContainer: FunctionComponent<Props> = ({
|
||||
<Localized id="configure-auth-sso-rotate-keys">
|
||||
<Label htmlFor="configure-auth-sso-rotate-keys">Keys</Label>
|
||||
</Localized>
|
||||
{sortedKeys.map((key) => (
|
||||
<SSOKeyCard
|
||||
{sortedSigningSecrets.map((key) => (
|
||||
<SSOSigningSecretCard
|
||||
key={key.kid}
|
||||
id={key.kid}
|
||||
secret={key.secret}
|
||||
@@ -108,12 +101,12 @@ const SSOKeyRotationContainer: FunctionComponent<Props> = ({
|
||||
|
||||
const enhanced = withFragmentContainer<Props>({
|
||||
settings: graphql`
|
||||
fragment SSOKeyRotationContainer_settings on Settings {
|
||||
fragment SSOSigningSecretRotationContainer_settings on Settings {
|
||||
auth {
|
||||
integrations {
|
||||
sso {
|
||||
enabled
|
||||
keys {
|
||||
signingSecrets {
|
||||
kid
|
||||
secret
|
||||
createdAt
|
||||
@@ -126,6 +119,6 @@ const enhanced = withFragmentContainer<Props>({
|
||||
}
|
||||
}
|
||||
`,
|
||||
})(SSOKeyRotationContainer);
|
||||
})(SSOSigningSecretRotationContainer);
|
||||
|
||||
export default enhanced;
|
||||
+9
-7
@@ -4,21 +4,23 @@ import { graphql } from "react-relay";
|
||||
import { QueryRenderData, QueryRenderer } from "coral-framework/lib/relay";
|
||||
import { CallOut, Spinner } from "coral-ui/components/v2";
|
||||
|
||||
import { SSOKeyRotationQuery as QueryTypes } from "coral-admin/__generated__/SSOKeyRotationQuery.graphql";
|
||||
import { SSOSigningSecretRotationQuery as QueryTypes } from "coral-admin/__generated__/SSOSigningSecretRotationQuery.graphql";
|
||||
|
||||
import SSOKeyRotationContainer from "./SSOKeyRotationContainer";
|
||||
import SSOSigningSecretRotationContainer from "./SSOSigningSecretRotationContainer";
|
||||
|
||||
interface Props {
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const SSOKeyRotationQuery: FunctionComponent<Props> = ({ disabled }) => {
|
||||
const SSOSigningSecretRotationQuery: FunctionComponent<Props> = ({
|
||||
disabled,
|
||||
}) => {
|
||||
return (
|
||||
<QueryRenderer<QueryTypes>
|
||||
query={graphql`
|
||||
query SSOKeyRotationQuery {
|
||||
query SSOSigningSecretRotationQuery {
|
||||
settings {
|
||||
...SSOKeyRotationContainer_settings
|
||||
...SSOSigningSecretRotationContainer_settings
|
||||
}
|
||||
}
|
||||
`}
|
||||
@@ -38,7 +40,7 @@ const SSOKeyRotationQuery: FunctionComponent<Props> = ({ disabled }) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<SSOKeyRotationContainer
|
||||
<SSOSigningSecretRotationContainer
|
||||
settings={props.settings}
|
||||
disabled={disabled}
|
||||
/>
|
||||
@@ -48,4 +50,4 @@ const SSOKeyRotationQuery: FunctionComponent<Props> = ({ disabled }) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default SSOKeyRotationQuery;
|
||||
export default SSOSigningSecretRotationQuery;
|
||||
+5
-5
@@ -6,19 +6,19 @@ import { Flex, Icon, Tooltip, TooltipButton } from "coral-ui/components/v2";
|
||||
|
||||
import styles from "./StatusField.css";
|
||||
|
||||
export enum SSOKeyStatus {
|
||||
export enum SSOSigningSecretStatus {
|
||||
EXPIRED,
|
||||
EXPIRING,
|
||||
ACTIVE,
|
||||
}
|
||||
|
||||
interface Props {
|
||||
status: SSOKeyStatus;
|
||||
status: SSOSigningSecretStatus;
|
||||
}
|
||||
|
||||
const StatusField: FunctionComponent<Props> = ({ status }) => {
|
||||
switch (status) {
|
||||
case SSOKeyStatus.ACTIVE:
|
||||
case SSOSigningSecretStatus.ACTIVE:
|
||||
return (
|
||||
<Localized id="configure-auth-sso-rotate-statusActive">
|
||||
<span
|
||||
@@ -29,7 +29,7 @@ const StatusField: FunctionComponent<Props> = ({ status }) => {
|
||||
</span>
|
||||
</Localized>
|
||||
);
|
||||
case SSOKeyStatus.EXPIRING:
|
||||
case SSOSigningSecretStatus.EXPIRING:
|
||||
return (
|
||||
<Flex alignItems="center" justifyContent="center">
|
||||
<Flex
|
||||
@@ -68,7 +68,7 @@ const StatusField: FunctionComponent<Props> = ({ status }) => {
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
case SSOKeyStatus.EXPIRED:
|
||||
case SSOSigningSecretStatus.EXPIRED:
|
||||
return (
|
||||
<Flex alignItems="center" justifyContent="center">
|
||||
<Localized id="configure-auth-sso-rotate-statusExpired">
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
import { Localized } from "@fluent/react/compat";
|
||||
import { Match, Router, withRouter } from "found";
|
||||
import React, { FunctionComponent, useCallback } from "react";
|
||||
|
||||
import ConfigBox from "coral-admin/routes/Configure/ConfigBox";
|
||||
import Header from "coral-admin/routes/Configure/Header";
|
||||
import { urls } from "coral-framework/helpers";
|
||||
import { HorizontalGutter } from "coral-ui/components/v2";
|
||||
|
||||
import { ConfigureExternalModerationPhaseForm } from "../ConfigureExternalModerationPhaseForm";
|
||||
import ExperimentalExternalModerationPhaseCallOut from "../ExperimentalExternalModerationPhaseCallOut";
|
||||
|
||||
interface Props {
|
||||
router: Router;
|
||||
match: Match;
|
||||
}
|
||||
|
||||
const AddExternalModerationPhaseContainer: FunctionComponent<Props> = ({
|
||||
router,
|
||||
}) => {
|
||||
const onCancel = useCallback(() => {
|
||||
router.push(urls.admin.moderationPhases);
|
||||
}, [router]);
|
||||
|
||||
return (
|
||||
<HorizontalGutter size="double">
|
||||
<ExperimentalExternalModerationPhaseCallOut />
|
||||
<ConfigBox
|
||||
title={
|
||||
<Localized id="configure-moderationPhases-addExternalModerationPhase">
|
||||
<Header>Add external moderation phase</Header>
|
||||
</Localized>
|
||||
}
|
||||
>
|
||||
<ConfigureExternalModerationPhaseForm
|
||||
phase={null}
|
||||
onCancel={onCancel}
|
||||
/>
|
||||
</ConfigBox>
|
||||
</HorizontalGutter>
|
||||
);
|
||||
};
|
||||
|
||||
const enhanced = withRouter(AddExternalModerationPhaseContainer);
|
||||
|
||||
export default enhanced;
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
import React, { FunctionComponent } from "react";
|
||||
|
||||
import AddExternalModerationPhaseContainer from "./AddExternalModerationPhaseContainer";
|
||||
|
||||
const AddExternalModerationPhaseRoute: FunctionComponent = () => (
|
||||
<AddExternalModerationPhaseContainer />
|
||||
);
|
||||
|
||||
export default AddExternalModerationPhaseRoute;
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
export {
|
||||
default,
|
||||
default as AddExternalModerationPhaseRoute,
|
||||
} from "./AddExternalModerationPhaseRoute";
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
import { Localized } from "@fluent/react/compat";
|
||||
import React, { FunctionComponent } from "react";
|
||||
import { graphql } from "react-relay";
|
||||
|
||||
import ConfigBox from "coral-admin/routes/Configure/ConfigBox";
|
||||
import Header from "coral-admin/routes/Configure/Header";
|
||||
import { withFragmentContainer } from "coral-framework/lib/relay";
|
||||
import { HorizontalGutter } from "coral-ui/components/v2";
|
||||
|
||||
import { ConfigureExternalModerationPhaseContainer_phase } from "coral-admin/__generated__/ConfigureExternalModerationPhaseContainer_phase.graphql";
|
||||
|
||||
import ExperimentalExternalModerationPhaseCallOut from "../ExperimentalExternalModerationPhaseCallOut";
|
||||
import ExternalModerationPhaseDangerZone from "./ExternalModerationPhaseDangerZone";
|
||||
import ExternalModerationPhaseDetails from "./ExternalModerationPhaseDetails";
|
||||
import ExternalModerationPhaseStatus from "./ExternalModerationPhaseStatus";
|
||||
|
||||
interface Props {
|
||||
phase: ConfigureExternalModerationPhaseContainer_phase;
|
||||
}
|
||||
|
||||
const ConfigureExternalModerationPhaseContainer: FunctionComponent<Props> = ({
|
||||
phase,
|
||||
}) => {
|
||||
return (
|
||||
<HorizontalGutter
|
||||
size="double"
|
||||
data-testid="external-moderation-phases-container"
|
||||
>
|
||||
<ExperimentalExternalModerationPhaseCallOut />
|
||||
<ConfigBox
|
||||
title={
|
||||
<Localized id="configure-moderationPhases-configureExternalModerationPhase">
|
||||
<Header htmlFor="configure-moderationPhases-header.title">
|
||||
Configure external moderation phase
|
||||
</Header>
|
||||
</Localized>
|
||||
}
|
||||
>
|
||||
<ExternalModerationPhaseDetails phase={phase} />
|
||||
<ExternalModerationPhaseStatus phase={phase} />
|
||||
<ExternalModerationPhaseDangerZone phase={phase} />
|
||||
</ConfigBox>
|
||||
</HorizontalGutter>
|
||||
);
|
||||
};
|
||||
|
||||
const enhanced = withFragmentContainer<Props>({
|
||||
phase: graphql`
|
||||
fragment ConfigureExternalModerationPhaseContainer_phase on ExternalModerationPhase {
|
||||
...ExternalModerationPhaseDetails_phase
|
||||
...ExternalModerationPhaseDangerZone_phase
|
||||
...ExternalModerationPhaseStatus_phase
|
||||
}
|
||||
`,
|
||||
})(ConfigureExternalModerationPhaseContainer);
|
||||
|
||||
export default enhanced;
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
import { Localized } from "@fluent/react/compat";
|
||||
import React, { FunctionComponent } from "react";
|
||||
import { graphql } from "react-relay";
|
||||
|
||||
import { withRouteConfig } from "coral-framework/lib/router";
|
||||
import { CallOut, Delay, Spinner } from "coral-ui/components/v2";
|
||||
|
||||
import { ConfigureExternalModerationPhaseRouteQueryResponse } from "coral-admin/__generated__/ConfigureExternalModerationPhaseRouteQuery.graphql";
|
||||
|
||||
import ConfigureExternalModerationPhaseContainer from "./ConfigureExternalModerationPhaseContainer";
|
||||
|
||||
interface Props {
|
||||
data: ConfigureExternalModerationPhaseRouteQueryResponse | null;
|
||||
}
|
||||
|
||||
const ConfigureExternalModerationPhaseRoute: FunctionComponent<Props> = ({
|
||||
data,
|
||||
}) => {
|
||||
if (!data) {
|
||||
return (
|
||||
<Delay>
|
||||
<Spinner />
|
||||
</Delay>
|
||||
);
|
||||
}
|
||||
|
||||
if (!data.externalModerationPhase) {
|
||||
return (
|
||||
<Localized id="configure-moderationPhases-phaseNotFound">
|
||||
<CallOut color="error" fullWidth>
|
||||
External moderation phase not found
|
||||
</CallOut>
|
||||
</Localized>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ConfigureExternalModerationPhaseContainer
|
||||
phase={data.externalModerationPhase}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const enhanced = withRouteConfig<Props>({
|
||||
query: graphql`
|
||||
query ConfigureExternalModerationPhaseRouteQuery($phaseID: ID!) {
|
||||
externalModerationPhase(id: $phaseID) {
|
||||
...ConfigureExternalModerationPhaseContainer_phase
|
||||
}
|
||||
}
|
||||
`,
|
||||
cacheConfig: { force: true },
|
||||
prepareVariables: (params, match) => {
|
||||
return {
|
||||
phaseID: match.params.phaseID,
|
||||
};
|
||||
},
|
||||
})(ConfigureExternalModerationPhaseRoute);
|
||||
|
||||
export default enhanced;
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
import { graphql } from "react-relay";
|
||||
import { Environment } from "relay-runtime";
|
||||
|
||||
import {
|
||||
commitMutationPromiseNormalized,
|
||||
createMutation,
|
||||
MutationInput,
|
||||
} from "coral-framework/lib/relay";
|
||||
|
||||
import { DeleteExternalModerationPhaseMutation as MutationTypes } from "coral-admin/__generated__/DeleteExternalModerationPhaseMutation.graphql";
|
||||
|
||||
let clientMutationId = 0;
|
||||
|
||||
const DeleteExternalModerationPhaseMutation = createMutation(
|
||||
"deleteExternalModerationPhase",
|
||||
(environment: Environment, { id }: MutationInput<MutationTypes>) =>
|
||||
commitMutationPromiseNormalized<MutationTypes>(environment, {
|
||||
mutation: graphql`
|
||||
mutation DeleteExternalModerationPhaseMutation(
|
||||
$input: DeleteExternalModerationPhaseInput!
|
||||
) {
|
||||
deleteExternalModerationPhase(input: $input) {
|
||||
phase {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
input: {
|
||||
id,
|
||||
clientMutationId: (clientMutationId++).toString(),
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
export default DeleteExternalModerationPhaseMutation;
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
import { graphql } from "react-relay";
|
||||
import { Environment } from "relay-runtime";
|
||||
|
||||
import {
|
||||
commitMutationPromiseNormalized,
|
||||
createMutation,
|
||||
MutationInput,
|
||||
} from "coral-framework/lib/relay";
|
||||
|
||||
import { DisableExternalModerationPhaseMutation as MutationTypes } from "coral-admin/__generated__/DisableExternalModerationPhaseMutation.graphql";
|
||||
|
||||
let clientMutationId = 0;
|
||||
|
||||
const DisableExternalModerationPhaseMutation = createMutation(
|
||||
"disableExternalModerationPhase",
|
||||
(environment: Environment, input: MutationInput<MutationTypes>) =>
|
||||
commitMutationPromiseNormalized<MutationTypes>(environment, {
|
||||
mutation: graphql`
|
||||
mutation DisableExternalModerationPhaseMutation(
|
||||
$input: DisableExternalModerationPhaseInput!
|
||||
) {
|
||||
disableExternalModerationPhase(input: $input) {
|
||||
phase {
|
||||
...ConfigureExternalModerationPhaseContainer_phase
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
input: {
|
||||
...input,
|
||||
clientMutationId: (clientMutationId++).toString(),
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
export default DisableExternalModerationPhaseMutation;
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
import { graphql } from "react-relay";
|
||||
import { Environment } from "relay-runtime";
|
||||
|
||||
import {
|
||||
commitMutationPromiseNormalized,
|
||||
createMutation,
|
||||
MutationInput,
|
||||
} from "coral-framework/lib/relay";
|
||||
|
||||
import { EnableExternalModerationPhaseMutation as MutationTypes } from "coral-admin/__generated__/EnableExternalModerationPhaseMutation.graphql";
|
||||
|
||||
let clientMutationId = 0;
|
||||
|
||||
const EnableExternalModerationPhaseMutation = createMutation(
|
||||
"enableExternalModerationPhase",
|
||||
(environment: Environment, input: MutationInput<MutationTypes>) =>
|
||||
commitMutationPromiseNormalized<MutationTypes>(environment, {
|
||||
mutation: graphql`
|
||||
mutation EnableExternalModerationPhaseMutation(
|
||||
$input: EnableExternalModerationPhaseInput!
|
||||
) {
|
||||
enableExternalModerationPhase(input: $input) {
|
||||
phase {
|
||||
...ConfigureExternalModerationPhaseContainer_phase
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
input: {
|
||||
...input,
|
||||
clientMutationId: (clientMutationId++).toString(),
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
export default EnableExternalModerationPhaseMutation;
|
||||
+185
@@ -0,0 +1,185 @@
|
||||
import { Localized } from "@fluent/react/compat";
|
||||
import { Match, Router, withRouter } from "found";
|
||||
import React, { FunctionComponent, useCallback, useState } from "react";
|
||||
import { graphql } from "react-relay";
|
||||
|
||||
import Subheader from "coral-admin/routes/Configure/Subheader";
|
||||
import { urls } from "coral-framework/helpers";
|
||||
import { useCoralContext } from "coral-framework/lib/bootstrap";
|
||||
import { getMessage } from "coral-framework/lib/i18n";
|
||||
import { useMutation, withFragmentContainer } from "coral-framework/lib/relay";
|
||||
import {
|
||||
Button,
|
||||
FormField,
|
||||
FormFieldDescription,
|
||||
Label,
|
||||
} from "coral-ui/components/v2";
|
||||
|
||||
import { ExternalModerationPhaseDangerZone_phase } from "coral-admin/__generated__/ExternalModerationPhaseDangerZone_phase.graphql";
|
||||
|
||||
import DeleteExternalModerationPhaseMutation from "./DeleteExternalModerationPhaseMutation";
|
||||
import DisableExternalModerationPhaseMutation from "./DisableExternalModerationPhaseMutation";
|
||||
import EnableExternalModerationPhaseMutation from "./EnableExternalModerationPhaseMutation";
|
||||
import RotateSigningSecretModal from "./RotateSigningSecretModal";
|
||||
|
||||
interface Props {
|
||||
phase: ExternalModerationPhaseDangerZone_phase;
|
||||
router: Router;
|
||||
match: Match;
|
||||
}
|
||||
|
||||
const ExternalModerationPhaseDangerZone: FunctionComponent<Props> = ({
|
||||
phase,
|
||||
router,
|
||||
}) => {
|
||||
const { localeBundles } = useCoralContext();
|
||||
const enableExternalModerationPhase = useMutation(
|
||||
EnableExternalModerationPhaseMutation
|
||||
);
|
||||
const disableExternalModerationPhase = useMutation(
|
||||
DisableExternalModerationPhaseMutation
|
||||
);
|
||||
const deleteExternalModerationPhase = useMutation(
|
||||
DeleteExternalModerationPhaseMutation
|
||||
);
|
||||
|
||||
const [rotateSecretOpen, setRotateSecretOpen] = useState<boolean>(false);
|
||||
const onRotateSecret = useCallback(async () => {
|
||||
setRotateSecretOpen(true);
|
||||
}, []);
|
||||
const onHideRotateSecret = useCallback(async () => {
|
||||
setRotateSecretOpen(false);
|
||||
}, [setRotateSecretOpen]);
|
||||
|
||||
const onEnable = useCallback(async () => {
|
||||
const message = getMessage(
|
||||
localeBundles,
|
||||
"configure-moderationPhases-confirmEnable",
|
||||
"Enabling the external moderation phase will start to send moderation queries to this URL. Are you sure you want to continue?"
|
||||
);
|
||||
|
||||
if (window.confirm(message)) {
|
||||
await enableExternalModerationPhase({ id: phase.id });
|
||||
}
|
||||
}, [phase, enableExternalModerationPhase]);
|
||||
const onDisable = useCallback(async () => {
|
||||
const message = getMessage(
|
||||
localeBundles,
|
||||
"configure-moderationPhases-confirmDisable",
|
||||
"Disabling this external moderation phase will stop any new moderation queries from being sent to this URL. Are you sure you want to continue?"
|
||||
);
|
||||
|
||||
if (window.confirm(message)) {
|
||||
await disableExternalModerationPhase({ id: phase.id });
|
||||
}
|
||||
}, [phase, disableExternalModerationPhase]);
|
||||
|
||||
const onDelete = useCallback(async () => {
|
||||
const message = getMessage(
|
||||
localeBundles,
|
||||
"configure-moderationPhases-confirmDelete",
|
||||
"Deleting this external moderation phase will stop any new moderation queries from being sent to this URL and will remove all the associated settings. Are you sure you want to continue?"
|
||||
);
|
||||
|
||||
if (window.confirm(message)) {
|
||||
await deleteExternalModerationPhase({ id: phase.id });
|
||||
|
||||
// Send the user back to the webhook endpoints listing.
|
||||
router.push(urls.admin.moderationPhases);
|
||||
}
|
||||
}, [phase, disableExternalModerationPhase, router]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Localized id="configure-moderationPhases-dangerZone">
|
||||
<Subheader>Danger Zone</Subheader>
|
||||
</Localized>
|
||||
<FormField>
|
||||
<Localized id="configure-moderationPhases-rotateSigningSecret">
|
||||
<Label>Rotate signing secret</Label>
|
||||
</Localized>
|
||||
<Localized id="configure-moderationPhases-rotateSigningSecretDescription">
|
||||
<FormFieldDescription>
|
||||
Rotating the signing secret will allow to you to safely replace a
|
||||
signing secret used in production with a delay.
|
||||
</FormFieldDescription>
|
||||
</Localized>
|
||||
<Localized id="configure-moderationPhases-rotateSigningSecretButton">
|
||||
<Button color="alert" onClick={onRotateSecret}>
|
||||
Rotate signing secret
|
||||
</Button>
|
||||
</Localized>
|
||||
</FormField>
|
||||
<RotateSigningSecretModal
|
||||
phaseID={phase.id}
|
||||
onHide={onHideRotateSecret}
|
||||
open={rotateSecretOpen}
|
||||
/>
|
||||
{phase.enabled ? (
|
||||
<FormField>
|
||||
<Localized id="configure-moderationPhases-disableExternalModerationPhase">
|
||||
<Label>Disable external moderation phase</Label>
|
||||
</Localized>
|
||||
<Localized id="configure-moderationPhases-disableExternalModerationPhaseDescription">
|
||||
<FormFieldDescription>
|
||||
This external moderation phase is current enabled. By disabling,
|
||||
no new moderation queries will be sent to the URL provided.
|
||||
</FormFieldDescription>
|
||||
</Localized>
|
||||
<Localized id="configure-moderationPhases-disableExternalModerationPhaseButton">
|
||||
<Button color="alert" onClick={onDisable}>
|
||||
Disable phase
|
||||
</Button>
|
||||
</Localized>
|
||||
</FormField>
|
||||
) : (
|
||||
<FormField>
|
||||
<Localized id="configure-moderationPhases-enableExternalModerationPhase">
|
||||
<Label>Enable external moderation phase</Label>
|
||||
</Localized>
|
||||
<Localized id="configure-moderationPhases-enableExternalModerationPhaseDescription">
|
||||
<FormFieldDescription>
|
||||
This external moderation phase is currently disabled. By enabling,
|
||||
new moderation queries will be sent to the URL provided.
|
||||
</FormFieldDescription>
|
||||
</Localized>
|
||||
<Localized id="configure-moderationPhases-enableExternalModerationPhaseButton">
|
||||
<Button color="regular" onClick={onEnable}>
|
||||
Enable phase
|
||||
</Button>
|
||||
</Localized>
|
||||
</FormField>
|
||||
)}
|
||||
<FormField>
|
||||
<Localized id="configure-moderationPhases-deleteExternalModerationPhase">
|
||||
<Label>Delete external moderation phase</Label>
|
||||
</Localized>
|
||||
<Localized id="configure-moderationPhases-deleteExternalModerationPhaseDescription">
|
||||
<FormFieldDescription>
|
||||
Deleting this external moderation phase will stop any new moderation
|
||||
queries from being sent to this URL and will remove all the
|
||||
associated settings.
|
||||
</FormFieldDescription>
|
||||
</Localized>
|
||||
<Localized id="configure-moderationPhases-deleteExternalModerationPhaseButton">
|
||||
<Button color="alert" onClick={onDelete}>
|
||||
Delete phase
|
||||
</Button>
|
||||
</Localized>
|
||||
</FormField>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const enhanced = withRouter(
|
||||
withFragmentContainer<Props>({
|
||||
phase: graphql`
|
||||
fragment ExternalModerationPhaseDangerZone_phase on ExternalModerationPhase {
|
||||
id
|
||||
enabled
|
||||
}
|
||||
`,
|
||||
})(ExternalModerationPhaseDangerZone)
|
||||
);
|
||||
|
||||
export default enhanced;
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
import { Localized } from "@fluent/react/compat";
|
||||
import React, { FunctionComponent } from "react";
|
||||
import { graphql } from "react-relay";
|
||||
|
||||
import Subheader from "coral-admin/routes/Configure/Subheader";
|
||||
import { withFragmentContainer } from "coral-framework/lib/relay";
|
||||
|
||||
import { ExternalModerationPhaseDetails_phase } from "coral-admin/__generated__/ExternalModerationPhaseDetails_phase.graphql";
|
||||
|
||||
import ConfigureExternalModerationPhaseForm from "../ConfigureExternalModerationPhaseForm";
|
||||
|
||||
interface Props {
|
||||
phase: ExternalModerationPhaseDetails_phase;
|
||||
}
|
||||
|
||||
const ExternalModerationPhaseDetails: FunctionComponent<Props> = ({
|
||||
phase,
|
||||
}) => (
|
||||
<>
|
||||
<Localized id="configure-moderationPhases-phaseDetails">
|
||||
<Subheader>Phase details</Subheader>
|
||||
</Localized>
|
||||
<ConfigureExternalModerationPhaseForm phase={phase} />
|
||||
</>
|
||||
);
|
||||
|
||||
const enhanced = withFragmentContainer<Props>({
|
||||
phase: graphql`
|
||||
fragment ExternalModerationPhaseDetails_phase on ExternalModerationPhase {
|
||||
...ConfigureExternalModerationPhaseForm_phase
|
||||
}
|
||||
`,
|
||||
})(ExternalModerationPhaseDetails);
|
||||
|
||||
export default enhanced;
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
import { Localized } from "@fluent/react/compat";
|
||||
import React, { FunctionComponent } from "react";
|
||||
import { graphql } from "react-relay";
|
||||
|
||||
import Subheader from "coral-admin/routes/Configure/Subheader";
|
||||
import { CopyButton } from "coral-framework/components";
|
||||
import { ExternalLink } from "coral-framework/lib/i18n/components";
|
||||
import { withFragmentContainer } from "coral-framework/lib/relay";
|
||||
import {
|
||||
Flex,
|
||||
FormField,
|
||||
FormFieldDescription,
|
||||
HelperText,
|
||||
Label,
|
||||
PasswordField,
|
||||
} from "coral-ui/components/v2";
|
||||
|
||||
import { ExternalModerationPhaseStatus_phase } from "coral-admin/__generated__/ExternalModerationPhaseStatus_phase.graphql";
|
||||
|
||||
import StatusMarker from "../StatusMarker";
|
||||
|
||||
interface Props {
|
||||
phase: ExternalModerationPhaseStatus_phase;
|
||||
}
|
||||
|
||||
const ExternalModerationPhaseStatus: FunctionComponent<Props> = ({ phase }) => {
|
||||
return (
|
||||
<>
|
||||
<Localized id="configure-moderationPhases-phaseStatus">
|
||||
<Subheader>Phase status</Subheader>
|
||||
</Localized>
|
||||
<FormField>
|
||||
<Localized id="configure-moderationPhases-status">
|
||||
<Label>Status</Label>
|
||||
</Localized>
|
||||
<StatusMarker enabled={phase.enabled} />
|
||||
</FormField>
|
||||
<FormField>
|
||||
<Localized id="configure-moderationPhases-signingSecret">
|
||||
<Label>Signing secret</Label>
|
||||
</Localized>
|
||||
<Localized
|
||||
id="configure-moderationPhases-signingSecretDescription"
|
||||
externalLink={
|
||||
<ExternalLink href="https://github.com/coralproject/talk/blob/master/EXTERNAL_MODERATION_PHASES.md#request-signing" />
|
||||
}
|
||||
>
|
||||
<FormFieldDescription>
|
||||
The following signing secret is used to sign request payloads sent
|
||||
to the URL. To learn more about webhook signing, visit our{" "}
|
||||
<ExternalLink href="https://github.com/coralproject/talk/blob/master/EXTERNAL_MODERATION_PHASES.md#request-signing">
|
||||
docs
|
||||
</ExternalLink>
|
||||
.
|
||||
</FormFieldDescription>
|
||||
</Localized>
|
||||
<Flex direction="row" itemGutter="half" alignItems="center">
|
||||
<PasswordField
|
||||
value={phase.signingSecret.secret}
|
||||
fullWidth
|
||||
readOnly
|
||||
/>
|
||||
<CopyButton text={phase.signingSecret.secret} />
|
||||
</Flex>
|
||||
<Localized
|
||||
id="configure-moderationPhases-generatedAt"
|
||||
$date={new Date(phase.signingSecret.createdAt)}
|
||||
>
|
||||
<HelperText>
|
||||
KEY GENERATED AT: {phase.signingSecret.createdAt}
|
||||
</HelperText>
|
||||
</Localized>
|
||||
</FormField>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const enhanced = withFragmentContainer<Props>({
|
||||
phase: graphql`
|
||||
fragment ExternalModerationPhaseStatus_phase on ExternalModerationPhase {
|
||||
id
|
||||
enabled
|
||||
signingSecret {
|
||||
secret
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
`,
|
||||
})(ExternalModerationPhaseStatus);
|
||||
|
||||
export default enhanced;
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
import { graphql } from "react-relay";
|
||||
import { Environment } from "relay-runtime";
|
||||
|
||||
import {
|
||||
commitMutationPromiseNormalized,
|
||||
createMutation,
|
||||
MutationInput,
|
||||
} from "coral-framework/lib/relay";
|
||||
|
||||
import { RotateExternalModerationPhaseSigningSecretMutation as MutationTypes } from "coral-admin/__generated__/RotateExternalModerationPhaseSigningSecretMutation.graphql";
|
||||
|
||||
let clientMutationId = 0;
|
||||
|
||||
const RotateExternalModerationPhaseSigningSecretMutation = createMutation(
|
||||
"rotateExternalModerationPhaseSigningSecret",
|
||||
(environment: Environment, input: MutationInput<MutationTypes>) =>
|
||||
commitMutationPromiseNormalized<MutationTypes>(environment, {
|
||||
mutation: graphql`
|
||||
mutation RotateExternalModerationPhaseSigningSecretMutation(
|
||||
$input: RotateExternalModerationPhaseSigningSecretInput!
|
||||
) {
|
||||
rotateExternalModerationPhaseSigningSecret(input: $input) {
|
||||
phase {
|
||||
...ConfigureExternalModerationPhaseContainer_phase
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
input: {
|
||||
...input,
|
||||
clientMutationId: (clientMutationId++).toString(),
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
export default RotateExternalModerationPhaseSigningSecretMutation;
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
.root {
|
||||
width: 500px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: var(--v2-font-size-5);
|
||||
font-family: var(--v2-font-family-primary);
|
||||
font-weight: var(--v2-font-weight-primary-semi-bold);
|
||||
line-height: var(--v2-line-height-title);
|
||||
}
|
||||
+170
@@ -0,0 +1,170 @@
|
||||
import { Localized } from "@fluent/react/compat";
|
||||
import { FORM_ERROR } from "final-form";
|
||||
import React, { FunctionComponent, useCallback } from "react";
|
||||
import { Field, Form } from "react-final-form";
|
||||
|
||||
import { useNotification } from "coral-admin/App/GlobalNotification";
|
||||
import { InvalidRequestError } from "coral-framework/lib/errors";
|
||||
import { useMutation } from "coral-framework/lib/relay";
|
||||
import {
|
||||
Button,
|
||||
CallOut,
|
||||
Card,
|
||||
CardCloseButton,
|
||||
Flex,
|
||||
FormField,
|
||||
HelperText,
|
||||
HorizontalGutter,
|
||||
Label,
|
||||
Modal,
|
||||
Option,
|
||||
SelectField,
|
||||
} from "coral-ui/components/v2";
|
||||
import AppNotification from "coral-ui/components/v2/AppNotification";
|
||||
|
||||
import RotateExternalModerationPhaseSigningSecretMutation from "./RotateExternalModerationPhaseSigningSecretMutation";
|
||||
|
||||
import styles from "./RotateSigningSecretModal.css";
|
||||
|
||||
interface Props {
|
||||
phaseID: string;
|
||||
onHide: () => void;
|
||||
open: boolean;
|
||||
}
|
||||
|
||||
const RotateWebhookEndpointSigningSecretModal: FunctionComponent<Props> = ({
|
||||
onHide,
|
||||
open,
|
||||
phaseID,
|
||||
}) => {
|
||||
const rotateExternalModerationPhaseSigningSecret = useMutation(
|
||||
RotateExternalModerationPhaseSigningSecretMutation
|
||||
);
|
||||
const { setMessage, clearMessage } = useNotification();
|
||||
const onRotateSecret = useCallback(
|
||||
async ({ inactiveIn: inactiveInString }) => {
|
||||
try {
|
||||
const inactiveIn = parseInt(inactiveInString, 10);
|
||||
await rotateExternalModerationPhaseSigningSecret({
|
||||
id: phaseID,
|
||||
inactiveIn,
|
||||
});
|
||||
|
||||
// Post a notification about the successful change.
|
||||
setMessage(
|
||||
<Localized id="configure-moderationPhases-rotateSigningSecretSuccessUseNewSecret">
|
||||
<AppNotification icon="check_circle_outline" onClose={clearMessage}>
|
||||
External moderation phase signing secret has been rotated. Please
|
||||
ensure you update your integrations to use the new secret below.
|
||||
</AppNotification>
|
||||
</Localized>
|
||||
);
|
||||
|
||||
// Scroll after a zero timeout because chrome won't scroll otherwise.
|
||||
setTimeout(() => window.scroll(0, 0), 0);
|
||||
} catch (err) {
|
||||
if (err instanceof InvalidRequestError) {
|
||||
return err.invalidArgs;
|
||||
}
|
||||
return { [FORM_ERROR]: err.message };
|
||||
}
|
||||
|
||||
// Dismiss the modal.
|
||||
onHide();
|
||||
|
||||
return;
|
||||
},
|
||||
[phaseID, rotateExternalModerationPhaseSigningSecret]
|
||||
);
|
||||
|
||||
return (
|
||||
<Modal open={open}>
|
||||
{({ firstFocusableRef, lastFocusableRef }) => (
|
||||
<Card className={styles.root}>
|
||||
<Flex justifyContent="flex-end">
|
||||
<CardCloseButton onClick={onHide} ref={firstFocusableRef} />
|
||||
</Flex>
|
||||
<Form onSubmit={onRotateSecret} initialValues={{ inactiveIn: 0 }}>
|
||||
{({ handleSubmit, submitting, submitError }) => (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<HorizontalGutter size="double">
|
||||
<Localized id="configure-moderationPhases-rotateSigningSecret">
|
||||
<h2 className={styles.title}>Rotate signing secret</h2>
|
||||
</Localized>
|
||||
{submitError && (
|
||||
<CallOut color="error" fullWidth>
|
||||
{submitError}
|
||||
</CallOut>
|
||||
)}
|
||||
<Localized id="configure-moderationPhases-rotateSigningSecretHelper">
|
||||
<HelperText>
|
||||
After it expires, signatures will no longer be generated
|
||||
with the old secret.
|
||||
</HelperText>
|
||||
</Localized>
|
||||
<Field name="inactiveIn">
|
||||
{({ input }) => (
|
||||
<FormField>
|
||||
<Localized id="configure-moderationPhases-expiresOldSecret">
|
||||
<Label>Expire the old secret</Label>
|
||||
</Localized>
|
||||
<SelectField {...input} fullWidth>
|
||||
<Localized id="configure-moderationPhases-expiresOldSecretImmediately">
|
||||
<Option value="0">Immediately</Option>
|
||||
</Localized>
|
||||
<Localized
|
||||
id="configure-moderationPhases-expiresOldSecretHoursFromNow"
|
||||
$hours={1}
|
||||
>
|
||||
<Option value="3600">1 hour from now</Option>
|
||||
</Localized>
|
||||
<Localized
|
||||
id="configure-moderationPhases-expiresOldSecretHoursFromNow"
|
||||
$hours={2}
|
||||
>
|
||||
<Option value="7200">2 hours from now</Option>
|
||||
</Localized>
|
||||
<Localized
|
||||
id="configure-moderationPhases-expiresOldSecretHoursFromNow"
|
||||
$hours={12}
|
||||
>
|
||||
<Option value="43200">12 hours from now</Option>
|
||||
</Localized>
|
||||
<Localized
|
||||
id="configure-moderationPhases-expiresOldSecretHoursFromNow"
|
||||
$hours={24}
|
||||
>
|
||||
<Option value="86400">24 hours from now</Option>
|
||||
</Localized>
|
||||
</SelectField>
|
||||
</FormField>
|
||||
)}
|
||||
</Field>
|
||||
<Flex direction="row" justifyContent="flex-end" itemGutter>
|
||||
<Localized id="configure-moderationPhases-cancelButton">
|
||||
<Button color="regular" onClick={onHide}>
|
||||
Cancel
|
||||
</Button>
|
||||
</Localized>
|
||||
<Localized id="configure-moderationPhases-rotateSigningSecretButton">
|
||||
<Button
|
||||
type="submit"
|
||||
color="alert"
|
||||
disabled={submitting}
|
||||
ref={lastFocusableRef}
|
||||
>
|
||||
Rotate signing secret
|
||||
</Button>
|
||||
</Localized>
|
||||
</Flex>
|
||||
</HorizontalGutter>
|
||||
</form>
|
||||
)}
|
||||
</Form>
|
||||
</Card>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default RotateWebhookEndpointSigningSecretModal;
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
export {
|
||||
default,
|
||||
default as ConfigureWebhookEndpointRoute,
|
||||
} from "./ConfigureExternalModerationPhaseRoute";
|
||||
+240
@@ -0,0 +1,240 @@
|
||||
import { Localized } from "@fluent/react/compat";
|
||||
import { FORM_ERROR } from "final-form";
|
||||
import { Match, Router, withRouter } from "found";
|
||||
import React, { FunctionComponent, useCallback } from "react";
|
||||
import { Field, Form } from "react-final-form";
|
||||
import { graphql } from "react-relay";
|
||||
|
||||
import getExternalModerationPhaseLink from "coral-admin/helpers/getExternalModerationPhaseLink";
|
||||
import { InvalidRequestError } from "coral-framework/lib/errors";
|
||||
import {
|
||||
colorFromMeta,
|
||||
parseInteger,
|
||||
ValidationMessage,
|
||||
} from "coral-framework/lib/form";
|
||||
import { useMutation, withFragmentContainer } from "coral-framework/lib/relay";
|
||||
import {
|
||||
composeValidators,
|
||||
required,
|
||||
validateURL,
|
||||
validateWholeNumberBetween,
|
||||
} from "coral-framework/lib/validation";
|
||||
import { GQLCOMMENT_BODY_FORMAT } from "coral-framework/schema";
|
||||
import {
|
||||
Button,
|
||||
CallOut,
|
||||
Flex,
|
||||
FormField,
|
||||
HelperText,
|
||||
HorizontalGutter,
|
||||
Label,
|
||||
Option,
|
||||
SelectField,
|
||||
TextField,
|
||||
} from "coral-ui/components/v2";
|
||||
|
||||
import { ConfigureExternalModerationPhaseForm_phase } from "coral-admin/__generated__/ConfigureExternalModerationPhaseForm_phase.graphql";
|
||||
|
||||
import CreateExternalModerationPhaseMutation from "./CreateExternalModerationPhaseMutation";
|
||||
import UpdateExternalModerationPhaseMutation from "./UpdateExternalModerationPhaseMutation";
|
||||
|
||||
interface Props {
|
||||
onCancel?: () => void;
|
||||
router: Router;
|
||||
match: Match;
|
||||
phase: ConfigureExternalModerationPhaseForm_phase | null;
|
||||
}
|
||||
|
||||
const initialValues = (phase?: any) =>
|
||||
phase
|
||||
? phase
|
||||
: {
|
||||
name: "",
|
||||
url: "",
|
||||
timeout: 200,
|
||||
format: "HTML",
|
||||
};
|
||||
|
||||
const ConfigureExternalModerationPhaseForm: FunctionComponent<Props> = ({
|
||||
onCancel,
|
||||
phase,
|
||||
router,
|
||||
}) => {
|
||||
const create = useMutation(CreateExternalModerationPhaseMutation);
|
||||
const update = useMutation(UpdateExternalModerationPhaseMutation);
|
||||
const onSubmit = useCallback(
|
||||
async (values) => {
|
||||
try {
|
||||
if (phase) {
|
||||
// The external moderation phase was defined, update it.
|
||||
await update(values);
|
||||
} else {
|
||||
// The external moderation phase wasn't defined, create it.
|
||||
const result = await create(values);
|
||||
|
||||
// Redirect the user to the new external moderation phase page.
|
||||
router.push(getExternalModerationPhaseLink(result.phase.id));
|
||||
|
||||
// We don't need to close this modal because we are navigating...
|
||||
}
|
||||
|
||||
return;
|
||||
} catch (err) {
|
||||
if (err instanceof InvalidRequestError) {
|
||||
return err.invalidArgs;
|
||||
}
|
||||
return { [FORM_ERROR]: err.message };
|
||||
}
|
||||
},
|
||||
[phase, create, update, router]
|
||||
);
|
||||
|
||||
return (
|
||||
<Form onSubmit={onSubmit} initialValues={initialValues(phase)}>
|
||||
{({ handleSubmit, submitting, submitError, pristine }) => (
|
||||
<form autoComplete="off" onSubmit={handleSubmit}>
|
||||
<HorizontalGutter size="double">
|
||||
{submitError && (
|
||||
<CallOut color="error" fullWidth>
|
||||
{submitError}
|
||||
</CallOut>
|
||||
)}
|
||||
<Field name="name" validate={composeValidators(required)}>
|
||||
{({ input, meta }) => (
|
||||
<FormField>
|
||||
<Localized id="configure-moderationPhases-name">
|
||||
<Label>Name</Label>
|
||||
</Localized>
|
||||
<TextField {...input} color={colorFromMeta(meta)} fullWidth />
|
||||
<ValidationMessage meta={meta} fullWidth />
|
||||
</FormField>
|
||||
)}
|
||||
</Field>
|
||||
<Field
|
||||
name="url"
|
||||
validate={composeValidators(required, validateURL)}
|
||||
>
|
||||
{({ input, meta }) => (
|
||||
<FormField>
|
||||
<Localized id="configure-moderationPhases-endpointURL">
|
||||
<Label>Callback URL</Label>
|
||||
</Localized>
|
||||
<Localized id="configure-moderationPhases-endpointURL-details">
|
||||
<HelperText>
|
||||
The URL that Coral moderation requests will be POST'ed to.
|
||||
The provided URL must respond within the designated
|
||||
timeout or the decision of the moderation action will be
|
||||
skipped.
|
||||
</HelperText>
|
||||
</Localized>
|
||||
<TextField
|
||||
{...input}
|
||||
placeholder="https://"
|
||||
color={colorFromMeta(meta)}
|
||||
fullWidth
|
||||
/>
|
||||
<ValidationMessage meta={meta} fullWidth />
|
||||
</FormField>
|
||||
)}
|
||||
</Field>
|
||||
<Field
|
||||
name="timeout"
|
||||
parse={parseInteger}
|
||||
validate={composeValidators(
|
||||
required,
|
||||
validateWholeNumberBetween(100, 10000)
|
||||
)}
|
||||
>
|
||||
{({ input, meta }) => (
|
||||
<FormField>
|
||||
<Localized id="configure-moderationPhases-timeout">
|
||||
<Label>Timeout</Label>
|
||||
</Localized>
|
||||
<Localized id="configure-moderationPhases-timeout-details">
|
||||
<HelperText>
|
||||
The time that Coral will wait for your moderation response
|
||||
in milliseconds.
|
||||
</HelperText>
|
||||
</Localized>
|
||||
<TextField
|
||||
{...input}
|
||||
type="number"
|
||||
color={colorFromMeta(meta)}
|
||||
fullWidth
|
||||
/>
|
||||
<ValidationMessage meta={meta} fullWidth />
|
||||
</FormField>
|
||||
)}
|
||||
</Field>
|
||||
<Field name="format" validate={composeValidators(required)}>
|
||||
{({ input, meta }) => (
|
||||
<FormField>
|
||||
<Localized id="configure-moderationPhases-format">
|
||||
<Label>Comment Body Format</Label>
|
||||
</Localized>
|
||||
<Localized id="configure-moderationPhases-format-details">
|
||||
<HelperText>
|
||||
The format that Coral will send the comment body in. By
|
||||
default, Coral will send the comment in the original HTML
|
||||
encoded format. If "Plain Text" is selected, then the HTML
|
||||
stripped version will be sent instead.
|
||||
</HelperText>
|
||||
</Localized>
|
||||
<SelectField {...input} fullWidth>
|
||||
<Localized id="configure-moderationPhases-format-html">
|
||||
<Option value={GQLCOMMENT_BODY_FORMAT.HTML}>HTML</Option>
|
||||
</Localized>
|
||||
<Localized id="configure-moderationPhases-format-plain">
|
||||
<Option value={GQLCOMMENT_BODY_FORMAT.PLAIN_TEXT}>
|
||||
Plain Text
|
||||
</Option>
|
||||
</Localized>
|
||||
</SelectField>
|
||||
<ValidationMessage meta={meta} fullWidth />
|
||||
</FormField>
|
||||
)}
|
||||
</Field>
|
||||
<Flex direction="row" justifyContent="flex-end" itemGutter>
|
||||
{onCancel && (
|
||||
<Localized id="configure-moderationPhases-cancelButton">
|
||||
<Button type="button" color="mono" onClick={onCancel}>
|
||||
Cancel
|
||||
</Button>
|
||||
</Localized>
|
||||
)}
|
||||
{phase ? (
|
||||
<Localized id="configure-moderationPhases-updateExternalModerationPhaseButton">
|
||||
<Button type="submit" disabled={submitting || pristine}>
|
||||
Update details
|
||||
</Button>
|
||||
</Localized>
|
||||
) : (
|
||||
<Localized id="configure-moderationPhases-addExternalModerationPhase">
|
||||
<Button type="submit" disabled={submitting}>
|
||||
Add external moderation phase
|
||||
</Button>
|
||||
</Localized>
|
||||
)}
|
||||
</Flex>
|
||||
</HorizontalGutter>
|
||||
</form>
|
||||
)}
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
const enhanced = withRouter(
|
||||
withFragmentContainer<Props>({
|
||||
phase: graphql`
|
||||
fragment ConfigureExternalModerationPhaseForm_phase on ExternalModerationPhase {
|
||||
id
|
||||
name
|
||||
url
|
||||
timeout
|
||||
format
|
||||
}
|
||||
`,
|
||||
})(ConfigureExternalModerationPhaseForm)
|
||||
);
|
||||
|
||||
export default enhanced;
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
import { graphql } from "react-relay";
|
||||
import { Environment } from "relay-runtime";
|
||||
|
||||
import {
|
||||
commitMutationPromiseNormalized,
|
||||
createMutation,
|
||||
MutationInput,
|
||||
} from "coral-framework/lib/relay";
|
||||
|
||||
import { CreateExternalModerationPhaseMutation as MutationTypes } from "coral-admin/__generated__/CreateExternalModerationPhaseMutation.graphql";
|
||||
|
||||
let clientMutationId = 0;
|
||||
|
||||
const CreateExternalModerationPhaseMutation = createMutation(
|
||||
"createExternalModerationPhase",
|
||||
(environment: Environment, input: MutationInput<MutationTypes>) =>
|
||||
commitMutationPromiseNormalized<MutationTypes>(environment, {
|
||||
mutation: graphql`
|
||||
mutation CreateExternalModerationPhaseMutation(
|
||||
$input: CreateExternalModerationPhaseInput!
|
||||
) {
|
||||
createExternalModerationPhase(input: $input) {
|
||||
phase {
|
||||
id
|
||||
}
|
||||
settings {
|
||||
...ModerationPhasesConfigContainer_settings
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
input: {
|
||||
...input,
|
||||
clientMutationId: (clientMutationId++).toString(),
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
export default CreateExternalModerationPhaseMutation;
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
import { graphql } from "react-relay";
|
||||
import { Environment } from "relay-runtime";
|
||||
|
||||
import {
|
||||
commitMutationPromiseNormalized,
|
||||
createMutation,
|
||||
MutationInput,
|
||||
} from "coral-framework/lib/relay";
|
||||
|
||||
import { UpdateExternalModerationPhaseMutation as MutationTypes } from "coral-admin/__generated__/UpdateExternalModerationPhaseMutation.graphql";
|
||||
|
||||
let clientMutationId = 0;
|
||||
|
||||
const UpdateExternalModerationPhaseMutation = createMutation(
|
||||
"updateExternalModerationPhase",
|
||||
(environment: Environment, input: MutationInput<MutationTypes>) =>
|
||||
commitMutationPromiseNormalized<MutationTypes>(environment, {
|
||||
mutation: graphql`
|
||||
mutation UpdateExternalModerationPhaseMutation(
|
||||
$input: UpdateExternalModerationPhaseInput!
|
||||
) {
|
||||
updateExternalModerationPhase(input: $input) {
|
||||
phase {
|
||||
...ConfigureExternalModerationPhaseContainer_phase
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
input: {
|
||||
...input,
|
||||
clientMutationId: (clientMutationId++).toString(),
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
export default UpdateExternalModerationPhaseMutation;
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
export {
|
||||
default,
|
||||
default as ConfigureExternalModerationPhaseForm,
|
||||
} from "./ConfigureExternalModerationPhaseForm";
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
import { Localized } from "@fluent/react/compat";
|
||||
import React, { FunctionComponent } from "react";
|
||||
|
||||
import ExperimentalCallOut from "coral-admin/components/ExperimentalCallOut";
|
||||
import { ExternalLink } from "coral-framework/lib/i18n/components";
|
||||
|
||||
const ExperimentalExternalModerationPhaseCallOut: FunctionComponent = () => (
|
||||
<Localized
|
||||
id="configure-moderationPhases-experimentalFeature"
|
||||
ContactUsLink={<ExternalLink href="https://coralproject.net/contact/" />}
|
||||
>
|
||||
<ExperimentalCallOut>
|
||||
The custom moderation phases feature is currently in active development.
|
||||
Please{" "}
|
||||
<ExternalLink href="https://coralproject.net/contact/">
|
||||
contact us with any feedback or requests
|
||||
</ExternalLink>
|
||||
.
|
||||
</ExperimentalCallOut>
|
||||
</Localized>
|
||||
);
|
||||
|
||||
export default ExperimentalExternalModerationPhaseCallOut;
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
.urlColumn {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.detailsButton {
|
||||
font-family: var(--v2-font-family-primary);
|
||||
font-weight: var(--v2-font-weight-primary-semi-bold);
|
||||
line-height: var(--v2-line-height-reset);
|
||||
font-size: var(--v2-font-size-2);
|
||||
}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
import { Localized } from "@fluent/react/compat";
|
||||
import React, { FunctionComponent } from "react";
|
||||
import { graphql } from "react-relay";
|
||||
|
||||
import getExternalModerationPhaseLink from "coral-admin/helpers/getExternalModerationPhaseLink";
|
||||
import { withFragmentContainer } from "coral-framework/lib/relay";
|
||||
import {
|
||||
Button,
|
||||
Flex,
|
||||
Icon,
|
||||
TableCell,
|
||||
TableRow,
|
||||
} from "coral-ui/components/v2";
|
||||
|
||||
import { ExternalModerationPhaseRow_phase } from "coral-admin/__generated__/ExternalModerationPhaseRow_phase.graphql";
|
||||
|
||||
import StatusMarker from "./StatusMarker";
|
||||
|
||||
import styles from "./ExternalModerationPhaseRow.css";
|
||||
|
||||
interface Props {
|
||||
phase: ExternalModerationPhaseRow_phase;
|
||||
}
|
||||
|
||||
const ExternalModerationPhaseRow: FunctionComponent<Props> = ({ phase }) => (
|
||||
<TableRow data-testid={`moderation-phase-${phase.id}`}>
|
||||
<TableCell className={styles.urlColumn}>{phase.name}</TableCell>
|
||||
<TableCell>
|
||||
<StatusMarker enabled={phase.enabled} />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Flex justifyContent="flex-end">
|
||||
<Localized
|
||||
id="configure-moderationPhases-detailsButton"
|
||||
icon={<Icon>keyboard_arrow_right</Icon>}
|
||||
>
|
||||
<Button
|
||||
variant="text"
|
||||
to={getExternalModerationPhaseLink(phase.id)}
|
||||
iconRight
|
||||
>
|
||||
Details
|
||||
<Icon>keyboard_arrow_right</Icon>
|
||||
</Button>
|
||||
</Localized>
|
||||
</Flex>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
|
||||
const enhanced = withFragmentContainer<Props>({
|
||||
phase: graphql`
|
||||
fragment ExternalModerationPhaseRow_phase on ExternalModerationPhase {
|
||||
id
|
||||
name
|
||||
enabled
|
||||
}
|
||||
`,
|
||||
})(ExternalModerationPhaseRow);
|
||||
|
||||
export default enhanced;
|
||||
+123
@@ -0,0 +1,123 @@
|
||||
import { Localized } from "@fluent/react/compat";
|
||||
import React, { FunctionComponent } from "react";
|
||||
import { graphql } from "react-relay";
|
||||
|
||||
import { urls } from "coral-framework/helpers";
|
||||
import { ExternalLink } from "coral-framework/lib/i18n/components";
|
||||
import { withFragmentContainer } from "coral-framework/lib/relay";
|
||||
import {
|
||||
Button,
|
||||
CallOut,
|
||||
FormFieldDescription,
|
||||
HorizontalGutter,
|
||||
Icon,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableRow,
|
||||
} from "coral-ui/components/v2";
|
||||
|
||||
import { ModerationPhasesConfigContainer_settings } from "coral-admin/__generated__/ModerationPhasesConfigContainer_settings.graphql";
|
||||
|
||||
import ConfigBox from "../../ConfigBox";
|
||||
import Header from "../../Header";
|
||||
import Subheader from "../../Subheader";
|
||||
import ExperimentalExternalModerationPhaseCallOut from "./ExperimentalExternalModerationPhaseCallOut";
|
||||
import ExternalModerationPhaseRow from "./ExternalModerationPhaseRow";
|
||||
|
||||
interface Props {
|
||||
settings: ModerationPhasesConfigContainer_settings;
|
||||
}
|
||||
|
||||
const ModerationPhasesConfigContainer: FunctionComponent<Props> = ({
|
||||
settings,
|
||||
}) => {
|
||||
return (
|
||||
<HorizontalGutter size="double" data-testid="moderation-phases-container">
|
||||
<ExperimentalExternalModerationPhaseCallOut />
|
||||
<ConfigBox
|
||||
title={
|
||||
<Localized id="configure-moderationPhases-header-title">
|
||||
<Header htmlFor="configure-moderationPhases-header.title">
|
||||
Moderation Phases
|
||||
</Header>
|
||||
</Localized>
|
||||
}
|
||||
>
|
||||
<Localized
|
||||
id="configure-moderationPhases-description"
|
||||
externalLink={
|
||||
<ExternalLink href="https://github.com/coralproject/talk/blob/master/EXTERNAL_MODERATION_PHASES.md#request-signing" />
|
||||
}
|
||||
>
|
||||
<FormFieldDescription>
|
||||
Configure a external moderation phase to automate some moderation
|
||||
actions. Moderation requests will be JSON encoded and signed. To
|
||||
learn more about moderation requests, visit our{" "}
|
||||
<ExternalLink href="https://github.com/coralproject/talk/blob/master/EXTERNAL_MODERATION_PHASES.md#request-signing">
|
||||
docs
|
||||
</ExternalLink>
|
||||
.
|
||||
</FormFieldDescription>
|
||||
</Localized>
|
||||
<Button
|
||||
to={urls.admin.addExternalModerationPhase}
|
||||
iconLeft
|
||||
data-testid="add-external-moderation-phase"
|
||||
>
|
||||
<Icon size="md">add</Icon>
|
||||
<Localized id="configure-moderationPhases-addExternalModerationPhaseButton">
|
||||
Add external moderation phase
|
||||
</Localized>
|
||||
</Button>
|
||||
<Localized id="configure-moderationPhases-moderationPhases">
|
||||
<Subheader>Moderation Phases</Subheader>
|
||||
</Localized>
|
||||
{settings.integrations.external &&
|
||||
settings.integrations.external.phases.length > 0 ? (
|
||||
<Table fullWidth>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<Localized id="configure-moderationPhases-name">
|
||||
<TableCell>Name</TableCell>
|
||||
</Localized>
|
||||
<Localized id="configure-moderationPhases-status">
|
||||
<TableCell>Status</TableCell>
|
||||
</Localized>
|
||||
<TableCell />
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{settings.integrations.external.phases.map((phase, idx) => (
|
||||
<ExternalModerationPhaseRow key={idx} phase={phase} />
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
) : (
|
||||
<Localized id="configure-moderationPhases-noExternalModerationPhases">
|
||||
<CallOut color="regular" fullWidth>
|
||||
There are no external moderation phases configured, add one above.
|
||||
</CallOut>
|
||||
</Localized>
|
||||
)}
|
||||
</ConfigBox>
|
||||
</HorizontalGutter>
|
||||
);
|
||||
};
|
||||
|
||||
const enhanced = withFragmentContainer<Props>({
|
||||
settings: graphql`
|
||||
fragment ModerationPhasesConfigContainer_settings on Settings {
|
||||
integrations {
|
||||
external {
|
||||
phases {
|
||||
...ExternalModerationPhaseRow_phase
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
})(ModerationPhasesConfigContainer);
|
||||
|
||||
export default enhanced;
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
import React, { FunctionComponent } from "react";
|
||||
import { graphql } from "react-relay";
|
||||
|
||||
import { withRouteConfig } from "coral-framework/lib/router";
|
||||
import { Delay, Spinner } from "coral-ui/components/v2";
|
||||
|
||||
import { ModerationPhasesConfigRouteQueryResponse } from "coral-admin/__generated__/ModerationPhasesConfigRouteQuery.graphql";
|
||||
|
||||
import ModerationPhasesConfigContainer from "./ModerationPhasesConfigContainer";
|
||||
|
||||
interface Props {
|
||||
data: ModerationPhasesConfigRouteQueryResponse | null;
|
||||
}
|
||||
|
||||
const ModerationPhasesConfigRoute: FunctionComponent<Props> = ({ data }) => {
|
||||
if (!data) {
|
||||
return (
|
||||
<Delay>
|
||||
<Spinner />
|
||||
</Delay>
|
||||
);
|
||||
}
|
||||
|
||||
return <ModerationPhasesConfigContainer settings={data.settings} />;
|
||||
};
|
||||
|
||||
const enhanced = withRouteConfig<Props>({
|
||||
query: graphql`
|
||||
query ModerationPhasesConfigRouteQuery {
|
||||
settings {
|
||||
...ModerationPhasesConfigContainer_settings
|
||||
}
|
||||
}
|
||||
`,
|
||||
})(ModerationPhasesConfigRoute);
|
||||
|
||||
export default enhanced;
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
import React, { FunctionComponent } from "react";
|
||||
|
||||
import MainLayout from "coral-admin/components/MainLayout";
|
||||
|
||||
import ConfigureLinks from "../../ConfigureLinks";
|
||||
import Layout from "../../Layout";
|
||||
import Main from "../../Main";
|
||||
import SideBar from "../../SideBar";
|
||||
|
||||
interface Props {
|
||||
children: React.ReactElement;
|
||||
}
|
||||
|
||||
const ModerationPhasesLayout: FunctionComponent<Props> = (props) => {
|
||||
return (
|
||||
<MainLayout>
|
||||
<Layout>
|
||||
<SideBar>
|
||||
<ConfigureLinks />
|
||||
</SideBar>
|
||||
<Main>{props.children}</Main>
|
||||
</Layout>
|
||||
</MainLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default ModerationPhasesLayout;
|
||||
@@ -0,0 +1,12 @@
|
||||
.success {
|
||||
background-color: var(--v2-palette-success-main);
|
||||
border-color: var(--v2-palette-success-main);
|
||||
color: var(--v2-colors-pure-white);
|
||||
}
|
||||
|
||||
.error {
|
||||
background-color: var(--v2-palette-error-darkest);
|
||||
border-color: var(--v2-palette-error-darkest);
|
||||
color: var(--v2-colors-pure-white);
|
||||
font-weight: var(--v2-font-weight-primary-semi-bold);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { Localized } from "@fluent/react/compat";
|
||||
import React, { FunctionComponent } from "react";
|
||||
|
||||
import { Marker } from "coral-ui/components/v2";
|
||||
|
||||
import styles from "./StatusMarker.css";
|
||||
|
||||
interface Props {
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
const StatusMarker: FunctionComponent<Props> = ({ enabled }) =>
|
||||
enabled ? (
|
||||
<Localized id="configure-moderationPhases-enabledModerationPhase">
|
||||
<Marker className={styles.success}>Enabled</Marker>
|
||||
</Localized>
|
||||
) : (
|
||||
<Localized id="configure-moderationPhases-disableModerationPhase">
|
||||
<Marker className={styles.error}>Disabled</Marker>
|
||||
</Localized>
|
||||
);
|
||||
|
||||
export default StatusMarker;
|
||||
@@ -0,0 +1,6 @@
|
||||
export {
|
||||
default,
|
||||
default as ModerationPhasesConfigRoute,
|
||||
} from "./ModerationPhasesConfigRoute";
|
||||
export { default as AddExternalModerationPhaseRoute } from "./AddExternalModerationPhase";
|
||||
export { default as ConfigureExternalModerationPhaseRoute } from "./ConfigureExternalModerationPhase";
|
||||
+4
-1
@@ -1,3 +1,4 @@
|
||||
import { Localized } from "@fluent/react/compat";
|
||||
import React, { FunctionComponent } from "react";
|
||||
import { graphql } from "react-relay";
|
||||
|
||||
@@ -19,7 +20,9 @@ const EndpointDetails: FunctionComponent<Props> = ({
|
||||
settings,
|
||||
}) => (
|
||||
<>
|
||||
<Subheader>Endpoint details</Subheader>
|
||||
<Localized id="configure-webhooks-endpointDetails">
|
||||
<Subheader>Endpoint details</Subheader>
|
||||
</Localized>
|
||||
<ConfigureWebhookEndpointForm
|
||||
settings={settings}
|
||||
webhookEndpoint={webhookEndpoint}
|
||||
|
||||
+8
-3
@@ -62,9 +62,14 @@ const EndpointStatus: FunctionComponent<Props> = ({ webhookEndpoint }) => {
|
||||
/>
|
||||
<CopyButton text={webhookEndpoint.signingSecret.secret} />
|
||||
</Flex>
|
||||
<HelperText>
|
||||
KEY GENERATED AT: {webhookEndpoint.signingSecret.createdAt}
|
||||
</HelperText>
|
||||
<Localized
|
||||
id="configure-webhooks-generatedAt"
|
||||
$date={new Date(webhookEndpoint.signingSecret.createdAt)}
|
||||
>
|
||||
<HelperText>
|
||||
KEY GENERATED AT: {webhookEndpoint.signingSecret.createdAt}
|
||||
</HelperText>
|
||||
</Localized>
|
||||
</FormField>
|
||||
</>
|
||||
);
|
||||
|
||||
+13
-8
@@ -22,7 +22,7 @@ import {
|
||||
} from "coral-ui/components/v2";
|
||||
import AppNotification from "coral-ui/components/v2/AppNotification";
|
||||
|
||||
import RotateWebhookEndpointSecretMutation from "./RotateWebhookEndpointSecretMutation";
|
||||
import RotateWebhookEndpointSigningSecretMutation from "./RotateWebhookEndpointSigningSecretMutation";
|
||||
|
||||
import styles from "./RotateSigningSecretModal.css";
|
||||
|
||||
@@ -32,20 +32,23 @@ interface Props {
|
||||
open: boolean;
|
||||
}
|
||||
|
||||
const RotateWebhookEndpointSecretModal: FunctionComponent<Props> = ({
|
||||
const RotateWebhookEndpointSigningSecretModal: FunctionComponent<Props> = ({
|
||||
onHide,
|
||||
open,
|
||||
endpointID,
|
||||
}) => {
|
||||
const rotateWebhookEndpointSecret = useMutation(
|
||||
RotateWebhookEndpointSecretMutation
|
||||
const rotateWebhookEndpointSigningSecret = useMutation(
|
||||
RotateWebhookEndpointSigningSecretMutation
|
||||
);
|
||||
const { setMessage, clearMessage } = useNotification();
|
||||
const onRotateSecret = useCallback(
|
||||
async ({ inactiveIn: inactiveInString }) => {
|
||||
try {
|
||||
const inactiveIn = parseInt(inactiveInString, 10);
|
||||
await rotateWebhookEndpointSecret({ id: endpointID, inactiveIn });
|
||||
await rotateWebhookEndpointSigningSecret({
|
||||
id: endpointID,
|
||||
inactiveIn,
|
||||
});
|
||||
|
||||
// Post a notification about the successful change.
|
||||
setMessage(
|
||||
@@ -56,7 +59,9 @@ const RotateWebhookEndpointSecretModal: FunctionComponent<Props> = ({
|
||||
</AppNotification>
|
||||
</Localized>
|
||||
);
|
||||
window.scroll(0, 0);
|
||||
|
||||
// Scroll after a zero timeout because chrome won't scroll otherwise.
|
||||
setTimeout(() => window.scroll(0, 0), 0);
|
||||
} catch (err) {
|
||||
if (err instanceof InvalidRequestError) {
|
||||
return err.invalidArgs;
|
||||
@@ -69,7 +74,7 @@ const RotateWebhookEndpointSecretModal: FunctionComponent<Props> = ({
|
||||
|
||||
return;
|
||||
},
|
||||
[endpointID, rotateWebhookEndpointSecret]
|
||||
[endpointID, rotateWebhookEndpointSigningSecret]
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -162,4 +167,4 @@ const RotateWebhookEndpointSecretModal: FunctionComponent<Props> = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default RotateWebhookEndpointSecretModal;
|
||||
export default RotateWebhookEndpointSigningSecretModal;
|
||||
|
||||
+7
-7
@@ -7,19 +7,19 @@ import {
|
||||
MutationInput,
|
||||
} from "coral-framework/lib/relay";
|
||||
|
||||
import { RotateWebhookEndpointSecretMutation as MutationTypes } from "coral-admin/__generated__/RotateWebhookEndpointSecretMutation.graphql";
|
||||
import { RotateWebhookEndpointSigningSecretMutation as MutationTypes } from "coral-admin/__generated__/RotateWebhookEndpointSigningSecretMutation.graphql";
|
||||
|
||||
let clientMutationId = 0;
|
||||
|
||||
const RotateWebhookEndpointSecretMutation = createMutation(
|
||||
"rotateWebhookEndpointSecret",
|
||||
const RotateWebhookEndpointSigningSecretMutation = createMutation(
|
||||
"rotateWebhookEndpointSigningSecret",
|
||||
(environment: Environment, input: MutationInput<MutationTypes>) =>
|
||||
commitMutationPromiseNormalized<MutationTypes>(environment, {
|
||||
mutation: graphql`
|
||||
mutation RotateWebhookEndpointSecretMutation(
|
||||
$input: RotateWebhookEndpointSecretInput!
|
||||
mutation RotateWebhookEndpointSigningSecretMutation(
|
||||
$input: RotateWebhookEndpointSigningSecretInput!
|
||||
) {
|
||||
rotateWebhookEndpointSecret(input: $input) {
|
||||
rotateWebhookEndpointSigningSecret(input: $input) {
|
||||
endpoint {
|
||||
...ConfigureWebhookEndpointContainer_webhookEndpoint
|
||||
}
|
||||
@@ -35,4 +35,4 @@ const RotateWebhookEndpointSecretMutation = createMutation(
|
||||
})
|
||||
);
|
||||
|
||||
export default RotateWebhookEndpointSecretMutation;
|
||||
export default RotateWebhookEndpointSigningSecretMutation;
|
||||
@@ -11,3 +11,8 @@ export {
|
||||
ConfigureWebhookEndpointRoute,
|
||||
AddWebhookEndpointRoute,
|
||||
} from "./WebhookEndpoints";
|
||||
export {
|
||||
ModerationPhasesConfigRoute,
|
||||
ConfigureExternalModerationPhaseRoute,
|
||||
AddExternalModerationPhaseRoute,
|
||||
} from "./ModerationPhases";
|
||||
|
||||
@@ -52,6 +52,15 @@ exports[`renders configure advanced 1`] = `
|
||||
Moderation
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
className="Link-link"
|
||||
href="/admin/configure/moderation/phases"
|
||||
onClick={[Function]}
|
||||
>
|
||||
Moderation Phases
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
className="Link-link"
|
||||
|
||||
@@ -52,6 +52,15 @@ exports[`renders configure auth 1`] = `
|
||||
Moderation
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
className="Link-link"
|
||||
href="/admin/configure/moderation/phases"
|
||||
onClick={[Function]}
|
||||
>
|
||||
Moderation Phases
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
className="Link-link"
|
||||
@@ -1284,10 +1293,10 @@ more about creating a JWT Token with
|
||||
className="Box-root Flex-root Flex-flex Flex-justifySpaceBetween Flex-alignCenter"
|
||||
>
|
||||
<div
|
||||
className="SSOKeyCard-keySection"
|
||||
className="SSOSigningSecretCard-keySection"
|
||||
>
|
||||
<div
|
||||
className="SSOKeyCard-label"
|
||||
className="SSOSigningSecretCard-label"
|
||||
>
|
||||
<label
|
||||
className="Label-root"
|
||||
@@ -1309,10 +1318,10 @@ more about creating a JWT Token with
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="SSOKeyCard-secretSection"
|
||||
className="SSOSigningSecretCard-secretSection"
|
||||
>
|
||||
<div
|
||||
className="SSOKeyCard-label"
|
||||
className="SSOSigningSecretCard-label"
|
||||
>
|
||||
<label
|
||||
className="Label-root"
|
||||
@@ -1390,10 +1399,10 @@ more about creating a JWT Token with
|
||||
className="Box-root Flex-root Flex-flex Flex-justifyFlexStart Flex-alignCenter"
|
||||
>
|
||||
<div
|
||||
className="SSOKeyCard-statusSection"
|
||||
className="SSOSigningSecretCard-statusSection"
|
||||
>
|
||||
<div
|
||||
className="SSOKeyCard-label"
|
||||
className="SSOSigningSecretCard-label"
|
||||
>
|
||||
<label
|
||||
className="Label-root"
|
||||
|
||||
@@ -52,6 +52,15 @@ exports[`renders configure general 1`] = `
|
||||
Moderation
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
className="Link-link"
|
||||
href="/admin/configure/moderation/phases"
|
||||
onClick={[Function]}
|
||||
>
|
||||
Moderation Phases
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
className="Link-link"
|
||||
|
||||
@@ -52,6 +52,15 @@ exports[`renders configure moderation 1`] = `
|
||||
Moderation
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
className="Link-link"
|
||||
href="/admin/configure/moderation/phases"
|
||||
onClick={[Function]}
|
||||
>
|
||||
Moderation Phases
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
className="Link-link"
|
||||
|
||||
@@ -52,6 +52,15 @@ exports[`renders configure organization 1`] = `
|
||||
Moderation
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
className="Link-link"
|
||||
href="/admin/configure/moderation/phases"
|
||||
onClick={[Function]}
|
||||
>
|
||||
Moderation Phases
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
className="Link-link"
|
||||
|
||||
@@ -52,6 +52,15 @@ exports[`renders configure wordList 1`] = `
|
||||
Moderation
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
className="Link-link"
|
||||
href="/admin/configure/moderation/phases"
|
||||
onClick={[Function]}
|
||||
>
|
||||
Moderation Phases
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
className="Link-link Link-linkActive"
|
||||
|
||||
@@ -62,7 +62,7 @@ it("rotate sso key", async () => {
|
||||
const { testRenderer } = await createTestRenderer({
|
||||
resolvers: createResolversStub<GQLResolver>({
|
||||
Mutation: {
|
||||
rotateSSOKey: () => {
|
||||
rotateSSOSigningSecret: () => {
|
||||
return {
|
||||
settings: pureMerge<typeof settingsWithEmptyAuth>(
|
||||
settingsWithEmptyAuth,
|
||||
@@ -71,7 +71,7 @@ it("rotate sso key", async () => {
|
||||
integrations: {
|
||||
sso: {
|
||||
enabled: true,
|
||||
keys: [
|
||||
signingSecrets: [
|
||||
{
|
||||
kid: "kid-01",
|
||||
secret: "secret",
|
||||
|
||||
@@ -114,7 +114,7 @@ export const settings = createFixture<GQLSettings>({
|
||||
admin: true,
|
||||
stream: true,
|
||||
},
|
||||
keys: [
|
||||
signingSecrets: [
|
||||
{
|
||||
kid: "kid-01",
|
||||
secret: "secret",
|
||||
@@ -212,7 +212,7 @@ export const settingsWithEmptyAuth = createFixture<GQLSettings>(
|
||||
stream: true,
|
||||
},
|
||||
key: "",
|
||||
keys: [
|
||||
signingSecrets: [
|
||||
{
|
||||
kid: "kid-01",
|
||||
secret: "secret",
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
export default {
|
||||
admin: {
|
||||
moderate: "/admin/moderate",
|
||||
configureWebhooks: "/admin/configure/webhooks",
|
||||
moderationPhases: "/admin/configure/moderation/phases",
|
||||
addExternalModerationPhase: "/admin/configure/moderation/phases/add",
|
||||
configureExternalModerationPhase: "/admin/configure/moderation/phases",
|
||||
webhooks: "/admin/configure/webhooks",
|
||||
addWebhookEndpoint: "/admin/configure/webhooks/add",
|
||||
configureWebhookEndpoint: "/admin/configure/webhooks/endpoint",
|
||||
|
||||
@@ -92,13 +92,13 @@ export const NOT_A_WHOLE_NUMBER = () => (
|
||||
|
||||
export const NOT_A_WHOLE_NUMBER_GREATER_THAN = (x: number) => (
|
||||
<Localized id="framework-validation-notAWholeNumberGreaterThan" $x={x}>
|
||||
<span>Please enter a valid whole number greater than $x</span>
|
||||
<span>Please enter a valid whole number greater than {x}</span>
|
||||
</Localized>
|
||||
);
|
||||
|
||||
export const NOT_A_WHOLE_NUMBER_GREATER_THAN_OR_EQUAL = (x: number) => (
|
||||
<Localized id="framework-validation-notAWholeNumberGreaterThanOrEqual" $x={x}>
|
||||
<span>Please enter a valid whole number greater than or equal to $x</span>
|
||||
<span>Please enter a valid whole number greater than or equal to {x}</span>
|
||||
</Localized>
|
||||
);
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { ReactNode } from "react";
|
||||
|
||||
import startsWith from "coral-common/utils/startsWith";
|
||||
|
||||
import {
|
||||
EMAIL_REGEX,
|
||||
PASSWORD_MIN_LENGTH,
|
||||
@@ -10,6 +8,7 @@ import {
|
||||
USERNAME_MIN_LENGTH,
|
||||
USERNAME_REGEX,
|
||||
} from "coral-common/helpers/validate";
|
||||
import startsWith from "coral-common/utils/startsWith";
|
||||
|
||||
import {
|
||||
DELETE_CONFIRMATION_INVALID,
|
||||
|
||||
@@ -35,7 +35,7 @@ import { Metrics } from "coral-server/services/metrics";
|
||||
import { MigrationManager } from "coral-server/services/migrate";
|
||||
import { PersistedQueryCache } from "coral-server/services/queries";
|
||||
import { AugmentedRedis } from "coral-server/services/redis";
|
||||
import TenantCache from "coral-server/services/tenant/cache";
|
||||
import { TenantCache } from "coral-server/services/tenant/cache";
|
||||
|
||||
import { healthHandler, versionHandler } from "./handlers";
|
||||
import { compileTrust } from "./helpers";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { FluentBundle } from "@fluent/bundle/compat";
|
||||
|
||||
import { CoralError, InternalError } from "coral-server/errors";
|
||||
import { CoralError, WrappedInternalError } from "coral-server/errors";
|
||||
import { I18n } from "coral-server/services/i18n";
|
||||
import { ErrorRequestHandler, Request } from "coral-server/types/express";
|
||||
|
||||
@@ -12,7 +12,7 @@ import { ErrorRequestHandler, Request } from "coral-server/types/express";
|
||||
const wrapError = (err: Error) =>
|
||||
err instanceof CoralError
|
||||
? err
|
||||
: new InternalError(err, "wrapped internal error");
|
||||
: new WrappedInternalError(err, "wrapped internal error");
|
||||
|
||||
/**
|
||||
* serializeError will return a serialized error that can be returned via the
|
||||
|
||||
@@ -7,8 +7,10 @@ import { IntegrationDisabled } from "coral-server/errors";
|
||||
import { AuthIntegrations } from "coral-server/models/settings";
|
||||
import { Tenant } from "coral-server/models/tenant";
|
||||
import { User } from "coral-server/models/user";
|
||||
import TenantCache from "coral-server/services/tenant/cache";
|
||||
import { TenantCacheAdapter } from "coral-server/services/tenant/cache/adapter";
|
||||
import {
|
||||
TenantCache,
|
||||
TenantCacheAdapter,
|
||||
} from "coral-server/services/tenant/cache";
|
||||
import { Request } from "coral-server/types/express";
|
||||
import { Profile } from "passport";
|
||||
import { VerifyCallback } from "passport-oauth2";
|
||||
|
||||
@@ -23,8 +23,10 @@ import {
|
||||
User,
|
||||
} from "coral-server/models/user";
|
||||
import { AsymmetricSigningAlgorithm } from "coral-server/services/jwt";
|
||||
import TenantCache from "coral-server/services/tenant/cache";
|
||||
import { TenantCacheAdapter } from "coral-server/services/tenant/cache/adapter";
|
||||
import {
|
||||
TenantCache,
|
||||
TenantCacheAdapter,
|
||||
} from "coral-server/services/tenant/cache";
|
||||
import { findOrCreate } from "coral-server/services/users";
|
||||
import { validateUsername } from "coral-server/services/users/helpers";
|
||||
import { Request } from "coral-server/types/express";
|
||||
|
||||
@@ -2,10 +2,10 @@ import jwks, { JwksClient } from "jwks-rsa";
|
||||
import { Db } from "mongodb";
|
||||
|
||||
import { AppOptions } from "coral-server/app";
|
||||
import { Tenant } from "coral-server/models/tenant";
|
||||
import { TenantCacheAdapter } from "coral-server/services/tenant/cache/adapter";
|
||||
|
||||
import logger from "coral-server/logger";
|
||||
import { Tenant } from "coral-server/models/tenant";
|
||||
import { TenantCacheAdapter } from "coral-server/services/tenant/cache";
|
||||
|
||||
import { Verifier } from "../jwt";
|
||||
import {
|
||||
findOrCreateOIDCUserWithToken,
|
||||
|
||||
@@ -7,10 +7,14 @@ import { Db } from "mongodb";
|
||||
import { validate } from "coral-server/app/request/body";
|
||||
import { IntegrationDisabled, TokenInvalidError } from "coral-server/errors";
|
||||
import logger from "coral-server/logger";
|
||||
import { Secret, SSOAuthIntegration } from "coral-server/models/settings";
|
||||
import {
|
||||
filterActiveSigningSecrets,
|
||||
SigningSecret,
|
||||
SSOAuthIntegration,
|
||||
} from "coral-server/models/settings";
|
||||
import {
|
||||
Tenant,
|
||||
updateLastUsedAtTenantSSOKey,
|
||||
updateLastUsedAtTenantSSOSigningSecret,
|
||||
} from "coral-server/models/tenant";
|
||||
import {
|
||||
retrieveUserWithProfile,
|
||||
@@ -167,7 +171,7 @@ export async function findOrCreateSSOUser(
|
||||
const updateLastUsedAtKID = throttle(
|
||||
async (redis: Redis, tenantID: string, kid: string, now: Date) => {
|
||||
try {
|
||||
await updateLastUsedAtTenantSSOKey(redis, tenantID, kid, now);
|
||||
await updateLastUsedAtTenantSSOSigningSecret(redis, tenantID, kid, now);
|
||||
logger.trace({ tenantID, kid }, "updated last used tenant sso key");
|
||||
} catch (err) {
|
||||
logger.error(
|
||||
@@ -185,23 +189,18 @@ export interface SSOVerifierOptions {
|
||||
redis: AugmentedRedis;
|
||||
}
|
||||
|
||||
export function getRelevantSSOKeys(
|
||||
export function getRelevantSSOSigningSecrets(
|
||||
integration: SSOAuthIntegration,
|
||||
tokenString: string,
|
||||
now: Date,
|
||||
kid?: string
|
||||
): Secret[] {
|
||||
): SigningSecret[] {
|
||||
// Collect all the current valid keys.
|
||||
const keys = integration.keys.filter((k) => {
|
||||
if (k.inactiveAt && now >= k.inactiveAt) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return k;
|
||||
});
|
||||
|
||||
// If there is only one key, that's all we can use!
|
||||
const keys = integration.signingSecrets.filter(
|
||||
filterActiveSigningSecrets(now)
|
||||
);
|
||||
if (keys.length === 1) {
|
||||
// There is only one key, that's all we can use!
|
||||
return keys;
|
||||
}
|
||||
|
||||
@@ -259,14 +258,13 @@ export class SSOVerifier implements Verifier<SSOToken> {
|
||||
throw new IntegrationDisabled("sso");
|
||||
}
|
||||
|
||||
// check to see if there is at least one key associated with this
|
||||
// integration.
|
||||
if (integration.keys.length === 0) {
|
||||
throw new Error("integration key does not exist");
|
||||
}
|
||||
|
||||
// Get the valid configurations for the given token and integration pair.
|
||||
const keys = getRelevantSSOKeys(integration, tokenString, now, kid);
|
||||
const keys = getRelevantSSOSigningSecrets(
|
||||
integration,
|
||||
tokenString,
|
||||
now,
|
||||
kid
|
||||
);
|
||||
if (keys.length === 0) {
|
||||
throw new TokenInvalidError(
|
||||
tokenString,
|
||||
|
||||
@@ -2,7 +2,7 @@ import { v1 as uuid } from "uuid";
|
||||
|
||||
import { TenantNotFoundError } from "coral-server/errors";
|
||||
import logger from "coral-server/logger";
|
||||
import TenantCache from "coral-server/services/tenant/cache";
|
||||
import { TenantCache } from "coral-server/services/tenant/cache";
|
||||
import { RequestHandler } from "coral-server/types/express";
|
||||
|
||||
export interface MiddlewareOptions {
|
||||
|
||||
@@ -9,7 +9,7 @@ import { cspSiteMiddleware } from "coral-server/app/middleware/csp/tenant";
|
||||
import { installedMiddleware } from "coral-server/app/middleware/installed";
|
||||
import { tenantMiddleware } from "coral-server/app/middleware/tenant";
|
||||
import logger from "coral-server/logger";
|
||||
import TenantCache from "coral-server/services/tenant/cache";
|
||||
import { TenantCache } from "coral-server/services/tenant/cache";
|
||||
import { RequestHandler } from "coral-server/types/express";
|
||||
|
||||
import Entrypoints, { Entrypoint } from "../helpers/entrypoints";
|
||||
|
||||
@@ -7,7 +7,7 @@ import os from "os";
|
||||
import { LOCALES } from "coral-common/helpers/i18n/locales";
|
||||
import { ensureEndSlash } from "coral-common/utils";
|
||||
|
||||
import { InternalError } from "./errors";
|
||||
import { WrappedInternalError } from "./errors";
|
||||
|
||||
// Add custom format for the mongo uri scheme.
|
||||
convict.addFormat({
|
||||
@@ -15,7 +15,7 @@ convict.addFormat({
|
||||
validate: (url: string) => {
|
||||
parseConnectionString(url, (err) => {
|
||||
if (err) {
|
||||
throw new InternalError(err, "invalid mongo-uri");
|
||||
throw new WrappedInternalError(err, "invalid mongo-uri");
|
||||
}
|
||||
});
|
||||
},
|
||||
@@ -237,6 +237,13 @@ const config = convict({
|
||||
default: false,
|
||||
env: "DISABLE_RATE_LIMITERS",
|
||||
},
|
||||
scrape_max_response_size: {
|
||||
doc: "The maximum size (in bytes) to allow for scraping responses.",
|
||||
format: Number,
|
||||
default: 10e6,
|
||||
env: "SCRAPE_MAX_RESPONSE_SIZE",
|
||||
arg: "scrapeMaxResponseSize",
|
||||
},
|
||||
scrape_timeout: {
|
||||
doc: "The request timeout (in ms) for scraping operations.",
|
||||
format: "ms",
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Db } from "mongodb";
|
||||
|
||||
import { retrieveUserScheduledForDeletion } from "coral-server/models/user";
|
||||
import { MailerQueue } from "coral-server/queue/tasks/mailer";
|
||||
import TenantCache from "coral-server/services/tenant/cache";
|
||||
import { TenantCache } from "coral-server/services/tenant/cache";
|
||||
import { deleteUser } from "coral-server/services/users/delete";
|
||||
|
||||
import {
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Db } from "mongodb";
|
||||
import { Config } from "coral-server/config";
|
||||
import { MailerQueue } from "coral-server/queue/tasks/mailer";
|
||||
import { JWTSigningConfig } from "coral-server/services/jwt";
|
||||
import TenantCache from "coral-server/services/tenant/cache";
|
||||
import { TenantCache } from "coral-server/services/tenant/cache";
|
||||
|
||||
import { registerAccountDeletion } from "./accountDeletion";
|
||||
import { registerNotificationDigesting } from "./notificationDigesting";
|
||||
|
||||
@@ -7,7 +7,7 @@ import { MailerQueue } from "coral-server/queue/tasks/mailer";
|
||||
import { DigestibleTemplate } from "coral-server/queue/tasks/mailer/templates";
|
||||
import { JWTSigningConfig } from "coral-server/services/jwt";
|
||||
import NotificationContext from "coral-server/services/notifications/context";
|
||||
import TenantCache from "coral-server/services/tenant/cache";
|
||||
import { TenantCache } from "coral-server/services/tenant/cache";
|
||||
|
||||
import {
|
||||
ScheduledJob,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { VError } from "verror";
|
||||
|
||||
import { CoralError, DuplicateUserError, InternalError } from ".";
|
||||
import { CoralError, DuplicateUserError, WrappedInternalError } from ".";
|
||||
|
||||
it("has the right inheritance chain", () => {
|
||||
const err = new DuplicateUserError();
|
||||
@@ -14,12 +14,12 @@ it("has the right inheritance chain", () => {
|
||||
});
|
||||
|
||||
it("provides an accurate stack", () => {
|
||||
const err = new InternalError(
|
||||
const err = new WrappedInternalError(
|
||||
new Error("this is a test"),
|
||||
"this is the reason"
|
||||
);
|
||||
|
||||
expect(err).toBeInstanceOf(InternalError);
|
||||
expect(err).toBeInstanceOf(WrappedInternalError);
|
||||
expect(err).toBeInstanceOf(CoralError);
|
||||
expect(err).toBeInstanceOf(VError);
|
||||
expect(err).toBeInstanceOf(Error);
|
||||
|
||||
@@ -465,7 +465,7 @@ export class IntegrationDisabled extends CoralError {
|
||||
}
|
||||
}
|
||||
|
||||
export class InternalError extends CoralError {
|
||||
export class WrappedInternalError extends CoralError {
|
||||
constructor(cause: Error, reason: string) {
|
||||
super({
|
||||
code: ERROR_CODES.INTERNAL_ERROR,
|
||||
@@ -476,6 +476,16 @@ export class InternalError extends CoralError {
|
||||
}
|
||||
}
|
||||
|
||||
export class InternalError extends CoralError {
|
||||
constructor(reason: string, context?: Record<string, any>) {
|
||||
super({
|
||||
code: ERROR_CODES.INTERNAL_ERROR,
|
||||
context: { pvt: { reason, ...context } },
|
||||
status: 500,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class InternalDevelopmentError extends CoralError {
|
||||
constructor(cause: Error, reason: string) {
|
||||
super({
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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)
|
||||
),
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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],
|
||||
};
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -34,7 +34,7 @@ import {
|
||||
createAugmentedRedisClient,
|
||||
createRedisClient,
|
||||
} from "coral-server/services/redis";
|
||||
import TenantCache from "coral-server/services/tenant/cache";
|
||||
import { TenantCache } from "coral-server/services/tenant/cache";
|
||||
|
||||
import {
|
||||
NotifierCoralEventListener,
|
||||
|
||||
@@ -57,3 +57,14 @@ export function calculateRejectionRate(counts: CommentStatusCounts): number {
|
||||
export function hasTag(comment: Pick<Comment, "tags">, tag: GQLTAG) {
|
||||
return comment.tags.some((v) => v.type === tag);
|
||||
}
|
||||
|
||||
/**
|
||||
* getDepth will return the depth of the comment.
|
||||
*
|
||||
* @param comment the comment to check for depth
|
||||
*/
|
||||
export function getDepth(
|
||||
comment: Pick<Comment, "ancestorIDs" | "parentID">
|
||||
): number {
|
||||
return hasAncestors(comment) ? comment.ancestorIDs.length : 0;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import {
|
||||
ExternalModerationExternalIntegration,
|
||||
ExternalModerationPhase,
|
||||
} from "./settings";
|
||||
|
||||
export function filterActivePhase() {
|
||||
return (phase: Pick<ExternalModerationPhase, "enabled">) => phase.enabled;
|
||||
}
|
||||
|
||||
export function getExternalModerationPhase(
|
||||
integration: ExternalModerationExternalIntegration,
|
||||
phaseID: string
|
||||
) {
|
||||
return integration.phases.find((p) => p.id === phaseID) || null;
|
||||
}
|
||||
@@ -1,2 +1,3 @@
|
||||
export * from "./settings";
|
||||
export * from "./secret";
|
||||
export * from "./helpers";
|
||||
export * from "./signingSecret";
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user