From 22fa940ddb89ad90049c8f820facd86cb9ef6db7 Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Fri, 10 Aug 2018 12:11:32 -0300 Subject: [PATCH 01/27] Adding PoweredBy Component --- .../stream/components/PostCommentForm.tsx | 3 ++- .../client/stream/components/PoweredBy.css | 3 +++ .../client/stream/components/PoweredBy.tsx | 18 ++++++++++++++++++ .../components/UserBoxUnauthenticated.tsx | 2 +- 4 files changed, 24 insertions(+), 2 deletions(-) create mode 100644 src/core/client/stream/components/PoweredBy.css create mode 100644 src/core/client/stream/components/PoweredBy.tsx diff --git a/src/core/client/stream/components/PostCommentForm.tsx b/src/core/client/stream/components/PostCommentForm.tsx index 72bc0ad70..656ec54f8 100644 --- a/src/core/client/stream/components/PostCommentForm.tsx +++ b/src/core/client/stream/components/PostCommentForm.tsx @@ -2,7 +2,7 @@ import { Localized } from "fluent-react/compat"; import * as React from "react"; import { StatelessComponent } from "react"; import { Field, Form } from "react-final-form"; - +import PoweredBy from "./PoweredBy"; import { OnSubmit } from "talk-framework/lib/form"; import { required } from "talk-framework/lib/validation"; import { Button, Typography } from "talk-ui/components"; @@ -40,6 +40,7 @@ const PostCommentForm: StatelessComponent = props => ( )}
+
-
- -
+ Sign In and join the conversation +
-
- -
+ Sign In and join the conversation +
-
- -
+ Sign In and join the conversation +
-
- -
+ Sign In and join the conversation +
-
- -
+ Sign In and join the conversation +
-
- -
+ Sign In and join the conversation +
-
- -
+ Sign In and join the conversation +
-
- -
+ Sign In and join the conversation +
From 6648dfe492b4859518b2a6283354028b4cc20a29 Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Mon, 13 Aug 2018 12:59:53 -0300 Subject: [PATCH 03/27] Adding InputDescription Component --- src/core/client/auth/components/SignUp.tsx | 15 +++---- .../stream/components/PostCommentForm.tsx | 8 ++-- .../client/stream/components/PoweredBy.css | 3 -- .../InputDescription/InputDescription.mdx | 26 ++---------- .../InputDescription/InputDescription.tsx | 41 +++++-------------- src/core/client/ui/components/index.ts | 1 + src/core/client/ui/shared/typography.css | 8 ++-- 7 files changed, 31 insertions(+), 71 deletions(-) delete mode 100644 src/core/client/stream/components/PoweredBy.css diff --git a/src/core/client/auth/components/SignUp.tsx b/src/core/client/auth/components/SignUp.tsx index d4c8a7d55..76f7d952a 100644 --- a/src/core/client/auth/components/SignUp.tsx +++ b/src/core/client/auth/components/SignUp.tsx @@ -16,6 +16,7 @@ import { Button, Flex, FormField, + InputDescription, InputLabel, TextField, Typography, @@ -36,7 +37,7 @@ export interface SignUpForm { const SignUp: StatelessComponent = props => { return (
- {({ handleSubmit, submitting }) => ( + {({ handleSubmit, submitting, invalid }) => ( @@ -74,10 +75,10 @@ const SignUp: StatelessComponent = props => { {({ input, meta }) => ( Username - + A unique identifier displayed on your comments. You may use “_” and “.” - + = props => { {({ input, meta }) => ( Password - + Must be at least 8 characters - + = props => { {({ input, meta }) => ( Confirm Password - + Must be at least 8 characters - + = props => ( fullWidth size="large" > - Sign In and join the conversation + Sign in and join the conversation )} diff --git a/src/core/client/stream/components/PoweredBy.css b/src/core/client/stream/components/PoweredBy.css deleted file mode 100644 index 25c884af5..000000000 --- a/src/core/client/stream/components/PoweredBy.css +++ /dev/null @@ -1,3 +0,0 @@ -.text { - font-size: calc(14rem / var(--rem-base)); -} diff --git a/src/core/client/ui/components/InputDescription/InputDescription.mdx b/src/core/client/ui/components/InputDescription/InputDescription.mdx index 3dc055085..c27a460be 100644 --- a/src/core/client/ui/components/InputDescription/InputDescription.mdx +++ b/src/core/client/ui/components/InputDescription/InputDescription.mdx @@ -1,36 +1,18 @@ --- -name: FormField +name: InputDescription menu: UI Kit --- import { Playground, PropsTable } from 'docz' -import FormField from './FormField' -import { InputLabel, ValidationMessage, TextField, Typography, Flex, Button} from '../core/client/ui/components' +import { InputDescription } from '../core/client/ui/components' # Flex -`FormField` is to be used with Form Components `flexbox`. - -## Justify content - - - - +`InputDescription` is to be used with Form Components. ## Align items - - - - - - -## Direction - - - - - + This is a description diff --git a/src/core/client/ui/components/InputDescription/InputDescription.tsx b/src/core/client/ui/components/InputDescription/InputDescription.tsx index fe5b6dd7b..d36d5fe7a 100644 --- a/src/core/client/ui/components/InputDescription/InputDescription.tsx +++ b/src/core/client/ui/components/InputDescription/InputDescription.tsx @@ -1,43 +1,24 @@ -import cn from "classnames"; -import React, { ReactNode } from "react"; -import { StatelessComponent } from "react"; +import React, { ReactNode, StatelessComponent } from "react"; +import { Typography } from "talk-ui/components"; -import { withStyles } from "talk-ui/hocs"; - -import * as styles from "./FormField.css"; - -interface InnerProps { +interface InputDescriptionProps { children: ReactNode; - classes: typeof styles; id?: string; className?: string; - itemGutter?: boolean | "half"; } -const FormField: StatelessComponent = props => { - const { classes, className, children, itemGutter, ...rest } = props; - - // TODO (bc): Use flex component once the extra div issue is solved. +const InputDescription: StatelessComponent = props => { + const { className, children, ...rest } = props; return ( -
{children} -
+ ); }; -FormField.defaultProps = { - itemGutter: true, -}; - -const enhanced = withStyles(styles)(FormField); -export default enhanced; +export default InputDescription; diff --git a/src/core/client/ui/components/index.ts b/src/core/client/ui/components/index.ts index 6b8e15d7f..76efab87c 100644 --- a/src/core/client/ui/components/index.ts +++ b/src/core/client/ui/components/index.ts @@ -15,3 +15,4 @@ export { default as CallOut } from "./CallOut"; export { default as ClickOutside } from "./ClickOutside"; export { default as Popup } from "./Popup"; export { default as FormField } from "./FormField"; +export { default as InputDescription } from "./InputDescription"; diff --git a/src/core/client/ui/shared/typography.css b/src/core/client/ui/shared/typography.css index 990004923..ba367cc44 100644 --- a/src/core/client/ui/shared/typography.css +++ b/src/core/client/ui/shared/typography.css @@ -137,9 +137,9 @@ color: var(--palette-common-black); font-family: var(--font-family-sans-serif); font-weight: var(--font-weight-medium); - font-size: calc(16rem / var(--rem-base)); - line-height: calc(16em / 16); - letter-spacing: calc(-0.1em / 16); + font-size: calc(14rem / var(--rem-base)); + line-height: calc(20em / 16); + letter-spacing: calc(0.2em / 16); } .inputText { @@ -163,7 +163,7 @@ .inputDescription { font-size: calc(14rem / var(--rem-base)); font-weight: var(--font-weight-regular); - font-family: var(--font-family); + font-family: var(--font-family-sans-serif); line-height: calc(18em / 16); letter-spacing: calc(0.2em / 16); color: var(--palette-text-secondary); From b4480f4bf7a8dccf027fa23c213d836a1ec2af51 Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Mon, 13 Aug 2018 13:25:51 -0300 Subject: [PATCH 04/27] Adding SignUp Form Functionality --- src/core/client/auth/components/SignIn.tsx | 139 +++++++++++++----- src/core/client/auth/components/SignUp.tsx | 2 +- .../auth/containers/SignInContainer.tsx | 35 ++++- 3 files changed, 131 insertions(+), 45 deletions(-) diff --git a/src/core/client/auth/components/SignIn.tsx b/src/core/client/auth/components/SignIn.tsx index 9244aa2af..f1b94b61e 100644 --- a/src/core/client/auth/components/SignIn.tsx +++ b/src/core/client/auth/components/SignIn.tsx @@ -1,6 +1,13 @@ -import * as React from "react"; -import { StatelessComponent } from "react"; -import * as styles from "./SignIn.css"; +import React, { StatelessComponent } from "react"; +import { Field, Form } from "react-final-form"; +import { OnSubmit } from "talk-framework/lib/form"; + +import { + composeValidators, + required, + validateEmail, + validatePassword, +} from "talk-framework/lib/validation"; import { Button, @@ -9,43 +16,101 @@ import { InputLabel, TextField, Typography, + ValidationMessage, } from "talk-ui/components"; -const SignIn: StatelessComponent = props => { +import * as styles from "./SignIn.css"; + +interface FormProps { + email: string; + password: string; +} + +export interface SignInForm { + onSubmit: OnSubmit; +} + +const SignIn: StatelessComponent = props => { return ( - - - Sign in to join the conversation - - - Email Address - - - - Password - - - - - -
- - - Don't have an account? - - -
-
+
+ {({ handleSubmit, submitting }) => ( + + + + Sign in to join the conversation + + + + {({ input, meta }) => ( + + Email Address + + {meta.touched && + (meta.error || meta.submitError) && ( + + {meta.error || meta.submitError} + + )} + + )} + + + + {({ input, meta }) => ( + + Password + + {meta.touched && + (meta.error || meta.submitError) && ( + + {meta.error || meta.submitError} + + )} + + + + + )} + + +
+ + + Don't have an account? + + +
+
+
+ )} + ); }; diff --git a/src/core/client/auth/components/SignUp.tsx b/src/core/client/auth/components/SignUp.tsx index 76f7d952a..7924ff5e2 100644 --- a/src/core/client/auth/components/SignUp.tsx +++ b/src/core/client/auth/components/SignUp.tsx @@ -37,7 +37,7 @@ export interface SignUpForm { const SignUp: StatelessComponent = props => { return (
- {({ handleSubmit, submitting, invalid }) => ( + {({ handleSubmit, submitting }) => ( diff --git a/src/core/client/auth/containers/SignInContainer.tsx b/src/core/client/auth/containers/SignInContainer.tsx index aabf4641d..fa4fa2d80 100644 --- a/src/core/client/auth/containers/SignInContainer.tsx +++ b/src/core/client/auth/containers/SignInContainer.tsx @@ -1,10 +1,31 @@ -import * as React from "react"; -import { StatelessComponent } from "react"; +import { BadUserInputError } from "talk-framework/lib/errors"; +import SignIn, { SignInForm } from "../components/SignIn"; -import SignIn from "../components/SignIn"; +import React, { Component } from "react"; +import { SignInMutation, withSignInMutation } from "../mutations"; -const SignInContainer: StatelessComponent = () => { - return ; -}; +interface SignInContainerProps { + signIn: SignInMutation; +} -export default SignInContainer; +class SignInContainer extends Component { + private onSubmit: SignInForm["onSubmit"] = async (input, form) => { + try { + await this.props.signIn(input); + form.reset(); + } catch (error) { + if (error instanceof BadUserInputError) { + return error.invalidArgsLocalized; + } + // tslint:disable-next-line:no-console + console.error(error); + } + return undefined; + }; + public render() { + return ; + } +} + +const enhanced = withSignInMutation(SignInContainer); +export default enhanced; From a94e56d45fb2766a7f22f8927b302c0feaa94623 Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Mon, 13 Aug 2018 13:37:18 -0300 Subject: [PATCH 05/27] Adding form validations for Forgot Password, ResetPassword, and more --- .../client/auth/components/ForgotPassword.tsx | 82 ++++++++---- .../client/auth/components/ResetPassword.tsx | 118 ++++++++++++++---- .../containers/ForgotPasswordContainer.tsx | 13 +- .../containers/ResetPasswordContainer.tsx | 13 +- 4 files changed, 171 insertions(+), 55 deletions(-) diff --git a/src/core/client/auth/components/ForgotPassword.tsx b/src/core/client/auth/components/ForgotPassword.tsx index 4c06dcbc4..e99230c9e 100644 --- a/src/core/client/auth/components/ForgotPassword.tsx +++ b/src/core/client/auth/components/ForgotPassword.tsx @@ -1,7 +1,14 @@ -import * as React from "react"; -import { StatelessComponent } from "react"; +import React, { StatelessComponent } from "react"; +import { Field, Form } from "react-final-form"; +import { OnSubmit } from "talk-framework/lib/form"; import * as styles from "./SignIn.css"; +import { + composeValidators, + required, + validateEmail, +} from "talk-framework/lib/validation"; + import { Button, Flex, @@ -9,28 +16,61 @@ import { InputLabel, TextField, Typography, + ValidationMessage, } from "talk-ui/components"; -const ForgotPassword: StatelessComponent = props => { +interface FormProps { + email: string; +} + +export interface ForgotPasswordForm { + onSubmit: OnSubmit; +} + +const ForgotPassword: StatelessComponent = props => { return ( - - - Forgot Password - - - Enter your email address below and we will send you a link to reset your - password. - - - Email Address - - -
- -
-
+ + {({ handleSubmit }) => ( + + + + Forgot Password + + + Enter your email address below and we will send you a link to + reset your password. + + + {({ input, meta }) => ( + + Email Address + + {meta.touched && + (meta.error || meta.submitError) && ( + + {meta.error || meta.submitError} + + )} + + )} + +
+ +
+
+ + )} + ); }; diff --git a/src/core/client/auth/components/ResetPassword.tsx b/src/core/client/auth/components/ResetPassword.tsx index 5f000d091..3ca37cf80 100644 --- a/src/core/client/auth/components/ResetPassword.tsx +++ b/src/core/client/auth/components/ResetPassword.tsx @@ -1,37 +1,111 @@ -import * as React from "react"; -import { StatelessComponent } from "react"; +import React, { StatelessComponent } from "react"; +import { Field, Form } from "react-final-form"; +import { OnSubmit } from "talk-framework/lib/form"; import * as styles from "./SignIn.css"; +import { + composeValidators, + required, + validateEqualPasswords, + validatePassword, +} from "talk-framework/lib/validation"; + import { Button, Flex, FormField, + InputDescription, InputLabel, TextField, Typography, + ValidationMessage, } from "talk-ui/components"; -const ResetPassword: StatelessComponent = props => { +interface FormProps { + password: string; + confirmPassword: string; +} + +export interface ResetPasswordForm { + onSubmit: OnSubmit; +} + +const ResetPassword: StatelessComponent = props => { return ( - - - Reset Password - - - Password - Must be at least 8 characters - - - - Confirm Password - - -
- -
-
+
+ {({ handleSubmit }) => ( + + + + Reset Password + + + {({ input, meta }) => ( + + Password + + Must be at least 8 characters + + + {meta.touched && + (meta.error || meta.submitError) && ( + + {meta.error || meta.submitError} + + )} + + )} + + + + {({ input, meta }) => ( + + Confirm Password + + Must be at least 8 characters + + + {meta.touched && + (meta.error || meta.submitError) && ( + + {meta.error || meta.submitError} + + )} + + )} + + +
+ +
+
+
+ )} + ); }; diff --git a/src/core/client/auth/containers/ForgotPasswordContainer.tsx b/src/core/client/auth/containers/ForgotPasswordContainer.tsx index b364e7e9e..37b842160 100644 --- a/src/core/client/auth/containers/ForgotPasswordContainer.tsx +++ b/src/core/client/auth/containers/ForgotPasswordContainer.tsx @@ -1,10 +1,11 @@ -import * as React from "react"; -import { StatelessComponent } from "react"; - +import React, { Component } from "react"; import ForgotPassword from "../components/ForgotPassword"; -const ForgotPasswordContainer: StatelessComponent = () => { - return ; -}; +class ForgotPasswordContainer extends Component { + public render() { + // tslint:disable-next-line:no-empty + return {}} />; + } +} export default ForgotPasswordContainer; diff --git a/src/core/client/auth/containers/ResetPasswordContainer.tsx b/src/core/client/auth/containers/ResetPasswordContainer.tsx index 6dab0eef1..cfca96d9d 100644 --- a/src/core/client/auth/containers/ResetPasswordContainer.tsx +++ b/src/core/client/auth/containers/ResetPasswordContainer.tsx @@ -1,10 +1,11 @@ -import * as React from "react"; -import { StatelessComponent } from "react"; - +import React, { Component } from "react"; import ResetPassword from "../components/ResetPassword"; -const ResetPasswordContainer: StatelessComponent = () => { - return ; -}; +class ResetPasswordContainer extends Component { + public render() { + // tslint:disable-next-line:no-empty + return {}} />; + } +} export default ResetPasswordContainer; From c40e5c58b205be83f84cc50fedb5195b68413787 Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Mon, 13 Aug 2018 13:52:18 -0300 Subject: [PATCH 06/27] Adding inner routing between views --- src/core/client/auth/components/SignIn.tsx | 23 +++++++++++++++---- src/core/client/auth/components/SignUp.tsx | 14 ++++++++--- .../auth/containers/SignInContainer.tsx | 22 ++++++++++++++---- .../auth/containers/SignUpContainer.tsx | 21 +++++++++++++---- 4 files changed, 63 insertions(+), 17 deletions(-) diff --git a/src/core/client/auth/components/SignIn.tsx b/src/core/client/auth/components/SignIn.tsx index f1b94b61e..f0e88fdd4 100644 --- a/src/core/client/auth/components/SignIn.tsx +++ b/src/core/client/auth/components/SignIn.tsx @@ -1,14 +1,12 @@ import React, { StatelessComponent } from "react"; import { Field, Form } from "react-final-form"; import { OnSubmit } from "talk-framework/lib/form"; - import { composeValidators, required, validateEmail, validatePassword, } from "talk-framework/lib/validation"; - import { Button, Flex, @@ -18,7 +16,7 @@ import { Typography, ValidationMessage, } from "talk-ui/components"; - +import { View } from "../containers/SignInContainer"; import * as styles from "./SignIn.css"; interface FormProps { @@ -28,6 +26,7 @@ interface FormProps { export interface SignInForm { onSubmit: OnSubmit; + setView: (view: View) => void; } const SignIn: StatelessComponent = props => { @@ -84,7 +83,14 @@ const SignIn: StatelessComponent = props => { )} - @@ -102,7 +108,14 @@ const SignIn: StatelessComponent = props => { className={styles.subFooter} > Don't have an account? -
diff --git a/src/core/client/auth/components/SignUp.tsx b/src/core/client/auth/components/SignUp.tsx index 7924ff5e2..5e4b913e6 100644 --- a/src/core/client/auth/components/SignUp.tsx +++ b/src/core/client/auth/components/SignUp.tsx @@ -10,8 +10,6 @@ import { validatePassword, validateUsername, } from "talk-framework/lib/validation"; -import * as styles from "./SignUp.css"; - import { Button, Flex, @@ -22,6 +20,8 @@ import { Typography, ValidationMessage, } from "talk-ui/components"; +import { View } from "../containers/SignUpContainer"; +import * as styles from "./SignUp.css"; interface FormProps { email: string; @@ -32,6 +32,7 @@ interface FormProps { export interface SignUpForm { onSubmit: OnSubmit; + setView: (view: View) => void; } const SignUp: StatelessComponent = props => { @@ -163,7 +164,14 @@ const SignUp: StatelessComponent = props => { className={styles.subFooter} > Already have an account? -
diff --git a/src/core/client/auth/containers/SignInContainer.tsx b/src/core/client/auth/containers/SignInContainer.tsx index fa4fa2d80..3b9892dbe 100644 --- a/src/core/client/auth/containers/SignInContainer.tsx +++ b/src/core/client/auth/containers/SignInContainer.tsx @@ -1,14 +1,26 @@ +import React, { Component } from "react"; import { BadUserInputError } from "talk-framework/lib/errors"; import SignIn, { SignInForm } from "../components/SignIn"; - -import React, { Component } from "react"; -import { SignInMutation, withSignInMutation } from "../mutations"; +import { + SetViewMutation, + SignInMutation, + withSetViewMutation, + withSignInMutation, +} from "../mutations"; interface SignInContainerProps { signIn: SignInMutation; + setView: SetViewMutation; } +export type View = "SIGN_UP" | "FORGOT_PASSWORD"; + class SignInContainer extends Component { + private setView = (view: View) => { + this.props.setView({ + view, + }); + }; private onSubmit: SignInForm["onSubmit"] = async (input, form) => { try { await this.props.signIn(input); @@ -23,9 +35,9 @@ class SignInContainer extends Component { return undefined; }; public render() { - return ; + return ; } } -const enhanced = withSignInMutation(SignInContainer); +const enhanced = withSetViewMutation(withSignInMutation(SignInContainer)); export default enhanced; diff --git a/src/core/client/auth/containers/SignUpContainer.tsx b/src/core/client/auth/containers/SignUpContainer.tsx index eac0f8d14..6cbe4bf02 100644 --- a/src/core/client/auth/containers/SignUpContainer.tsx +++ b/src/core/client/auth/containers/SignUpContainer.tsx @@ -1,14 +1,27 @@ +import React, { Component } from "react"; import { BadUserInputError } from "talk-framework/lib/errors"; import SignUp, { SignUpForm } from "../components/SignUp"; -import React, { Component } from "react"; -import { SignUpMutation, withSignUpMutation } from "../mutations"; +import { + SetViewMutation, + SignUpMutation, + withSetViewMutation, + withSignUpMutation, +} from "../mutations"; interface SignUpContainerProps { signUp: SignUpMutation; + setView: SetViewMutation; } +export type View = "SIGN_IN"; + class SignUpContainer extends Component { + private setView = (view: View) => { + this.props.setView({ + view, + }); + }; private onSubmit: SignUpForm["onSubmit"] = async (input, form) => { try { await this.props.signUp(input); @@ -23,9 +36,9 @@ class SignUpContainer extends Component { return undefined; }; public render() { - return ; + return ; } } -const enhanced = withSignUpMutation(SignUpContainer); +const enhanced = withSetViewMutation(withSignUpMutation(SignUpContainer)); export default enhanced; From 0b3aead1d2c14add3dee7fa9c93b01c5585169dc Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Tue, 14 Aug 2018 14:37:00 +0000 Subject: [PATCH 07/27] [next] Perspective API Integration (#1797) * feat: initial toxic comments impl * feat: improved logging * feat: tenant cache adapter * feat: move more types into graphql --- package-lock.json | 501 ++++++++++-------- package.json | 4 + scripts/generateSchemaTypes.js | 2 +- src/core/common/config.ts | 8 + src/core/server/app/index.ts | 2 + src/core/server/app/middleware/error.ts | 17 + src/core/server/app/middleware/logging.ts | 6 +- .../server/app/middleware/passport/index.ts | 2 + .../server/app/middleware/passport/oidc.ts | 16 +- .../tenant/resolvers/auth_integrations.ts | 7 +- .../server/graph/tenant/schema/schema.graphql | 134 ++++- src/core/server/index.ts | 6 +- src/core/server/models/actions.ts | 3 +- src/core/server/models/comment.ts | 2 +- src/core/server/models/settings.ts | 92 +--- src/core/server/models/tenant.ts | 3 + src/core/server/services/comments/index.ts | 7 +- .../comments/moderation/index.spec.ts | 105 ++++ .../services/comments/moderation/index.ts | 39 +- .../comments/moderation/phases/assetClosed.ts | 11 +- .../moderation/phases/commentLength.ts | 14 +- .../moderation/phases/commentingDisabled.ts | 7 +- .../comments/moderation/phases/index.ts | 2 + .../comments/moderation/phases/karma.ts | 15 +- .../comments/moderation/phases/links.ts | 13 +- .../comments/moderation/phases/premod.ts | 12 +- .../comments/moderation/phases/spam.ts | 99 +++- .../comments/moderation/phases/staff.ts | 9 +- .../comments/moderation/phases/toxic.ts | 183 +++++++ .../comments/moderation/phases/wordlist.ts | 16 +- .../server/services/tenant/cache/adapter.ts | 92 ++++ .../tenant/{cache.ts => cache/index.ts} | 72 ++- src/core/server/services/users/karma.ts | 4 +- 33 files changed, 1088 insertions(+), 417 deletions(-) create mode 100644 src/core/server/services/comments/moderation/index.spec.ts create mode 100644 src/core/server/services/comments/moderation/phases/toxic.ts create mode 100644 src/core/server/services/tenant/cache/adapter.ts rename src/core/server/services/tenant/{cache.ts => cache/index.ts} (72%) diff --git a/package-lock.json b/package-lock.json index ee06575e5..0db1f4766 100644 --- a/package-lock.json +++ b/package-lock.json @@ -69,6 +69,14 @@ "dev": true, "requires": { "ms": "2.0.0" + }, + "dependencies": { + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } } }, "expand-brackets": { @@ -1310,6 +1318,14 @@ "dev": true, "requires": { "ms": "2.0.0" + }, + "dependencies": { + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } } } } @@ -1911,11 +1927,26 @@ "@types/node": "*" } }, + "@types/ms": { + "version": "0.7.30", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.30.tgz", + "integrity": "sha512-OftRLCgAzJP7vmKn9by/GVjnf4hloz/pXNOwPo0vKGAfXI7GqWXJi9N2kRar4cP5s1dGwuwcagWqO6iHBTq1Mg==", + "dev": true + }, "@types/node": { "version": "10.5.2", "resolved": "https://registry.npmjs.org/@types/node/-/node-10.5.2.tgz", "integrity": "sha512-m9zXmifkZsMHZBOyxZWilMwmTlpC8x5Ty360JKTiXvlXZfBWYpsg9ZZvP/Ye+iZUh+Q+MxDLjItVTWIsfwz+8Q==" }, + "@types/node-fetch": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.1.2.tgz", + "integrity": "sha512-XroxUzLpKuL+CVkQqXlffRkEPi4Gh3Oui/mWyS7ztKiyqVxiU+h3imCW5I2NQmde5jK+3q++36/Q96cyRWsweg==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/nunjucks": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@types/nunjucks/-/nunjucks-3.0.0.tgz", @@ -2211,6 +2242,14 @@ "dev": true, "requires": { "ms": "2.0.0" + }, + "dependencies": { + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } } } } @@ -2243,6 +2282,14 @@ "dev": true, "requires": { "ms": "2.0.0" + }, + "dependencies": { + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } } } } @@ -2279,6 +2326,14 @@ "dev": true, "requires": { "ms": "2.0.0" + }, + "dependencies": { + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } } } } @@ -2309,6 +2364,14 @@ "dev": true, "requires": { "ms": "2.0.0" + }, + "dependencies": { + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } } } } @@ -2361,6 +2424,14 @@ "dev": true, "requires": { "ms": "2.0.0" + }, + "dependencies": { + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } } } } @@ -2398,6 +2469,14 @@ "dev": true, "requires": { "ms": "2.0.0" + }, + "dependencies": { + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } } } } @@ -4821,6 +4900,14 @@ "dev": true, "requires": { "ms": "2.0.0" + }, + "dependencies": { + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } } }, "expand-brackets": { @@ -6879,6 +6966,11 @@ "whatwg-fetch": "2.0.3" }, "dependencies": { + "node-fetch": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.0.0.tgz", + "integrity": "sha1-mCu6Q+zU8pIqKcwYamu7C7c/y6Y=" + }, "whatwg-fetch": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-2.0.3.tgz", @@ -7383,6 +7475,13 @@ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "requires": { "ms": "2.0.0" + }, + "dependencies": { + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + } } }, "debug-fabulous": { @@ -7404,6 +7503,12 @@ "requires": { "ms": "2.0.0" } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true } } }, @@ -8320,6 +8425,14 @@ "dev": true, "requires": { "ms": "2.0.0" + }, + "dependencies": { + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } } }, "decamelize": { @@ -8348,6 +8461,14 @@ "dev": true, "requires": { "ms": "2.0.0" + }, + "dependencies": { + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } } } } @@ -9834,13 +9955,6 @@ "dev": true, "requires": { "intl-pluralrules": "github:projectfluent/IntlPluralRules#94cb0fa1c23ad943bc5aafef43cea132fa51d68b" - }, - "dependencies": { - "intl-pluralrules": { - "version": "github:projectfluent/IntlPluralRules#94cb0fa1c23ad943bc5aafef43cea132fa51d68b", - "from": "github:projectfluent/IntlPluralRules#module", - "dev": true - } } }, "fluent-langneg": { @@ -9885,6 +9999,14 @@ "dev": true, "requires": { "ms": "2.0.0" + }, + "dependencies": { + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } } } } @@ -10045,24 +10167,24 @@ "dependencies": { "abbrev": { "version": "1.1.1", - "resolved": "", + "resolved": false, "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", "optional": true }, "ansi-regex": { "version": "2.1.1", - "resolved": "", + "resolved": false, "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" }, "aproba": { "version": "1.2.0", - "resolved": "", + "resolved": false, "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", "optional": true }, "are-we-there-yet": { "version": "1.1.4", - "resolved": "", + "resolved": false, "integrity": "sha1-u13KOCu5TwXhUZQ3PRb9O6HKEQ0=", "optional": true, "requires": { @@ -10072,12 +10194,12 @@ }, "balanced-match": { "version": "1.0.0", - "resolved": "", + "resolved": false, "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" }, "brace-expansion": { "version": "1.1.11", - "resolved": "", + "resolved": false, "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "requires": { "balanced-match": "^1.0.0", @@ -10086,34 +10208,34 @@ }, "chownr": { "version": "1.0.1", - "resolved": "", + "resolved": false, "integrity": "sha1-4qdQQqlVGQi+vSW4Uj1fl2nXkYE=", "optional": true }, "code-point-at": { "version": "1.1.0", - "resolved": "", + "resolved": false, "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" }, "concat-map": { "version": "0.0.1", - "resolved": "", + "resolved": false, "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" }, "console-control-strings": { "version": "1.1.0", - "resolved": "", + "resolved": false, "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" }, "core-util-is": { "version": "1.0.2", - "resolved": "", + "resolved": false, "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", "optional": true }, "debug": { "version": "2.6.9", - "resolved": "", + "resolved": false, "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "optional": true, "requires": { @@ -10122,25 +10244,25 @@ }, "deep-extend": { "version": "0.5.1", - "resolved": "", + "resolved": false, "integrity": "sha512-N8vBdOa+DF7zkRrDCsaOXoCs/E2fJfx9B9MrKnnSiHNh4ws7eSys6YQE4KvT1cecKmOASYQBhbKjeuDD9lT81w==", "optional": true }, "delegates": { "version": "1.0.0", - "resolved": "", + "resolved": false, "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", "optional": true }, "detect-libc": { "version": "1.0.3", - "resolved": "", + "resolved": false, "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=", "optional": true }, "fs-minipass": { "version": "1.2.5", - "resolved": "", + "resolved": false, "integrity": "sha512-JhBl0skXjUPCFH7x6x61gQxrKyXsxB5gcgePLZCwfyCGGsTISMoIeObbrvVeP6Xmyaudw4TT43qV2Gz+iyd2oQ==", "optional": true, "requires": { @@ -10149,13 +10271,13 @@ }, "fs.realpath": { "version": "1.0.0", - "resolved": "", + "resolved": false, "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", "optional": true }, "gauge": { "version": "2.7.4", - "resolved": "", + "resolved": false, "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", "optional": true, "requires": { @@ -10171,7 +10293,7 @@ }, "glob": { "version": "7.1.2", - "resolved": "", + "resolved": false, "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", "optional": true, "requires": { @@ -10185,13 +10307,13 @@ }, "has-unicode": { "version": "2.0.1", - "resolved": "", + "resolved": false, "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", "optional": true }, "iconv-lite": { "version": "0.4.21", - "resolved": "", + "resolved": false, "integrity": "sha512-En5V9za5mBt2oUA03WGD3TwDv0MKAruqsuxstbMUZaj9W9k/m1CV/9py3l0L5kw9Bln8fdHQmzHSYtvpvTLpKw==", "optional": true, "requires": { @@ -10200,7 +10322,7 @@ }, "ignore-walk": { "version": "3.0.1", - "resolved": "", + "resolved": false, "integrity": "sha512-DTVlMx3IYPe0/JJcYP7Gxg7ttZZu3IInhuEhbchuqneY9wWe5Ojy2mXLBaQFUQmo0AW2r3qG7m1mg86js+gnlQ==", "optional": true, "requires": { @@ -10209,7 +10331,7 @@ }, "inflight": { "version": "1.0.6", - "resolved": "", + "resolved": false, "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", "optional": true, "requires": { @@ -10219,18 +10341,18 @@ }, "inherits": { "version": "2.0.3", - "resolved": "", + "resolved": false, "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" }, "ini": { "version": "1.3.5", - "resolved": "", + "resolved": false, "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", "optional": true }, "is-fullwidth-code-point": { "version": "1.0.0", - "resolved": "", + "resolved": false, "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", "requires": { "number-is-nan": "^1.0.0" @@ -10238,13 +10360,13 @@ }, "isarray": { "version": "1.0.0", - "resolved": "", + "resolved": false, "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", "optional": true }, "minimatch": { "version": "3.0.4", - "resolved": "", + "resolved": false, "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", "requires": { "brace-expansion": "^1.1.7" @@ -10252,12 +10374,12 @@ }, "minimist": { "version": "0.0.8", - "resolved": "", + "resolved": false, "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" }, "minipass": { "version": "2.2.4", - "resolved": "", + "resolved": false, "integrity": "sha512-hzXIWWet/BzWhYs2b+u7dRHlruXhwdgvlTMDKC6Cb1U7ps6Ac6yQlR39xsbjWJE377YTCtKwIXIpJ5oP+j5y8g==", "requires": { "safe-buffer": "^5.1.1", @@ -10266,7 +10388,7 @@ }, "minizlib": { "version": "1.1.0", - "resolved": "", + "resolved": false, "integrity": "sha512-4T6Ur/GctZ27nHfpt9THOdRZNgyJ9FZchYO1ceg5S8Q3DNLCKYy44nCZzgCJgcvx2UM8czmqak5BCxJMrq37lA==", "optional": true, "requires": { @@ -10275,7 +10397,7 @@ }, "mkdirp": { "version": "0.5.1", - "resolved": "", + "resolved": false, "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", "requires": { "minimist": "0.0.8" @@ -10283,13 +10405,13 @@ }, "ms": { "version": "2.0.0", - "resolved": "", + "resolved": false, "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", "optional": true }, "needle": { "version": "2.2.0", - "resolved": "", + "resolved": false, "integrity": "sha512-eFagy6c+TYayorXw/qtAdSvaUpEbBsDwDyxYFgLZ0lTojfH7K+OdBqAF7TAFwDokJaGpubpSGG0wO3iC0XPi8w==", "optional": true, "requires": { @@ -10300,7 +10422,7 @@ }, "node-pre-gyp": { "version": "0.10.0", - "resolved": "", + "resolved": false, "integrity": "sha512-G7kEonQLRbcA/mOoFoxvlMrw6Q6dPf92+t/l0DFSMuSlDoWaI9JWIyPwK0jyE1bph//CUEL65/Fz1m2vJbmjQQ==", "optional": true, "requires": { @@ -10318,7 +10440,7 @@ }, "nopt": { "version": "4.0.1", - "resolved": "", + "resolved": false, "integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=", "optional": true, "requires": { @@ -10328,13 +10450,13 @@ }, "npm-bundled": { "version": "1.0.3", - "resolved": "", + "resolved": false, "integrity": "sha512-ByQ3oJ/5ETLyglU2+8dBObvhfWXX8dtPZDMePCahptliFX2iIuhyEszyFk401PZUNQH20vvdW5MLjJxkwU80Ow==", "optional": true }, "npm-packlist": { "version": "1.1.10", - "resolved": "", + "resolved": false, "integrity": "sha512-AQC0Dyhzn4EiYEfIUjCdMl0JJ61I2ER9ukf/sLxJUcZHfo+VyEfz2rMJgLZSS1v30OxPQe1cN0LZA1xbcaVfWA==", "optional": true, "requires": { @@ -10344,7 +10466,7 @@ }, "npmlog": { "version": "4.1.2", - "resolved": "", + "resolved": false, "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", "optional": true, "requires": { @@ -10356,18 +10478,18 @@ }, "number-is-nan": { "version": "1.0.1", - "resolved": "", + "resolved": false, "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" }, "object-assign": { "version": "4.1.1", - "resolved": "", + "resolved": false, "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", "optional": true }, "once": { "version": "1.4.0", - "resolved": "", + "resolved": false, "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", "requires": { "wrappy": "1" @@ -10375,19 +10497,19 @@ }, "os-homedir": { "version": "1.0.2", - "resolved": "", + "resolved": false, "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", "optional": true }, "os-tmpdir": { "version": "1.0.2", - "resolved": "", + "resolved": false, "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", "optional": true }, "osenv": { "version": "0.1.5", - "resolved": "", + "resolved": false, "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", "optional": true, "requires": { @@ -10397,19 +10519,19 @@ }, "path-is-absolute": { "version": "1.0.1", - "resolved": "", + "resolved": false, "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", "optional": true }, "process-nextick-args": { "version": "2.0.0", - "resolved": "", + "resolved": false, "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", "optional": true }, "rc": { "version": "1.2.7", - "resolved": "", + "resolved": false, "integrity": "sha512-LdLD8xD4zzLsAT5xyushXDNscEjB7+2ulnl8+r1pnESlYtlJtVSoCMBGr30eDRJ3+2Gq89jK9P9e4tCEH1+ywA==", "optional": true, "requires": { @@ -10421,7 +10543,7 @@ "dependencies": { "minimist": { "version": "1.2.0", - "resolved": "", + "resolved": false, "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", "optional": true } @@ -10429,7 +10551,7 @@ }, "readable-stream": { "version": "2.3.6", - "resolved": "", + "resolved": false, "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "optional": true, "requires": { @@ -10444,7 +10566,7 @@ }, "rimraf": { "version": "2.6.2", - "resolved": "", + "resolved": false, "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", "optional": true, "requires": { @@ -10453,42 +10575,42 @@ }, "safe-buffer": { "version": "5.1.1", - "resolved": "", + "resolved": false, "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" }, "safer-buffer": { "version": "2.1.2", - "resolved": "", + "resolved": false, "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "optional": true }, "sax": { "version": "1.2.4", - "resolved": "", + "resolved": false, "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", "optional": true }, "semver": { "version": "5.5.0", - "resolved": "", + "resolved": false, "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==", "optional": true }, "set-blocking": { "version": "2.0.0", - "resolved": "", + "resolved": false, "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", "optional": true }, "signal-exit": { "version": "3.0.2", - "resolved": "", + "resolved": false, "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", "optional": true }, "string-width": { "version": "1.0.2", - "resolved": "", + "resolved": false, "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", "requires": { "code-point-at": "^1.0.0", @@ -10498,7 +10620,7 @@ }, "string_decoder": { "version": "1.1.1", - "resolved": "", + "resolved": false, "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "optional": true, "requires": { @@ -10507,7 +10629,7 @@ }, "strip-ansi": { "version": "3.0.1", - "resolved": "", + "resolved": false, "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", "requires": { "ansi-regex": "^2.0.0" @@ -10515,13 +10637,13 @@ }, "strip-json-comments": { "version": "2.0.1", - "resolved": "", + "resolved": false, "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", "optional": true }, "tar": { "version": "4.4.1", - "resolved": "", + "resolved": false, "integrity": "sha512-O+v1r9yN4tOsvl90p5HAP4AEqbYhx4036AGMm075fH9F8Qwi3oJ+v4u50FkT/KkvywNGtwkk0zRI+8eYm1X/xg==", "optional": true, "requires": { @@ -10536,13 +10658,13 @@ }, "util-deprecate": { "version": "1.0.2", - "resolved": "", + "resolved": false, "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", "optional": true }, "wide-align": { "version": "1.1.2", - "resolved": "", + "resolved": false, "integrity": "sha512-ijDLlyQ7s6x1JgCLur53osjm/UXUYD9+0PbYKrBsYisYXzCxN+HC3mYDNy/dWdmf3AwqwU3CXwDCvsNgGK1S0w==", "optional": true, "requires": { @@ -10551,12 +10673,12 @@ }, "wrappy": { "version": "1.0.2", - "resolved": "", + "resolved": false, "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" }, "yallist": { "version": "3.0.2", - "resolved": "", + "resolved": false, "integrity": "sha1-hFK0u36Dx8GI2AQcGoN8dz1ti7k=" } } @@ -11236,15 +11358,6 @@ "is-utf8": "^0.2.0" } }, - "v8flags": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-3.1.1.tgz", - "integrity": "sha512-iw/1ViSEaff8NJ3HLyEjawk/8hjJib3E7pvG4pddVXfUg1983s3VGsiClDjhK64MQVDGqc1Q8r18S4VKQZS9EQ==", - "dev": true, - "requires": { - "homedir-polyfill": "^1.0.1" - } - }, "which-module": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/which-module/-/which-module-1.0.0.tgz", @@ -11346,113 +11459,11 @@ "integrity": "sha512-yAdfUZ+c2wetVNIFsNRn44THW+Lty6S5TwMpUfLA/UaGhiXbBv/F8E60/1hMLd0cnF/CDoWH8vzVaI5bAcHCjw==", "dev": true }, - "clone": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", - "integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=", - "dev": true - }, - "clone-stats": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/clone-stats/-/clone-stats-1.0.0.tgz", - "integrity": "sha1-s3gt/4u1R04Yuba/D9/ngvh3doA=", - "dev": true - }, - "glob": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", - "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "glob-stream": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/glob-stream/-/glob-stream-6.1.0.tgz", - "integrity": "sha1-cEXJlBOz65SIjYOrRtC0BMx73eQ=", - "dev": true, - "requires": { - "extend": "^3.0.0", - "glob": "^7.1.1", - "glob-parent": "^3.1.0", - "is-negated-glob": "^1.0.0", - "ordered-read-streams": "^1.0.0", - "pumpify": "^1.3.5", - "readable-stream": "^2.1.5", - "remove-trailing-separator": "^1.0.1", - "to-absolute-glob": "^2.0.0", - "unique-stream": "^2.0.2" - } - }, - "ordered-read-streams": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/ordered-read-streams/-/ordered-read-streams-1.0.1.tgz", - "integrity": "sha1-d8DLN8QVJdZBZtmQ/61+xqDhNj4=", - "dev": true, - "requires": { - "readable-stream": "^2.0.1" - } - }, "source-map": { "version": "0.7.3", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", "dev": true - }, - "unique-stream": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/unique-stream/-/unique-stream-2.2.1.tgz", - "integrity": "sha1-WqADz76Uxf+GbE59ZouxxNuts2k=", - "dev": true, - "requires": { - "json-stable-stringify": "^1.0.0", - "through2-filter": "^2.0.0" - } - }, - "vinyl": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-2.2.0.tgz", - "integrity": "sha512-MBH+yP0kC/GQ5GwBqrTPTzEfiiLjta7hTtvQtbxBgTeSXsmKQRQecjibMbxIXzVT3Y9KJK+drOz1/k+vsu8Nkg==", - "dev": true, - "requires": { - "clone": "^2.1.1", - "clone-buffer": "^1.0.0", - "clone-stats": "^1.0.0", - "cloneable-readable": "^1.0.0", - "remove-trailing-separator": "^1.0.1", - "replace-ext": "^1.0.0" - } - }, - "vinyl-fs": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/vinyl-fs/-/vinyl-fs-3.0.3.tgz", - "integrity": "sha512-vIu34EkyNyJxmP0jscNzWBSygh7VWhqun6RmqVfXePrOwi9lhvRs//dOaGOTRUQr4tx7/zd26Tk5WeSVZitgng==", - "dev": true, - "requires": { - "fs-mkdirp-stream": "^1.0.0", - "glob-stream": "^6.1.0", - "graceful-fs": "^4.0.0", - "is-valid-glob": "^1.0.0", - "lazystream": "^1.0.0", - "lead": "^1.0.0", - "object.assign": "^4.0.4", - "pumpify": "^1.3.5", - "readable-stream": "^2.3.3", - "remove-bom-buffer": "^3.0.0", - "remove-bom-stream": "^1.2.0", - "resolve-options": "^1.1.0", - "through2": "^2.0.0", - "to-through": "^2.0.0", - "value-or-function": "^3.0.0", - "vinyl": "^2.0.0", - "vinyl-sourcemap": "^1.1.0" - } } } }, @@ -12373,6 +12384,11 @@ "integrity": "sha1-ftGxQQxqDg94z5XTuEQMY/eLhhQ=", "dev": true }, + "intl-pluralrules": { + "version": "github:projectfluent/IntlPluralRules#94cb0fa1c23ad943bc5aafef43cea132fa51d68b", + "from": "github:projectfluent/IntlPluralRules#module", + "dev": true + }, "invariant": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", @@ -13093,6 +13109,14 @@ "dev": true, "requires": { "ms": "2.0.0" + }, + "dependencies": { + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } } }, "glob": { @@ -15156,6 +15180,14 @@ "dev": true, "requires": { "ms": "2.0.0" + }, + "dependencies": { + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } } }, "koa-compose": { @@ -15232,6 +15264,14 @@ "dev": true, "requires": { "ms": "2.0.0" + }, + "dependencies": { + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } } } } @@ -15253,6 +15293,14 @@ "dev": true, "requires": { "ms": "2.0.0" + }, + "dependencies": { + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } } } } @@ -16406,9 +16454,9 @@ } }, "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" }, "multicast-dns": { "version": "6.2.3", @@ -16622,9 +16670,9 @@ } }, "node-fetch": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.0.0.tgz", - "integrity": "sha1-mCu6Q+zU8pIqKcwYamu7C7c/y6Y=" + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.2.0.tgz", + "integrity": "sha512-OayFWziIxiHY8bCUyLX6sTpDH8Jsbp4FfYd1j1f7vZyfgkcOnAyM4oQR16f8a0s7Gl/viMGRey8eScYk4V4EZA==" }, "node-forge": { "version": "0.7.5", @@ -17695,9 +17743,9 @@ "dev": true }, "popper.js": { - "version": "1.14.4", - "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.14.4.tgz", - "integrity": "sha1-juwdj/AqWjoVLdQ0FKFce3n9abY=", + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.14.3.tgz", + "integrity": "sha1-FDj5jQRqz3tNeM1QK/QYrGTU8JU=", "dev": true }, "portfinder": { @@ -22114,6 +22162,13 @@ "on-finished": "~2.3.0", "range-parser": "~1.2.0", "statuses": "~1.4.0" + }, + "dependencies": { + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + } } }, "serialize-error": { @@ -22988,6 +23043,13 @@ "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", "requires": { "ms": "2.0.0" + }, + "dependencies": { + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + } } } } @@ -24601,6 +24663,15 @@ "integrity": "sha512-qNdTUMaCjPs4eEnM3W9H94R3sU70YCuT+/ST7nUf+id1bVOrdjrpUaeZLqPBPRph3hsgn4a4BvwpxhHZx+oSDg==", "dev": true }, + "v8flags": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-3.1.1.tgz", + "integrity": "sha512-iw/1ViSEaff8NJ3HLyEjawk/8hjJib3E7pvG4pddVXfUg1983s3VGsiClDjhK64MQVDGqc1Q8r18S4VKQZS9EQ==", + "dev": true, + "requires": { + "homedir-polyfill": "^1.0.1" + } + }, "validate-npm-package-license": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.3.tgz", @@ -24741,34 +24812,6 @@ "now-and-later": "^2.0.0", "remove-bom-buffer": "^3.0.0", "vinyl": "^2.0.0" - }, - "dependencies": { - "clone": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", - "integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=", - "dev": true - }, - "clone-stats": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/clone-stats/-/clone-stats-1.0.0.tgz", - "integrity": "sha1-s3gt/4u1R04Yuba/D9/ngvh3doA=", - "dev": true - }, - "vinyl": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-2.2.0.tgz", - "integrity": "sha512-MBH+yP0kC/GQ5GwBqrTPTzEfiiLjta7hTtvQtbxBgTeSXsmKQRQecjibMbxIXzVT3Y9KJK+drOz1/k+vsu8Nkg==", - "dev": true, - "requires": { - "clone": "^2.1.1", - "clone-buffer": "^1.0.0", - "clone-stats": "^1.0.0", - "cloneable-readable": "^1.0.0", - "remove-trailing-separator": "^1.0.1", - "replace-ext": "^1.0.0" - } - } } }, "vinyl-sourcemaps-apply": { @@ -25130,6 +25173,14 @@ "dev": true, "requires": { "ms": "2.0.0" + }, + "dependencies": { + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } } }, "supports-color": { @@ -25296,6 +25347,14 @@ "dev": true, "requires": { "ms": "2.0.0" + }, + "dependencies": { + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } } }, "decamelize": { diff --git a/package.json b/package.json index ec4f1a241..8ca462e9d 100644 --- a/package.json +++ b/package.json @@ -57,6 +57,8 @@ "lodash": "^4.17.10", "luxon": "^1.3.1", "mongodb": "^3.1.1", + "ms": "^2.1.1", + "node-fetch": "^2.2.0", "nunjucks": "^3.1.3", "passport": "^0.4.0", "passport-local": "^1.0.0", @@ -101,7 +103,9 @@ "@types/luxon": "^0.5.3", "@types/mini-css-extract-plugin": "^0.2.0", "@types/mongodb": "^3.1.1", + "@types/ms": "^0.7.30", "@types/node": "^10.5.2", + "@types/node-fetch": "^2.1.2", "@types/nunjucks": "^3.0.0", "@types/passport": "^0.4.6", "@types/passport-local": "^1.0.33", diff --git a/scripts/generateSchemaTypes.js b/scripts/generateSchemaTypes.js index f2c52a705..c52393830 100644 --- a/scripts/generateSchemaTypes.js +++ b/scripts/generateSchemaTypes.js @@ -66,7 +66,7 @@ async function main() { file.types = await generateTSTypesAsString(schema, { tabSpaces: 2, typePrefix: "GQL", - strictNulls: true, + strictNulls: false, ...file.config, }); } diff --git a/src/core/common/config.ts b/src/core/common/config.ts index 099bcc4b2..665370638 100644 --- a/src/core/common/config.ts +++ b/src/core/common/config.ts @@ -86,6 +86,14 @@ const config = convict({ env: "LOGGING_LEVEL", arg: "logging", }, + disable_tenant_caching: { + doc: + "Disables the tenant caching, all tenants will be loaded from MongoDB each time it's needed", + format: Boolean, + default: false, + env: "DISABLE_TENANT_CACHING", + arg: "disableTenantCaching", + }, }); export type Config = typeof config; diff --git a/src/core/server/app/index.ts b/src/core/server/app/index.ts index 5f7ba22e6..1559a37e5 100644 --- a/src/core/server/app/index.ts +++ b/src/core/server/app/index.ts @@ -14,6 +14,7 @@ import { handleSubscriptions } from "talk-server/graph/common/subscriptions/midd import { Schemas } from "talk-server/graph/schemas"; import TenantCache from "talk-server/services/tenant/cache"; +import { errorHandler } from "talk-server/app/middleware/error"; import { accessLogger, errorLogger } from "./middleware/logging"; import serveStatic from "./middleware/serveStatic"; import { createRouter } from "./router"; @@ -57,6 +58,7 @@ export async function createApp(options: AppOptions): Promise { // Error Handling parent.use(notFoundMiddleware); parent.use(errorLogger); + parent.use(errorHandler); return parent; } diff --git a/src/core/server/app/middleware/error.ts b/src/core/server/app/middleware/error.ts index 2f666260b..b7be782cb 100644 --- a/src/core/server/app/middleware/error.ts +++ b/src/core/server/app/middleware/error.ts @@ -4,3 +4,20 @@ export const apiErrorHandler: ErrorRequestHandler = (err, req, res, next) => { // TODO: handle better when we improve errors. res.status(500).json({ error: err.message }); }; + +export const errorHandler: ErrorRequestHandler = (err, req, res, next) => { + // TODO: handle better when we improve errors. + if (err.message === "not found") { + // TODO: handle better when we improve errors. + res + .status(404) + .send(err.message) + .end(); + } else { + // TODO: handle better when we improve errors. + res + .status(500) + .send(err.message) + .end(); + } +}; diff --git a/src/core/server/app/middleware/logging.ts b/src/core/server/app/middleware/logging.ts index 134c19df1..cc651825c 100644 --- a/src/core/server/app/middleware/logging.ts +++ b/src/core/server/app/middleware/logging.ts @@ -39,6 +39,10 @@ export const accessLogger: RequestHandler = (req, res, next) => { }; export const errorLogger: ErrorRequestHandler = (err, req, res, next) => { - logger.error(err, "http error"); + // TODO: handle better when we improve errors. + if (err.message !== "not found") { + logger.error({ err }, "http error"); + } + next(err); }; diff --git a/src/core/server/app/middleware/passport/index.ts b/src/core/server/app/middleware/passport/index.ts index a6bd0032b..ae1a3406f 100644 --- a/src/core/server/app/middleware/passport/index.ts +++ b/src/core/server/app/middleware/passport/index.ts @@ -2,6 +2,7 @@ import { NextFunction, RequestHandler, Response } from "express"; import { Db } from "mongodb"; import passport, { Authenticator } from "passport"; +import { Config } from "talk-common/config"; import { createJWTStrategy, JWTSigningConfig, @@ -22,6 +23,7 @@ export type VerifyCallback = ( ) => void; export interface PassportOptions { + config: Config; mongo: Db; signingConfig: JWTSigningConfig; tenantCache: TenantCache; diff --git a/src/core/server/app/middleware/passport/oidc.ts b/src/core/server/app/middleware/passport/oidc.ts index a835bd042..025e1bd4d 100644 --- a/src/core/server/app/middleware/passport/oidc.ts +++ b/src/core/server/app/middleware/passport/oidc.ts @@ -5,6 +5,7 @@ import { Db } from "mongodb"; import { Strategy as OAuth2Strategy, VerifyCallback } from "passport-oauth2"; import { Strategy } from "passport-strategy"; +import { Config } from "talk-common/config"; import { validate } from "talk-server/app/request/body"; import { reconstructURL } from "talk-server/app/url"; import { GQLUSER_ROLE } from "talk-server/graph/tenant/schema/__generated__/types"; @@ -12,6 +13,7 @@ import { OIDCAuthIntegration } from "talk-server/models/settings"; import { Tenant } from "talk-server/models/tenant"; import { OIDCProfile, retrieveUserWithProfile } from "talk-server/models/user"; import TenantCache from "talk-server/services/tenant/cache"; +import { TenantCacheAdapter } from "talk-server/services/tenant/cache/adapter"; import { upsert } from "talk-server/services/users"; import { Request } from "talk-server/types/express"; @@ -177,25 +179,23 @@ const OIDC_SCOPE = "openid email profile"; export interface OIDCStrategyOptions { mongo: Db; tenantCache: TenantCache; + config: Config; } export default class OIDCStrategy extends Strategy { public name = "oidc"; private mongo: Db; - private cache = new Map(); + private cache: TenantCacheAdapter; - constructor({ mongo, tenantCache }: OIDCStrategyOptions) { + constructor({ mongo, tenantCache, config }: OIDCStrategyOptions) { super(); this.mongo = mongo; + this.cache = new TenantCacheAdapter(tenantCache, config); - // Subscribe to updates with Tenants. - tenantCache.subscribe(tenant => { - // Delete the tenant cache item when the tenant changes. The refreshed - // Tenant will come in with the request. - this.cache.delete(tenant.id); - }); + // Connect the cache adapter. + this.cache.subscribe(); } private lookupJWKSClient( diff --git a/src/core/server/graph/tenant/resolvers/auth_integrations.ts b/src/core/server/graph/tenant/resolvers/auth_integrations.ts index 164e15bfe..b40d9d832 100644 --- a/src/core/server/graph/tenant/resolvers/auth_integrations.ts +++ b/src/core/server/graph/tenant/resolvers/auth_integrations.ts @@ -1,7 +1,10 @@ import { GQLAuthIntegrationsTypeResolver } from "talk-server/graph/tenant/schema/__generated__/types"; -import { AuthIntegration, AuthIntegrations } from "talk-server/models/settings"; +import { + AuthIntegrations, + EnableableIntegration, +} from "talk-server/models/settings"; -const disabled: AuthIntegration = { enabled: false }; +const disabled: EnableableIntegration = { enabled: false }; const AuthIntegrations: GQLAuthIntegrationsTypeResolver = { local: auth => auth.local || disabled, diff --git a/src/core/server/graph/tenant/schema/schema.graphql b/src/core/server/graph/tenant/schema/schema.graphql index 302b9e525..1c96051bd 100644 --- a/src/core/server/graph/tenant/schema/schema.graphql +++ b/src/core/server/graph/tenant/schema/schema.graphql @@ -38,6 +38,16 @@ enum ACTION_ITEM_TYPE { COMMENTS } +enum ACTION_GROUP { + SPAM_COMMENT + TOXIC_COMMENT + BODY_COUNT + TRUST + LINKS + BANNED_WORD + SUSPECT_WORD +} + ################################################################################ ## Settings ################################################################################ @@ -57,9 +67,9 @@ enum MODERATION_MODE { } """ -WordlistSettings describes all the available wordlists. +Wordlist describes all the available wordlists. """ -type WordlistSettings { +type Wordlist { """ banned words will by default reject the comment if it is found. """ @@ -177,6 +187,111 @@ type AuthSettings { integrations: AuthIntegrations! } +################################################################################ +## ExternalIntegrations +################################################################################ + +type AkismetExternalIntegration { + """ + enabled when True will enable the integration. + """ + enabled: Boolean! + + """ + The key for the Akismet integration. + """ + key: String + + """ + The site (blog) for the Akismet integration. + """ + site: String +} + +type PerspectiveExternalIntegration { + """ + enabled when True will enable the integration. + """ + enabled: Boolean! + + """ + The endpoint that Talk should use to communicate with the perspective API. + """ + endpoint: String + + """ + The key for the Perspective API integration. + """ + key: String + + """ + The threshold that given a specific toxic comment score, the comment will + be marked by Talk as toxic. + """ + threshold: Float + + """ + When True, comments sent will not be stored by the Google Perspective API. + """ + doNotStore: Boolean +} + +type ExternalIntegrations { + """ + akismet provides integration with the Akismet Spam detection service. + """ + akismet: AkismetExternalIntegration! + + """ + perspective provides integration with the Perspective API comment analysis + platform. + """ + perspective: PerspectiveExternalIntegration! +} + +################################################################################ +## Karma +################################################################################ + +""" +KarmaThreshold defines the bounds for which a User will become unreliable or +reliable based on their karma score. If the score is equal or less than the +unreliable value, they are unreliable. If the score is equal or more than the +reliable value, they are reliable. If they are neither reliable or unreliable +then they are neutral. +""" +type KarmaThreshold { + reliable: Int! + unreliable: Int! +} + +type KarmaThresholds { + """ + flag represents karma settings in relation to how well a User's flagging + ability aligns with the moderation decicions made by moderators. + """ + flag: KarmaThreshold! + + """ + comment represents the karma setting in relation to how well a User's comments are moderated. + """ + comment: KarmaThreshold! +} + +type Karma { + """ + When true, checks will be completed to ensure that the Karma checks are + completed. + """ + enabled: Boolean! + + """ + karmaThresholds contains the currently set thresholds for triggering Trust + beheviour. + """ + thresholds: KarmaThresholds! +} + ################################################################################ ## Settings ################################################################################ @@ -295,7 +410,7 @@ type Settings { """ wordlist will return a given list of words. """ - wordlist: WordlistSettings @auth(roles: [ADMIN, MODERATOR]) + wordlist: Wordlist @auth(roles: [ADMIN, MODERATOR]) """ domains will return a given list of whitelisted domains. @@ -306,6 +421,17 @@ type Settings { auth contains all the settings related to authentication and authorization. """ auth: AuthSettings! + + """ + integrations contains all the external integrations that can be enabled. + """ + integrations: ExternalIntegrations @auth(roles: [ADMIN]) + + """ + karma is the set of settings related to how user Trust and Karma are + handled. + """ + karma: Karma @auth(roles: [ADMIN, MODERATOR]) } ################################################################################ @@ -728,7 +854,7 @@ input SettingsInput { charCount: Int organizationName: String organizationContactEmail: String - # wordlist: WordlistSettings @auth(roles: [ADMIN, MODERATOR]) + # wordlist: Wordlist @auth(roles: [ADMIN, MODERATOR]) domains: [String!] # auth: AuthSettings! } diff --git a/src/core/server/index.ts b/src/core/server/index.ts index 47a0c2db4..b09763c26 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -70,7 +70,11 @@ class Server { const signingConfig = createJWTSigningConfig(this.config); // Create the TenantCache. - const tenantCache = new TenantCache(mongo, await createRedisClient(config)); + const tenantCache = new TenantCache( + mongo, + await createRedisClient(config), + config + ); // Prime the tenant cache so it'll be ready to serve now. await tenantCache.primeAll(); diff --git a/src/core/server/models/actions.ts b/src/core/server/models/actions.ts index f304da550..d162b1300 100644 --- a/src/core/server/models/actions.ts +++ b/src/core/server/models/actions.ts @@ -1,4 +1,5 @@ import { + GQLACTION_GROUP, GQLACTION_ITEM_TYPE, GQLACTION_TYPE, } from "talk-server/graph/tenant/schema/__generated__/types"; @@ -10,7 +11,7 @@ export interface Action { action_type: GQLACTION_TYPE; item_type: GQLACTION_ITEM_TYPE; item_id: string; - group_id?: string; + group_id?: GQLACTION_GROUP; user_id?: string; created_at: Date; metadata?: Record; diff --git a/src/core/server/models/comment.ts b/src/core/server/models/comment.ts index 430f85225..9a961a17a 100644 --- a/src/core/server/models/comment.ts +++ b/src/core/server/models/comment.ts @@ -34,7 +34,7 @@ export interface StatusHistoryItem { export interface Comment extends TenantResource { readonly id: string; - parent_id: string | null; + parent_id?: string; author_id: string; asset_id: string; body: string; diff --git a/src/core/server/models/settings.ts b/src/core/server/models/settings.ts index 217db50b9..9df3eaffe 100644 --- a/src/core/server/models/settings.ts +++ b/src/core/server/models/settings.ts @@ -1,13 +1,11 @@ import { + GQLExternalIntegrations, + GQLKarma, GQLMODERATION_MODE, GQLUSER_ROLE, + GQLWordlist, } from "talk-server/graph/tenant/schema/__generated__/types"; -export interface Wordlist { - banned: string[]; - suspect: string[]; -} - export interface EmailDomainRuleCondition { /** * emailDomain is the domain name component of the email addresses that should @@ -49,7 +47,7 @@ export interface AuthRules { restrictTo?: EmailDomainRuleCondition[]; } -export interface AuthIntegration { +export interface EnableableIntegration { enabled: boolean; } @@ -63,7 +61,7 @@ export interface DisplayNameAuthIntegration { * embed to allow single sign on. */ export interface SSOAuthIntegration - extends AuthIntegration, + extends EnableableIntegration, DisplayNameAuthIntegration { key: string; } @@ -73,7 +71,7 @@ export interface SSOAuthIntegration * will be used in the admin to provide staff logins for users. */ export interface OIDCAuthIntegration - extends AuthIntegration, + extends EnableableIntegration, DisplayNameAuthIntegration { clientID: string; clientSecret: string; @@ -83,17 +81,17 @@ export interface OIDCAuthIntegration tokenURL: string; } -export interface FacebookAuthIntegration extends AuthIntegration { +export interface FacebookAuthIntegration extends EnableableIntegration { clientID: string; clientSecret: string; } -export interface GoogleAuthIntegration extends AuthIntegration { +export interface GoogleAuthIntegration extends EnableableIntegration { clientID: string; clientSecret: string; } -export type LocalAuthIntegration = AuthIntegration; +export type LocalAuthIntegration = EnableableIntegration; /** * AuthIntegrations describes all of the possible auth integration @@ -130,33 +128,6 @@ export interface Auth { integrations: AuthIntegrations; } -/** - * Akismet provides integration with the Akismet Spam detection service. - */ -export interface AkismetIntegration { - /** - * When true, it will enable comments to be checked by Akismet. - */ - enabled: boolean; - - /** - * The key for the Akismet integration. - */ - key?: string; - - /** - * The site (blog) for the Akismet integration. - */ - site?: string; -} - -export interface ExternalIntegrations { - /** - * akismet provides integration with the Akismet Spam detection service. - */ - akismet: AkismetIntegration; -} - export interface ModerationSettings { moderation: GQLMODERATION_MODE; requireEmailConfirmation: boolean; @@ -175,45 +146,6 @@ export interface ModerationSettings { charCount?: number; } -/** - * KarmaThreshold defines the bounds for which a User will become unreliable or - * reliable based on their karma score. If the score is equal or less than the - * unreliable value, they are unreliable. If the score is equal or more than the - * reliable value, they are reliable. If they are neither reliable or unreliable - * then they are neutral. - */ -export interface KarmaThreshold { - reliable: number; - unreliable: number; -} - -export interface KarmaThresholds { - /** - * flag represents karma settings in relation to how well a User's flagging - * ability aligns with the moderation decicions made by moderators. - */ - flag: KarmaThreshold; - - /** - * comment represents the karma setting in relation to how well a User's comments are moderated. - */ - comment: KarmaThreshold; -} - -export interface Karma { - /** - * When true, checks will be completed to ensure that the Karma checks are - * completed. - */ - enabled: boolean; - - /** - * karmaThresholds contains the currently set thresholds for triggering Trust - * beheviour. - */ - thresholds: KarmaThresholds; -} - export interface Settings extends ModerationSettings { customCssUrl?: string; @@ -227,12 +159,12 @@ export interface Settings extends ModerationSettings { * karma is the set of settings related to how user Trust and Karma are * handled. */ - karma: Karma; + karma: GQLKarma; /** * wordlist stores all the banned/suspect words. */ - wordlist: Wordlist; + wordlist: GQLWordlist; /** * Set of configured authentication integrations. @@ -242,5 +174,5 @@ export interface Settings extends ModerationSettings { /** * Various integrations with external services. */ - integrations: ExternalIntegrations; + integrations: GQLExternalIntegrations; } diff --git a/src/core/server/models/tenant.ts b/src/core/server/models/tenant.ts index bd67c99b4..f98b62727 100644 --- a/src/core/server/models/tenant.ts +++ b/src/core/server/models/tenant.ts @@ -91,6 +91,9 @@ export async function createTenant(db: Db, input: CreateTenantInput) { akismet: { enabled: false, }, + perspective: { + enabled: false, + }, }, }; diff --git a/src/core/server/services/comments/index.ts b/src/core/server/services/comments/index.ts index 6bacbd415..1c4531f3e 100644 --- a/src/core/server/services/comments/index.ts +++ b/src/core/server/services/comments/index.ts @@ -14,7 +14,7 @@ import { Request } from "talk-server/types/express"; export type CreateComment = Omit< CreateCommentInput, - "status" | "action_counts" + "status" | "action_counts" | "metadata" >; export async function create( @@ -44,7 +44,7 @@ export async function create( } // Run the comment through the moderation phases. - const { status } = await processForModeration({ + const { status, metadata } = await processForModeration({ asset, tenant, comment: input, @@ -55,9 +55,10 @@ export async function create( // TODO: (wyattjoh) use the actions somehow. const comment = await createComment(mongo, tenant.id, { + ...input, status, action_counts: {}, - ...input, + metadata, }); if (input.parent_id) { diff --git a/src/core/server/services/comments/moderation/index.spec.ts b/src/core/server/services/comments/moderation/index.spec.ts new file mode 100644 index 000000000..4b0ce2ebf --- /dev/null +++ b/src/core/server/services/comments/moderation/index.spec.ts @@ -0,0 +1,105 @@ +import { + GQLACTION_GROUP, + GQLACTION_TYPE, + GQLCOMMENT_STATUS, +} from "talk-server/graph/tenant/schema/__generated__/types"; +import { + compose, + ModerationPhaseContext, +} from "talk-server/services/comments/moderation"; + +describe("compose", () => { + it("handles when a phase throws an error", async () => { + const err = new Error("this is an error"); + const enhanced = compose([ + () => { + throw err; + }, + ]); + + await expect(enhanced({} as ModerationPhaseContext)).rejects.toEqual(err); + }); + + it("handles when it returns a status", async () => { + const status = GQLCOMMENT_STATUS.ACCEPTED; + const enhanced = compose([() => ({ status })]); + + await expect(enhanced({} as ModerationPhaseContext)).resolves.toEqual({ + status, + metadata: {}, + actions: [], + }); + }); + + it("merges the metadata", async () => { + const status = GQLCOMMENT_STATUS.ACCEPTED; + const enhanced = compose([ + () => ({ metadata: { first: true } }), + () => ({ status, metadata: { second: true } }), + () => ({ metadata: { third: true } }), + ]); + + await expect(enhanced({} as ModerationPhaseContext)).resolves.toEqual({ + status, + metadata: { first: true, second: true }, + actions: [], + }); + }); + + it("merges actions", async () => { + const status = GQLCOMMENT_STATUS.ACCEPTED; + + const flags = [ + { + action_type: GQLACTION_TYPE.FLAG, + group_id: GQLACTION_GROUP.TOXIC_COMMENT, + }, + { + action_type: GQLACTION_TYPE.FLAG, + group_id: GQLACTION_GROUP.SPAM_COMMENT, + }, + ]; + + const enhanced = compose([ + () => ({ + actions: [flags[0]], + }), + () => ({ + status, + actions: [flags[1]], + }), + () => ({ + actions: [ + { + action_type: GQLACTION_TYPE.FLAG, + group_id: GQLACTION_GROUP.LINKS, + }, + ], + }), + ]); + + const final = await enhanced({} as ModerationPhaseContext); + + for (const flag of flags) { + expect(final.actions).toContainEqual(flag); + } + + expect(final.actions).not.toContainEqual({ + action_type: GQLACTION_TYPE.FLAG, + group_id: GQLACTION_GROUP.LINKS, + }); + }); + + it("handles when it does not return a status", async () => { + const enhanced = compose([ + () => ({ metadata: { first: true } }), + () => ({ metadata: { second: true } }), + ]); + + await expect(enhanced({} as ModerationPhaseContext)).resolves.toEqual({ + status: GQLCOMMENT_STATUS.NONE, + metadata: { first: true, second: true }, + actions: [], + }); + }); +}); diff --git a/src/core/server/services/comments/moderation/index.ts b/src/core/server/services/comments/moderation/index.ts index cfd7f2c80..33f286320 100644 --- a/src/core/server/services/comments/moderation/index.ts +++ b/src/core/server/services/comments/moderation/index.ts @@ -3,13 +3,13 @@ import { GQLCOMMENT_STATUS } from "talk-server/graph/tenant/schema/__generated__ import { Action } from "talk-server/models/actions"; import { Asset } from "talk-server/models/asset"; import { Tenant } from "talk-server/models/tenant"; -import { CreateComment } from "talk-server/services/comments"; - import { User } from "talk-server/models/user"; +import { CreateComment } from "talk-server/services/comments"; import { Request } from "talk-server/types/express"; + import { moderationPhases } from "./phases"; -// TODO: (wyattjoh) move into actions module. +// TODO: (wyattjoh) move into actions module once we have action methods. export type CreateAction = Omit< Action, "id" | "item_type" | "item_id" | "created_at" @@ -18,6 +18,7 @@ export type CreateAction = Omit< export interface PhaseResult { actions: CreateAction[]; status: GQLCOMMENT_STATUS; + metadata: Record; } export interface ModerationPhaseContext { @@ -42,31 +43,47 @@ export type IntermediateModerationPhase = ( * compose will create a moderation pipeline for which is executable with the * passed actions. */ -const compose = ( +export const compose = ( phases: IntermediateModerationPhase[] ): ModerationPhase => async context => { - const actions: CreateAction[] = []; + const final: PhaseResult = { + status: GQLCOMMENT_STATUS.NONE, + actions: [], + metadata: {}, + }; // Loop over all the moderation phases and see if we've resolved the status. for (const phase of phases) { const result = await phase(context); if (result) { - if (result.actions) { - actions.push(...result.actions); + // If this result contained actions, then we should push it into the + // other actions. + const { actions } = result; + if (actions) { + final.actions.push(...actions); + } + + // If this result contained metadata, then we should merge it into the + // other metadata. + const { metadata } = result; + if (metadata) { + final.metadata = { + ...final.metadata, + ...metadata, + }; } // If this result contained a status, then we've finished resolving // phases! const { status } = result; if (status) { - return { status, actions }; + final.status = status; + break; } } } - // If we didn't determine a different comment from a previous itteration, set - // it to 'NONE'. - return { status: GQLCOMMENT_STATUS.NONE, actions }; + return final; }; /** diff --git a/src/core/server/services/comments/moderation/phases/assetClosed.ts b/src/core/server/services/comments/moderation/phases/assetClosed.ts index 0755949ac..2aec0db0b 100644 --- a/src/core/server/services/comments/moderation/phases/assetClosed.ts +++ b/src/core/server/services/comments/moderation/phases/assetClosed.ts @@ -1,12 +1,15 @@ -import { IntermediateModerationPhase } from "talk-server/services/comments/moderation"; +import { + IntermediateModerationPhase, + IntermediatePhaseResult, +} from "talk-server/services/comments/moderation"; // This phase checks to see if the asset being processed is closed or not. -export const assetClosed: IntermediateModerationPhase = ({ asset }) => { +export const assetClosed: IntermediateModerationPhase = ({ + asset, +}): IntermediatePhaseResult | void => { // Check to see if the asset has closed commenting... if (asset.closedAt && asset.closedAt.valueOf() <= Date.now()) { // TODO: (wyattjoh) return better error. throw new Error("asset is currently closed for commenting"); } - - return; }; diff --git a/src/core/server/services/comments/moderation/phases/commentLength.ts b/src/core/server/services/comments/moderation/phases/commentLength.ts index d714950b2..82a4ada04 100644 --- a/src/core/server/services/comments/moderation/phases/commentLength.ts +++ b/src/core/server/services/comments/moderation/phases/commentLength.ts @@ -1,18 +1,22 @@ import { + GQLACTION_GROUP, GQLACTION_TYPE, GQLCOMMENT_STATUS, } from "talk-server/graph/tenant/schema/__generated__/types"; import { ModerationSettings } from "talk-server/models/settings"; -import { IntermediateModerationPhase } from "talk-server/services/comments/moderation"; +import { + IntermediateModerationPhase, + IntermediatePhaseResult, +} from "talk-server/services/comments/moderation"; const testCharCount = (settings: Partial, length: number) => settings.charCountEnable && settings.charCount && length > settings.charCount; -export const commentLength: IntermediateModerationPhase = async ({ +export const commentLength: IntermediateModerationPhase = ({ asset, tenant, comment, -}) => { +}): IntermediatePhaseResult | void => { const length = comment.body.length; // Check to see if the body is too short, if it is, then complain about it! @@ -32,7 +36,7 @@ export const commentLength: IntermediateModerationPhase = async ({ actions: [ { action_type: GQLACTION_TYPE.FLAG, - group_id: "BODY_COUNT", + group_id: GQLACTION_GROUP.BODY_COUNT, metadata: { count: length, }, @@ -40,6 +44,4 @@ export const commentLength: IntermediateModerationPhase = async ({ ], }; } - - return; }; diff --git a/src/core/server/services/comments/moderation/phases/commentingDisabled.ts b/src/core/server/services/comments/moderation/phases/commentingDisabled.ts index 742d97974..07ad21ecb 100644 --- a/src/core/server/services/comments/moderation/phases/commentingDisabled.ts +++ b/src/core/server/services/comments/moderation/phases/commentingDisabled.ts @@ -1,5 +1,8 @@ import { ModerationSettings } from "talk-server/models/settings"; -import { IntermediateModerationPhase } from "talk-server/services/comments/moderation"; +import { + IntermediateModerationPhase, + IntermediatePhaseResult, +} from "talk-server/services/comments/moderation"; const testDisabledCommenting = (settings: Partial) => settings.disableCommenting; @@ -7,7 +10,7 @@ const testDisabledCommenting = (settings: Partial) => export const commentingDisabled: IntermediateModerationPhase = ({ asset, tenant, -}) => { +}): IntermediatePhaseResult | void => { // Check to see if the asset has closed commenting. if ( testDisabledCommenting(tenant) || diff --git a/src/core/server/services/comments/moderation/phases/index.ts b/src/core/server/services/comments/moderation/phases/index.ts index 6cb656903..a0a724cc6 100644 --- a/src/core/server/services/comments/moderation/phases/index.ts +++ b/src/core/server/services/comments/moderation/phases/index.ts @@ -1,6 +1,7 @@ import { IntermediateModerationPhase } from "talk-server/services/comments/moderation"; import { premod } from "talk-server/services/comments/moderation/phases/premod"; +import { toxic } from "talk-server/services/comments/moderation/phases/toxic"; import { assetClosed } from "./assetClosed"; import { commentingDisabled } from "./commentingDisabled"; import { commentLength } from "./commentLength"; @@ -22,5 +23,6 @@ export const moderationPhases: IntermediateModerationPhase[] = [ links, karma, spam, + toxic, premod, ]; diff --git a/src/core/server/services/comments/moderation/phases/karma.ts b/src/core/server/services/comments/moderation/phases/karma.ts index 3a4d8fa84..f5f4d9e37 100755 --- a/src/core/server/services/comments/moderation/phases/karma.ts +++ b/src/core/server/services/comments/moderation/phases/karma.ts @@ -1,8 +1,12 @@ import { + GQLACTION_GROUP, GQLACTION_TYPE, GQLCOMMENT_STATUS, } from "talk-server/graph/tenant/schema/__generated__/types"; -import { IntermediateModerationPhase } from "talk-server/services/comments/moderation"; +import { + IntermediateModerationPhase, + IntermediatePhaseResult, +} from "talk-server/services/comments/moderation"; import { getCommentTrustScore, isReliableCommenter, @@ -10,7 +14,10 @@ import { // This phase checks to see if the user making the comment is allowed to do so // considering their reliability (Trust) status. -export const karma: IntermediateModerationPhase = ({ tenant, author }) => { +export const karma: IntermediateModerationPhase = ({ + tenant, + author, +}): IntermediatePhaseResult | void => { // If the user is not a reliable commenter (passed the unreliability // threshold by having too many rejected comments) then we can change the // status of the comment to `SYSTEM_WITHHELD`, therefore pushing the user's @@ -27,7 +34,7 @@ export const karma: IntermediateModerationPhase = ({ tenant, author }) => { actions: [ { action_type: GQLACTION_TYPE.FLAG, - group_id: "TRUST", + group_id: GQLACTION_GROUP.TRUST, metadata: { trust: getCommentTrustScore(author), }, @@ -35,6 +42,4 @@ export const karma: IntermediateModerationPhase = ({ tenant, author }) => { ], }; } - - return; }; diff --git a/src/core/server/services/comments/moderation/phases/links.ts b/src/core/server/services/comments/moderation/phases/links.ts index 3db715d5f..18628920a 100755 --- a/src/core/server/services/comments/moderation/phases/links.ts +++ b/src/core/server/services/comments/moderation/phases/links.ts @@ -2,11 +2,15 @@ import linkify from "linkify-it"; import tlds from "tlds"; import { + GQLACTION_GROUP, GQLACTION_TYPE, GQLCOMMENT_STATUS, } from "talk-server/graph/tenant/schema/__generated__/types"; import { ModerationSettings } from "talk-server/models/settings"; -import { IntermediateModerationPhase } from "talk-server/services/comments/moderation"; +import { + IntermediateModerationPhase, + IntermediatePhaseResult, +} from "talk-server/services/comments/moderation"; /** * The preloaded linkify instance with common tlds. @@ -24,8 +28,7 @@ export const links: IntermediateModerationPhase = ({ asset, tenant, comment, - author, -}) => { +}): IntermediatePhaseResult | void => { if ( testPremodLinksEnable(tenant, comment.body) || (asset.settings && testPremodLinksEnable(asset.settings, comment.body)) @@ -36,7 +39,7 @@ export const links: IntermediateModerationPhase = ({ actions: [ { action_type: GQLACTION_TYPE.FLAG, - group_id: "LINKS", + group_id: GQLACTION_GROUP.LINKS, metadata: { links: comment.body, }, @@ -44,6 +47,4 @@ export const links: IntermediateModerationPhase = ({ ], }; } - - return; }; diff --git a/src/core/server/services/comments/moderation/phases/premod.ts b/src/core/server/services/comments/moderation/phases/premod.ts index c12fb582d..7ccdcfb73 100755 --- a/src/core/server/services/comments/moderation/phases/premod.ts +++ b/src/core/server/services/comments/moderation/phases/premod.ts @@ -3,14 +3,20 @@ import { GQLMODERATION_MODE, } from "talk-server/graph/tenant/schema/__generated__/types"; import { ModerationSettings } from "talk-server/models/settings"; -import { IntermediateModerationPhase } from "talk-server/services/comments/moderation"; +import { + IntermediateModerationPhase, + IntermediatePhaseResult, +} from "talk-server/services/comments/moderation"; const testModerationMode = (settings: Partial) => settings.moderation === GQLMODERATION_MODE.PRE; // This phase checks to see if the settings have premod enabled, if they do, // the comment is premod, otherwise, it's just none. -export const premod: IntermediateModerationPhase = ({ asset, tenant }) => { +export const premod: IntermediateModerationPhase = ({ + asset, + tenant, +}): IntermediatePhaseResult | void => { // If the settings say that we're in premod mode, then the comment is in // premod status. @@ -23,6 +29,4 @@ export const premod: IntermediateModerationPhase = ({ asset, tenant }) => { status: GQLCOMMENT_STATUS.PREMOD, }; } - - return; }; diff --git a/src/core/server/services/comments/moderation/phases/spam.ts b/src/core/server/services/comments/moderation/phases/spam.ts index f44a4e85b..d5fc85fc5 100644 --- a/src/core/server/services/comments/moderation/phases/spam.ts +++ b/src/core/server/services/comments/moderation/phases/spam.ts @@ -1,10 +1,15 @@ import { Client } from "akismet-api"; import { + GQLACTION_GROUP, GQLACTION_TYPE, GQLCOMMENT_STATUS, } from "talk-server/graph/tenant/schema/__generated__/types"; -import { IntermediateModerationPhase } from "talk-server/services/comments/moderation"; +import logger from "talk-server/logger"; +import { + IntermediateModerationPhase, + IntermediatePhaseResult, +} from "talk-server/services/comments/moderation"; export const spam: IntermediateModerationPhase = async ({ asset, @@ -12,16 +17,26 @@ export const spam: IntermediateModerationPhase = async ({ comment, author, req, -}) => { +}): Promise => { const integration = tenant.integrations.akismet; // We can only check for spam if this comment originated from a graphql // request via an HTTP call. - if (!req || !integration.enabled) { + if (!req) { + logger.debug({ tenant_id: tenant.id }, "request was not available"); + return; + } + + if (!integration.enabled) { + logger.debug({ tenant_id: tenant.id }, "akismet integration was disabled"); return; } if (!integration.key || !integration.site) { + logger.error( + { tenant_id: tenant.id }, + "akismet integration was enabled but configuration was missing" + ); return; } @@ -34,41 +49,73 @@ export const spam: IntermediateModerationPhase = async ({ // Grab the properties we need. const userIP = req.ip; if (!userIP) { + logger.debug( + { tenant_id: tenant.id }, + "request did not contain ip address, aborting spam check" + ); return; } const userAgent = req.get("User-Agent"); if (!userAgent || userAgent.length === 0) { + logger.debug( + { tenant_id: tenant.id }, + "request did not contain User-Agent header, aborting spam check" + ); return; } const referrer = req.get("Referrer"); if (!referrer || referrer.length === 0) { + logger.debug( + { tenant_id: tenant.id }, + "request did not contain Referrer header, aborting spam check" + ); return; } - // Check the comment for spam. - const isSpam = await client.checkSpam({ - user_ip: userIP, // REQUIRED - referrer, // REQUIRED - user_agent: userAgent, // REQUIRED - comment_content: comment.body, - permalink: asset.url, - comment_author: author.displayName || author.username || "", - comment_type: "comment", - is_test: false, - }); - if (isSpam) { - return { - status: GQLCOMMENT_STATUS.SYSTEM_WITHHELD, - actions: [ - { - action_type: GQLACTION_TYPE.FLAG, - group_id: "SPAM_COMMENT", - }, - ], - }; - } + try { + logger.trace({ tenant_id: tenant.id }, "checking comment for spam"); - return; + // Check the comment for spam. + const isSpam = await client.checkSpam({ + user_ip: userIP, // REQUIRED + referrer, // REQUIRED + user_agent: userAgent, // REQUIRED + comment_content: comment.body, + permalink: asset.url, + comment_author: author.displayName || author.username || "", + comment_type: "comment", + is_test: false, + }); + if (isSpam) { + logger.trace( + { tenant_id: tenant.id, is_spam: isSpam }, + "comment contained spam" + ); + return { + status: GQLCOMMENT_STATUS.SYSTEM_WITHHELD, + actions: [ + { + action_type: GQLACTION_TYPE.FLAG, + group_id: GQLACTION_GROUP.SPAM_COMMENT, + }, + ], + metadata: { + // Store the spam result from Akismet in the Comment metadata. + akismet: spam, + }, + }; + } + + logger.trace( + { tenant_id: tenant.id, is_spam: isSpam }, + "comment did not contain spam" + ); + } catch (err) { + logger.error( + { tenant_id: tenant.id, err }, + "could not determine if comment contained spam" + ); + } }; diff --git a/src/core/server/services/comments/moderation/phases/staff.ts b/src/core/server/services/comments/moderation/phases/staff.ts index 607e49c82..aad2a931d 100755 --- a/src/core/server/services/comments/moderation/phases/staff.ts +++ b/src/core/server/services/comments/moderation/phases/staff.ts @@ -2,7 +2,10 @@ import { GQLCOMMENT_STATUS, GQLUSER_ROLE, } from "talk-server/graph/tenant/schema/__generated__/types"; -import { IntermediateModerationPhase } from "talk-server/services/comments/moderation"; +import { + IntermediateModerationPhase, + IntermediatePhaseResult, +} from "talk-server/services/comments/moderation"; // If a given user is a staff member, always approve their comment. export const staff: IntermediateModerationPhase = ({ @@ -10,12 +13,10 @@ export const staff: IntermediateModerationPhase = ({ tenant, comment, author, -}) => { +}): IntermediatePhaseResult | void => { if (author.role !== GQLUSER_ROLE.COMMENTER) { return { status: GQLCOMMENT_STATUS.ACCEPTED, }; } - - return; }; diff --git a/src/core/server/services/comments/moderation/phases/toxic.ts b/src/core/server/services/comments/moderation/phases/toxic.ts new file mode 100644 index 000000000..1690458f6 --- /dev/null +++ b/src/core/server/services/comments/moderation/phases/toxic.ts @@ -0,0 +1,183 @@ +import { isNil } from "lodash"; +import ms from "ms"; +import fetch from "node-fetch"; + +import { Omit } from "talk-common/types"; +import { + GQLACTION_GROUP, + GQLACTION_TYPE, + GQLCOMMENT_STATUS, + GQLPerspectiveExternalIntegration, +} from "talk-server/graph/tenant/schema/__generated__/types"; +import logger from "talk-server/logger"; +import { + IntermediateModerationPhase, + IntermediatePhaseResult, +} from "talk-server/services/comments/moderation"; + +export const toxic: IntermediateModerationPhase = async ({ + tenant, + comment, +}): Promise => { + const integration = tenant.integrations.perspective; + + if (!integration.enabled) { + // The Toxic comment plugin is not enabled. + logger.debug( + { tenant_id: tenant.id }, + "perspective integration was disabled" + ); + return; + } + + if (!integration.key) { + // The Toxic comment requires a key in order to communicate with the API. + logger.error( + { tenant_id: tenant.id }, + "perspective integration was enabled but configuration was missing" + ); + return; + } + + let endpoint = integration.endpoint; + if (isNil(endpoint)) { + // TODO: (wyattjoh) replace hardcoded default with config. + endpoint = "https://commentanalyzer.googleapis.com/v1alpha1"; + + logger.trace( + { tenant_id: tenant.id, endpoint }, + "endpoint missing in integration settings, using defaults" + ); + } + + let threshold = integration.threshold; + if (isNil(threshold)) { + // TODO: (wyattjoh) replace hardcoded default with config. + threshold = 0.8; + + logger.trace( + { tenant_id: tenant.id, threshold }, + "threshold missing in integration settings, using defaults" + ); + } + + let doNotStore = integration.doNotStore; + if (isNil(doNotStore)) { + doNotStore = true; + + logger.trace( + { tenant_id: tenant.id, do_not_store: doNotStore }, + "doNotStore missing in integration settings, using defaults" + ); + } + + // TODO: (wyattjoh) replace hardcoded default with config. + const timeout = ms("300ms"); + + try { + logger.trace({ tenant_id: tenant.id }, "checking comment toxicity"); + + // Call into the Toxic comment API. + const scores = await getScores( + comment.body, + { + endpoint, + key: integration.key, + doNotStore, + }, + timeout + ); + + const score = scores.SEVERE_TOXICITY.summaryScore; + const isToxic = score > threshold; + if (isToxic) { + logger.trace( + { tenant_id: tenant.id, score, is_toxic: isToxic, threshold }, + "comment was toxic" + ); + return { + status: GQLCOMMENT_STATUS.SYSTEM_WITHHELD, + actions: [ + { + action_type: GQLACTION_TYPE.FLAG, + group_id: GQLACTION_GROUP.TOXIC_COMMENT, + }, + ], + metadata: { + // Store the scores from perspective in the Comment metadata. + perspective: scores, + }, + }; + } + + logger.trace( + { tenant_id: tenant.id, score, is_toxic: isToxic, threshold }, + "comment was not toxic" + ); + } catch (err) { + logger.error( + { tenant_id: tenant.id, err }, + "could not determine comment toxicity" + ); + } +}; + +/** + * getScores will return the toxicity scores for the comment text. + * + * @param text comment text to check for toxicity + * @param settings integration settings used to communicate with the perspective api + * @param timeout timeout for communicating with the perspective api + */ +async function getScores( + text: string, + { + key, + endpoint, + doNotStore, + }: Required>, + timeout: number +) { + try { + const response = await fetch(`${endpoint}/comments:analyze?key=${key}`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + timeout, + body: JSON.stringify({ + comment: { + text, + }, + // TODO: (wyattjoh) support other languages. + languages: ["en"], + doNotStore, + requestedAttributes: { + TOXICITY: {}, + SEVERE_TOXICITY: {}, + }, + }), + }); + + // Grab the data out of the Perspective API. + const data = await response.json(); + + // Reformat the scores. + return { + TOXICITY: { + summaryScore: data.attributeScores.TOXICITY.summaryScore.value, + }, + SEVERE_TOXICITY: { + summaryScore: data.attributeScores.SEVERE_TOXICITY.summaryScore.value, + }, + }; + } catch (err) { + // Ensure that the API key doesn't get leaked to the logs by accident. + if (err.message) { + err.message = err.message.replace(key, "***"); + } + + // Rethrow the error. + throw err; + } +} diff --git a/src/core/server/services/comments/moderation/phases/wordlist.ts b/src/core/server/services/comments/moderation/phases/wordlist.ts index 0c62f3261..a817d3fea 100755 --- a/src/core/server/services/comments/moderation/phases/wordlist.ts +++ b/src/core/server/services/comments/moderation/phases/wordlist.ts @@ -1,17 +1,19 @@ import { + GQLACTION_GROUP, GQLACTION_TYPE, GQLCOMMENT_STATUS, } from "talk-server/graph/tenant/schema/__generated__/types"; -import { IntermediateModerationPhase } from "talk-server/services/comments/moderation"; +import { + IntermediateModerationPhase, + IntermediatePhaseResult, +} from "talk-server/services/comments/moderation"; import { containsMatchingPhrase } from "talk-server/services/comments/moderation/wordlist"; // This phase checks the comment against the wordlist. export const wordlist: IntermediateModerationPhase = ({ - asset, tenant, comment, - author, -}) => { +}): IntermediatePhaseResult | void => { // Decide the status based on whether or not the current asset/settings // has pre-mod enabled or not. If the comment was rejected based on the // wordlist, then reject it, otherwise if the moderation setting is @@ -23,7 +25,7 @@ export const wordlist: IntermediateModerationPhase = ({ actions: [ { action_type: GQLACTION_TYPE.FLAG, - group_id: "BANNED_WORD", + group_id: GQLACTION_GROUP.BANNED_WORD, }, ], }; @@ -40,11 +42,9 @@ export const wordlist: IntermediateModerationPhase = ({ actions: [ { action_type: GQLACTION_TYPE.FLAG, - group_id: "SUSPECT_WORD", + group_id: GQLACTION_GROUP.SUSPECT_WORD, }, ], }; } - - return; }; diff --git a/src/core/server/services/tenant/cache/adapter.ts b/src/core/server/services/tenant/cache/adapter.ts new file mode 100644 index 000000000..feb6269a9 --- /dev/null +++ b/src/core/server/services/tenant/cache/adapter.ts @@ -0,0 +1,92 @@ +import { Config } from "talk-common/config"; +import TenantCache from "talk-server/services/tenant/cache"; + +export type DeconstructionFn = (tenantID: string, value: T) => Promise; + +/** + * TenantCacheAdapter is designed to allow additional services to cache entries + * that are related to tenants that could possibly be cached. When caching of + * tenants are enabled, this acts as a map to store entries, and will + * automatically invalidate tenants that have been updated. + */ +export class TenantCacheAdapter { + private cache = new Map(); + private tenantCache: TenantCache; + private isCachingEnabled: boolean; + + private unsubscribeFn?: () => void; + private deconstructionFn?: DeconstructionFn; + + constructor( + tenantCache: TenantCache, + config: Config, + deconstructionFn?: DeconstructionFn + ) { + this.tenantCache = tenantCache; + this.isCachingEnabled = !config.get("disable_tenant_caching"); + this.deconstructionFn = deconstructionFn; + } + + public subscribe() { + if (this.isCachingEnabled) { + // Unsubscribe from updates if we + this.unsubscribe(); + + this.unsubscribeFn = this.tenantCache.subscribe(async tenant => { + // Get the current set value for the item in the cache. + const value = this.get(tenant.id); + + // Delete the tenant cache item when the tenant changes. + this.cache.delete(tenant.id); + + if (this.deconstructionFn) { + // The deconstruction function is set. We will check that the value + // exists, and if it does, we will call the function with the given + // identifier, this allows the caller to attach deconstruction + // components to the tenant being removed. The only side affect to + // note is that by the time that the deconstruction function is + // called, the tenant has already been purged from the cache. + if (typeof value !== "undefined") { + await this.deconstructionFn(tenant.id, value); + } + } + }); + } + } + + /** + * This will disconnect the map/cache from getting updates. + */ + public unsubscribe() { + if (this.isCachingEnabled && this.unsubscribeFn) { + this.unsubscribeFn(); + } + } + + /** + * get will return the cached entry keyed on the tenantID. + * + * @param tenantID the tenantID for the cached item + */ + public get(tenantID: string): T | undefined { + if (this.isCachingEnabled) { + return this.cache.get(tenantID); + } + + return; + } + + /** + * set will set the cached entry into the map (if caching is enabled). + * + * @param tenantID the tenantID for the cached item + * @param value the value to set in the map (if caching is enabled) + */ + public set(tenantID: string, value: T): TenantCacheAdapter { + if (this.isCachingEnabled) { + this.cache.set(tenantID, value); + } + + return this; + } +} diff --git a/src/core/server/services/tenant/cache.ts b/src/core/server/services/tenant/cache/index.ts similarity index 72% rename from src/core/server/services/tenant/cache.ts rename to src/core/server/services/tenant/cache/index.ts index 58b1a4a1a..9b14ee35a 100644 --- a/src/core/server/services/tenant/cache.ts +++ b/src/core/server/services/tenant/cache/index.ts @@ -4,6 +4,7 @@ import { Db } from "mongodb"; import uuid from "uuid"; import { EventEmitter } from "events"; +import { Config } from "talk-common/config"; import logger from "talk-server/logger"; import { retrieveAllTenants, @@ -29,21 +30,13 @@ export default class TenantCache { /** * tenantsByID reference the tenants that have been cached/retrieved by ID. */ - private tenantsByID = new DataLoader | null>(ids => { - logger.debug({ ids: ids.length }, "now loading tenants"); - return retrieveManyTenants(this.mongo, ids); - }); + private tenantsByID: DataLoader | null>; /** * tenantsByDomain reference the tenants that have been cached/retrieved by * Domain. */ - private tenantsByDomain = new DataLoader | null>( - domains => { - logger.debug({ domains: domains.length }, "now loading tenants"); - return retrieveManyTenantsByDomain(this.mongo, domains); - } - ); + private tenantsByDomain: DataLoader | null>; /** * Create a new client application ID. This prevents duplicated messages @@ -54,23 +47,70 @@ export default class TenantCache { private mongo: Db; private emitter = new EventEmitter(); + private cachingEnabled: boolean; + + constructor(mongo: Db, subscriber: Redis, config: Config) { + this.cachingEnabled = !config.get("disable_tenant_caching"); + if (!this.cachingEnabled) { + logger.warn("tenant caching is disabled"); + } else { + logger.debug("tenant caching is enabled"); + } - constructor(mongo: Db, subscriber: Redis) { // Save the Db reference. this.mongo = mongo; - // Attach to messages on this connection so we can receive updates when - // the tenant are changed. - subscriber.on("message", this.onMessage); + // Configure the data loaders. + this.tenantsByID = new DataLoader( + async ids => { + logger.debug({ ids: ids.length }, "now loading tenants"); + const tenants = await retrieveManyTenants(this.mongo, ids); + logger.debug( + { tenants: tenants.filter(t => t !== null).length }, + "loaded tenants" + ); + return tenants; + }, + { + cache: this.cachingEnabled, + } + ); - // Subscribe to tenant notifications. - subscriber.subscribe(TENANT_UPDATE_CHANNEL); + this.tenantsByDomain = new DataLoader( + async domains => { + logger.debug({ domains: domains.length }, "now loading tenants"); + const tenants = await retrieveManyTenantsByDomain(this.mongo, domains); + logger.debug( + { tenants: tenants.filter(t => t !== null).length }, + "loaded tenants" + ); + return tenants; + }, + { + cache: this.cachingEnabled, + } + ); + + // We don't need updates if we aren't synced to tenant updates. + if (this.cachingEnabled) { + // Attach to messages on this connection so we can receive updates when + // the tenant are changed. + subscriber.on("message", this.onMessage); + + // Subscribe to tenant notifications. + subscriber.subscribe(TENANT_UPDATE_CHANNEL); + } } /** * primeAll will load all the tenants into the cache on startup. */ public async primeAll() { + if (!this.cachingEnabled) { + logger.debug("tenants not primed, caching disabled"); + return; + } + // Grab all the tenants for this node. const tenants = await retrieveAllTenants(this.mongo); diff --git a/src/core/server/services/users/karma.ts b/src/core/server/services/users/karma.ts index 4a17538b0..387c44125 100644 --- a/src/core/server/services/users/karma.ts +++ b/src/core/server/services/users/karma.ts @@ -1,13 +1,13 @@ import { get } from "lodash"; -import { KarmaThresholds } from "talk-server/models/settings"; +import { GQLKarmaThresholds } from "talk-server/graph/tenant/schema/__generated__/types"; import { User } from "talk-server/models/user"; export const getCommentTrustScore = (user: User): number => get(user, "metadata.trust.comment.karma", 0); export const isReliableCommenter = ( - thresholds: KarmaThresholds, + thresholds: GQLKarmaThresholds, user: User ): boolean | null => { const score = getCommentTrustScore(user); From 7c5ff1bcb121e7179177ea183ed48cb97bea4009 Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Tue, 14 Aug 2018 13:11:35 -0300 Subject: [PATCH 08/27] Able to Sign In --- src/core/client/stream/components/Stream.tsx | 5 +++- .../components/UserBoxAuthenticated.tsx | 4 ++- .../stream/containers/StreamContainer.tsx | 3 ++ .../stream/containers/UserBoxContainer.tsx | 28 +++++++++++++++++-- .../client/stream/queries/StreamQuery.tsx | 8 +++++- 5 files changed, 42 insertions(+), 6 deletions(-) diff --git a/src/core/client/stream/components/Stream.tsx b/src/core/client/stream/components/Stream.tsx index 7f1ea183c..f171acb86 100644 --- a/src/core/client/stream/components/Stream.tsx +++ b/src/core/client/stream/components/Stream.tsx @@ -10,6 +10,8 @@ import ReplyListContainer from "../containers/ReplyListContainer"; import UserBoxContainer from "../containers/UserBoxContainer"; import * as styles from "./Stream.css"; +import { User } from "../containers/UserBoxContainer"; + export interface StreamProps { assetID: string; isClosed?: boolean; @@ -17,12 +19,13 @@ export interface StreamProps { onLoadMore?: () => void; hasMore?: boolean; disableLoadMore?: boolean; + user: User | null; } const Stream: StatelessComponent = props => { return ( - + void; + user: User; } const UserBoxAuthenticated: StatelessComponent< @@ -23,7 +25,7 @@ const UserBoxAuthenticated: StatelessComponent< - okbel + {props.user.username} diff --git a/src/core/client/stream/containers/StreamContainer.tsx b/src/core/client/stream/containers/StreamContainer.tsx index d81319deb..1a185c902 100644 --- a/src/core/client/stream/containers/StreamContainer.tsx +++ b/src/core/client/stream/containers/StreamContainer.tsx @@ -10,9 +10,11 @@ import { } from "talk-stream/__generated__/StreamContainerPaginationQuery.graphql"; import Stream from "../components/Stream"; +import { User } from "../containers/UserBoxContainer"; interface InnerProps { asset: Data; + user: User | null; relay: RelayPaginationProp; } @@ -31,6 +33,7 @@ export class StreamContainer extends React.Component { onLoadMore={this.loadMore} hasMore={this.props.relay.hasMore()} disableLoadMore={this.state.disableLoadMore} + user={this.props.user} /> ); } diff --git a/src/core/client/stream/containers/UserBoxContainer.tsx b/src/core/client/stream/containers/UserBoxContainer.tsx index a1f2bb87b..b9896e97b 100644 --- a/src/core/client/stream/containers/UserBoxContainer.tsx +++ b/src/core/client/stream/containers/UserBoxContainer.tsx @@ -1,6 +1,5 @@ import * as React from "react"; import { Component } from "react"; - import { graphql, withLocalStateContainer } from "talk-framework/lib/relay"; import { UserBoxContainerLocal as Local } from "talk-stream/__generated__/UserBoxContainerLocal.graphql"; import { @@ -11,11 +10,26 @@ import { } from "talk-stream/mutations"; import { Popup } from "talk-ui/components"; -// import UserBoxAuthenticated from "../components/UserBoxAuthenticated"; +import UserBoxAuthenticated from "../components/UserBoxAuthenticated"; import UserBoxUnauthenticated from "../components/UserBoxUnauthenticated"; +export type USER_ROLE = + | "ADMIN" + | "COMMENTER" + | "MODERATOR" + | "STAFF" + | "%future added value"; + +export interface User { + id?: string; + username?: string | null; + displayName?: string | null; + role?: USER_ROLE; +} + interface InnerProps { local: Local; + user: User | null; showAuthPopup: ShowAuthPopupMutation; setAuthPopupState: SetAuthPopupStateMutation; } @@ -32,7 +46,15 @@ export class UserBoxContainer extends Component { local: { authPopup: { open, focus, view }, }, + user, } = this.props; + + if (user) { + return ( + + ); + } + return ( <> { onBlur={this.handleBlur} onClose={this.handleClose} /> - {/* */} ) => { return
{error.message}
; } if (props) { - return ; + return ; } return
Loading
; }; @@ -38,6 +38,12 @@ const StreamQuery: StatelessComponent = ({ asset(id: $assetID) { ...StreamContainer_asset } + me { + id + username + displayName + role + } } `} variables={{ From 8e9ac0748e98efc0d7f0b9c1ff467e9fe6112e0a Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Wed, 15 Aug 2018 12:05:38 -0300 Subject: [PATCH 09/27] Working signin and comment --- src/core/client/stream/components/Stream.tsx | 5 ++++- .../client/stream/containers/PostCommentFormContainer.tsx | 8 +++++++- src/core/client/stream/queries/StreamQuery.tsx | 2 ++ 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/core/client/stream/components/Stream.tsx b/src/core/client/stream/components/Stream.tsx index f171acb86..3d113df44 100644 --- a/src/core/client/stream/components/Stream.tsx +++ b/src/core/client/stream/components/Stream.tsx @@ -26,7 +26,10 @@ const Stream: StatelessComponent = props => { return ( - + { @@ -31,7 +32,12 @@ class PostCommentFormContainer extends Component { return undefined; }; public render() { - return ; + return ( + + ); } } diff --git a/src/core/client/stream/queries/StreamQuery.tsx b/src/core/client/stream/queries/StreamQuery.tsx index f5df60cbe..f6ad272bb 100644 --- a/src/core/client/stream/queries/StreamQuery.tsx +++ b/src/core/client/stream/queries/StreamQuery.tsx @@ -23,9 +23,11 @@ export const render = ({ error, props }: ReadyState) => { if (error) { return
{error.message}
; } + if (props) { return ; } + return
Loading
; }; From 551ce1521c24b879aca51aac0d9304aca96c57f7 Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Wed, 15 Aug 2018 13:12:19 -0300 Subject: [PATCH 10/27] Progress --- .../client/auth/mutations/SignOffMutation.ts | 14 +++++++++ src/core/client/auth/mutations/index.ts | 1 + src/core/client/framework/lib/rest.ts | 2 +- src/core/client/framework/rest/index.ts | 1 + src/core/client/framework/rest/signOff.ts | 7 +++++ .../components/UserBoxAuthenticated.tsx | 2 +- .../components/UserBoxUnauthenticated.tsx | 1 + .../UserBoxAuthenticatedContainer.tsx | 29 +++++++++++++++++++ .../stream/containers/UserBoxContainer.tsx | 4 +-- 9 files changed, 57 insertions(+), 4 deletions(-) create mode 100644 src/core/client/auth/mutations/SignOffMutation.ts create mode 100644 src/core/client/framework/rest/signOff.ts create mode 100644 src/core/client/stream/containers/UserBoxAuthenticatedContainer.tsx diff --git a/src/core/client/auth/mutations/SignOffMutation.ts b/src/core/client/auth/mutations/SignOffMutation.ts new file mode 100644 index 000000000..68ab26d0d --- /dev/null +++ b/src/core/client/auth/mutations/SignOffMutation.ts @@ -0,0 +1,14 @@ +import { Environment } from "relay-runtime"; + +import { TalkContext } from "talk-framework/lib/bootstrap"; +import { createMutationContainer } from "talk-framework/lib/relay"; +import { signOff } from "talk-framework/rest"; + +export async function commit( + environment: Environment, + { rest, postMessage }: TalkContext +) { + signOff(rest); +} + +export const withSignOffMutation = createMutationContainer("signOff", commit); diff --git a/src/core/client/auth/mutations/index.ts b/src/core/client/auth/mutations/index.ts index 46e02df95..f6fe1b2f7 100644 --- a/src/core/client/auth/mutations/index.ts +++ b/src/core/client/auth/mutations/index.ts @@ -1,3 +1,4 @@ export { withSetViewMutation, SetViewMutation } from "./SetViewMutation"; export { withSignInMutation, SignInMutation } from "./SignInMutation"; export { withSignUpMutation, SignUpMutation } from "./SignUpMutation"; +export { withSignOffMutation } from "./SignOffMutation"; diff --git a/src/core/client/framework/lib/rest.ts b/src/core/client/framework/lib/rest.ts index 6ba52b958..790465798 100644 --- a/src/core/client/framework/lib/rest.ts +++ b/src/core/client/framework/lib/rest.ts @@ -32,7 +32,7 @@ const handleResp = (res: Response) => { } }; -type PartialRequestInit = Overwrite, { body: any }>; +type PartialRequestInit = Overwrite, { body?: any }>; export class RestClient { public readonly uri: string; diff --git a/src/core/client/framework/rest/index.ts b/src/core/client/framework/rest/index.ts index 0b3032bb5..7216d255f 100644 --- a/src/core/client/framework/rest/index.ts +++ b/src/core/client/framework/rest/index.ts @@ -1,2 +1,3 @@ export { default as signIn, SignInInput } from "./signIn"; export { default as signUp, SignUpInput } from "./signUp"; +export { default as signOff } from "./signOff"; diff --git a/src/core/client/framework/rest/signOff.ts b/src/core/client/framework/rest/signOff.ts new file mode 100644 index 000000000..5a1904d7c --- /dev/null +++ b/src/core/client/framework/rest/signOff.ts @@ -0,0 +1,7 @@ +import { RestClient } from "../lib/rest"; + +export default function signOff(rest: RestClient) { + return rest.fetch("/tenant/auth/local", { + method: "DELETE", + }); +} diff --git a/src/core/client/stream/components/UserBoxAuthenticated.tsx b/src/core/client/stream/components/UserBoxAuthenticated.tsx index daa8fcb35..27e6e4965 100644 --- a/src/core/client/stream/components/UserBoxAuthenticated.tsx +++ b/src/core/client/stream/components/UserBoxAuthenticated.tsx @@ -16,7 +16,7 @@ const UserBoxAuthenticated: StatelessComponent< UserBoxUnauthenticatedProps > = props => { return ( - + diff --git a/src/core/client/stream/components/UserBoxUnauthenticated.tsx b/src/core/client/stream/components/UserBoxUnauthenticated.tsx index f1a8716ee..0a81149bc 100644 --- a/src/core/client/stream/components/UserBoxUnauthenticated.tsx +++ b/src/core/client/stream/components/UserBoxUnauthenticated.tsx @@ -9,6 +9,7 @@ import * as styles from "./UserBoxUnauthenticated.css"; export interface UserBoxUnauthenticatedProps { onSignIn: () => void; onRegister: () => void; + onSignOff: () => void; } const UserBoxUnauthenticated: StatelessComponent< diff --git a/src/core/client/stream/containers/UserBoxAuthenticatedContainer.tsx b/src/core/client/stream/containers/UserBoxAuthenticatedContainer.tsx new file mode 100644 index 000000000..f1de51853 --- /dev/null +++ b/src/core/client/stream/containers/UserBoxAuthenticatedContainer.tsx @@ -0,0 +1,29 @@ +import React, { Component } from "react"; +import UserBoxUnauthenticated from "talk-stream/components/UserBoxUnauthenticated"; +import { withSignOffMutation } from "../../auth/mutations"; + +interface SignInContainerProps { + onSignIn: () => void; + onRegister: () => void; + signOff: () => void; +} + +export type View = "SIGN_UP" | "FORGOT_PASSWORD"; + +class UserBoxUnauthenticatedContainer extends Component { + private signOff = () => { + this.props.signOff(); + }; + public render() { + return ( + + ); + } +} + +const enhanced = withSignOffMutation(UserBoxUnauthenticatedContainer); +export default enhanced; diff --git a/src/core/client/stream/containers/UserBoxContainer.tsx b/src/core/client/stream/containers/UserBoxContainer.tsx index b9896e97b..68845a3e3 100644 --- a/src/core/client/stream/containers/UserBoxContainer.tsx +++ b/src/core/client/stream/containers/UserBoxContainer.tsx @@ -11,7 +11,7 @@ import { import { Popup } from "talk-ui/components"; import UserBoxAuthenticated from "../components/UserBoxAuthenticated"; -import UserBoxUnauthenticated from "../components/UserBoxUnauthenticated"; +import UserBoxAuthenticatedContainer from "../containers/UserBoxAuthenticatedContainer"; export type USER_ROLE = | "ADMIN" @@ -67,7 +67,7 @@ export class UserBoxContainer extends Component { onBlur={this.handleBlur} onClose={this.handleClose} /> - From e704c6d96833858f84100d572faebcbc774f62d7 Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Thu, 16 Aug 2018 12:02:09 -0300 Subject: [PATCH 11/27] SignOff Mutation --- .../client/auth/mutations/SignOffMutation.ts | 14 +++++++-- src/core/client/auth/mutations/index.ts | 2 +- src/core/client/framework/rest/index.ts | 2 +- src/core/client/framework/rest/signOff.ts | 10 +++++-- .../components/UserBoxAuthenticated.tsx | 8 ++--- .../components/UserBoxUnauthenticated.tsx | 1 - .../UserBoxAuthenticatedContainer.tsx | 30 ++++++++----------- .../stream/containers/UserBoxContainer.tsx | 8 ++--- 8 files changed, 42 insertions(+), 33 deletions(-) diff --git a/src/core/client/auth/mutations/SignOffMutation.ts b/src/core/client/auth/mutations/SignOffMutation.ts index 68ab26d0d..b926960df 100644 --- a/src/core/client/auth/mutations/SignOffMutation.ts +++ b/src/core/client/auth/mutations/SignOffMutation.ts @@ -2,13 +2,23 @@ import { Environment } from "relay-runtime"; import { TalkContext } from "talk-framework/lib/bootstrap"; import { createMutationContainer } from "talk-framework/lib/relay"; -import { signOff } from "talk-framework/rest"; +import { signOff, SignOffInput } from "talk-framework/rest"; + +export type SignOffMutation = (input: SignOffInput) => Promise; export async function commit( environment: Environment, + input: SignOffInput, { rest, postMessage }: TalkContext ) { - signOff(rest); + try { + await signOff(rest, input); + console.log("signing off"); + // postMessage.send("setAuthToken", result.token, window.opener); + // window.close(); + } catch (err) { + // postMessage.send("authError", err.toString(), window.opener); + } } export const withSignOffMutation = createMutationContainer("signOff", commit); diff --git a/src/core/client/auth/mutations/index.ts b/src/core/client/auth/mutations/index.ts index f6fe1b2f7..e83f7aabf 100644 --- a/src/core/client/auth/mutations/index.ts +++ b/src/core/client/auth/mutations/index.ts @@ -1,4 +1,4 @@ export { withSetViewMutation, SetViewMutation } from "./SetViewMutation"; export { withSignInMutation, SignInMutation } from "./SignInMutation"; export { withSignUpMutation, SignUpMutation } from "./SignUpMutation"; -export { withSignOffMutation } from "./SignOffMutation"; +export { withSignOffMutation, SignOffMutation } from "./SignOffMutation"; diff --git a/src/core/client/framework/rest/index.ts b/src/core/client/framework/rest/index.ts index 7216d255f..97437ed10 100644 --- a/src/core/client/framework/rest/index.ts +++ b/src/core/client/framework/rest/index.ts @@ -1,3 +1,3 @@ export { default as signIn, SignInInput } from "./signIn"; export { default as signUp, SignUpInput } from "./signUp"; -export { default as signOff } from "./signOff"; +export { default as signOff, SignOffInput } from "./signOff"; diff --git a/src/core/client/framework/rest/signOff.ts b/src/core/client/framework/rest/signOff.ts index 5a1904d7c..6072f3ba9 100644 --- a/src/core/client/framework/rest/signOff.ts +++ b/src/core/client/framework/rest/signOff.ts @@ -1,7 +1,13 @@ import { RestClient } from "../lib/rest"; -export default function signOff(rest: RestClient) { - return rest.fetch("/tenant/auth/local", { +// tslint:disable-next-line:no-empty-interface +export interface SignOffInput {} + +// tslint:disable-next-line:no-empty-interface +export interface SignOffResponse {} + +export default function signOff(rest: RestClient, input: SignOffInput) { + return rest.fetch("/tenant/auth/local", { method: "DELETE", }); } diff --git a/src/core/client/stream/components/UserBoxAuthenticated.tsx b/src/core/client/stream/components/UserBoxAuthenticated.tsx index 27e6e4965..dc58ebafa 100644 --- a/src/core/client/stream/components/UserBoxAuthenticated.tsx +++ b/src/core/client/stream/components/UserBoxAuthenticated.tsx @@ -7,13 +7,13 @@ import MatchMedia from "talk-ui/components/MatchMedia"; import { User } from "../containers/UserBoxContainer"; import * as styles from "./UserBoxAuthenticated.css"; -export interface UserBoxUnauthenticatedProps { - onSignOut: () => void; +export interface UserBoxAuthenticatedProps { + onSignOff: () => void; user: User; } const UserBoxAuthenticated: StatelessComponent< - UserBoxUnauthenticatedProps + UserBoxAuthenticatedProps > = props => { return ( @@ -40,7 +40,7 @@ const UserBoxAuthenticated: StatelessComponent< color="primary" size="small" variant="underlined" - onClick={props.onSignOut} + onClick={props.onSignOff} > Sign Out diff --git a/src/core/client/stream/components/UserBoxUnauthenticated.tsx b/src/core/client/stream/components/UserBoxUnauthenticated.tsx index 0a81149bc..f1a8716ee 100644 --- a/src/core/client/stream/components/UserBoxUnauthenticated.tsx +++ b/src/core/client/stream/components/UserBoxUnauthenticated.tsx @@ -9,7 +9,6 @@ import * as styles from "./UserBoxUnauthenticated.css"; export interface UserBoxUnauthenticatedProps { onSignIn: () => void; onRegister: () => void; - onSignOff: () => void; } const UserBoxUnauthenticated: StatelessComponent< diff --git a/src/core/client/stream/containers/UserBoxAuthenticatedContainer.tsx b/src/core/client/stream/containers/UserBoxAuthenticatedContainer.tsx index f1de51853..54b36ecbf 100644 --- a/src/core/client/stream/containers/UserBoxAuthenticatedContainer.tsx +++ b/src/core/client/stream/containers/UserBoxAuthenticatedContainer.tsx @@ -1,29 +1,25 @@ import React, { Component } from "react"; -import UserBoxUnauthenticated from "talk-stream/components/UserBoxUnauthenticated"; -import { withSignOffMutation } from "../../auth/mutations"; +import UserBoxAuthenticated from "talk-stream/components/UserBoxAuthenticated"; +import { SignOffMutation, withSignOffMutation } from "../../auth/mutations"; +import { User } from "../containers/UserBoxContainer"; -interface SignInContainerProps { - onSignIn: () => void; - onRegister: () => void; - signOff: () => void; +interface UserBoxAuthenticatedProps { + signOff: SignOffMutation; + user: User; } -export type View = "SIGN_UP" | "FORGOT_PASSWORD"; - -class UserBoxUnauthenticatedContainer extends Component { - private signOff = () => { - this.props.signOff(); +class UserBoxAuthenticatedContainer extends Component< + UserBoxAuthenticatedProps +> { + private onSignOff = () => { + this.props.signOff({}); }; public render() { return ( - + ); } } -const enhanced = withSignOffMutation(UserBoxUnauthenticatedContainer); +const enhanced = withSignOffMutation(UserBoxAuthenticatedContainer); export default enhanced; diff --git a/src/core/client/stream/containers/UserBoxContainer.tsx b/src/core/client/stream/containers/UserBoxContainer.tsx index 68845a3e3..0a9d72561 100644 --- a/src/core/client/stream/containers/UserBoxContainer.tsx +++ b/src/core/client/stream/containers/UserBoxContainer.tsx @@ -10,7 +10,7 @@ import { } from "talk-stream/mutations"; import { Popup } from "talk-ui/components"; -import UserBoxAuthenticated from "../components/UserBoxAuthenticated"; +import UserBoxUnauthenticated from "talk-stream/components/UserBoxUnauthenticated"; import UserBoxAuthenticatedContainer from "../containers/UserBoxAuthenticatedContainer"; export type USER_ROLE = @@ -50,9 +50,7 @@ export class UserBoxContainer extends Component { } = this.props; if (user) { - return ( - - ); + return ; } return ( @@ -67,7 +65,7 @@ export class UserBoxContainer extends Component { onBlur={this.handleBlur} onClose={this.handleClose} /> - From a48bcc3076795d6787edd0fa27e0cc703781062e Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Thu, 16 Aug 2018 12:20:56 -0300 Subject: [PATCH 12/27] Hitting the delete route and deleting the token --- src/core/client/auth/mutations/SignOffMutation.ts | 14 +++++--------- src/core/client/framework/lib/rest.ts | 13 +++++++------ src/core/server/app/router.ts | 5 +++++ 3 files changed, 17 insertions(+), 15 deletions(-) diff --git a/src/core/client/auth/mutations/SignOffMutation.ts b/src/core/client/auth/mutations/SignOffMutation.ts index b926960df..dcaadfd8f 100644 --- a/src/core/client/auth/mutations/SignOffMutation.ts +++ b/src/core/client/auth/mutations/SignOffMutation.ts @@ -9,16 +9,12 @@ export type SignOffMutation = (input: SignOffInput) => Promise; export async function commit( environment: Environment, input: SignOffInput, - { rest, postMessage }: TalkContext + { rest, localStorage }: TalkContext ) { - try { - await signOff(rest, input); - console.log("signing off"); - // postMessage.send("setAuthToken", result.token, window.opener); - // window.close(); - } catch (err) { - // postMessage.send("authError", err.toString(), window.opener); - } + await signOff(rest, input); + // tslint:disable-next-line:no-console + console.log("Signing off"); + localStorage.removeItem("authToken"); } export const withSignOffMutation = createMutationContainer("signOff", commit); diff --git a/src/core/client/framework/lib/rest.ts b/src/core/client/framework/lib/rest.ts index 790465798..9548febea 100644 --- a/src/core/client/framework/lib/rest.ts +++ b/src/core/client/framework/lib/rest.ts @@ -19,12 +19,13 @@ const buildOptions = (inputOptions: RequestInit = {}) => { const handleResp = (res: Response) => { if (res.status > 399) { - return res.json().then((err: any) => { - // TODO: sync error handling with server. - const message = err.message || err.error || res.status; - const error = new Error(message); - throw error; - }); + return res.text(); + // TODO (bc): sync error handling with server. + // return res.json().then((err: any) => { + // const message = err.message || err.error || res.status; + // const error = new Error(message); + // throw error; + // }); } else if (res.status === 204) { return res.text(); } else { diff --git a/src/core/server/app/router.ts b/src/core/server/app/router.ts index 6c06f8848..75b195ab3 100644 --- a/src/core/server/app/router.ts +++ b/src/core/server/app/router.ts @@ -69,11 +69,16 @@ function createNewAuthRouter(app: AppOptions, options: RouterOptions) { express.json(), wrapAuthn(options.passport, app.signingConfig, "local") ); + + // TODO (bc) - add delete user serverside + router.delete("/local", express.json()); + router.post( "/local/signup", express.json(), signupHandler({ db: app.mongo, signingConfig: app.signingConfig }) ); + router.post("/sso", wrapAuthn(options.passport, app.signingConfig, "sso")); router.get("/oidc", wrapAuthn(options.passport, app.signingConfig, "oidc")); router.get( From ecb82cf9a914a980fcecf015b6b687106f6e4d73 Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Thu, 16 Aug 2018 12:41:21 -0300 Subject: [PATCH 13/27] Working type submit for forms - Button Component --- src/core/client/auth/components/SignIn.tsx | 8 +++++++- .../client/ui/components/BaseButton/BaseButton.tsx | 12 +++++++++--- src/core/client/ui/components/Button/Button.tsx | 6 ++++++ 3 files changed, 22 insertions(+), 4 deletions(-) diff --git a/src/core/client/auth/components/SignIn.tsx b/src/core/client/auth/components/SignIn.tsx index f0e88fdd4..5785ea2e9 100644 --- a/src/core/client/auth/components/SignIn.tsx +++ b/src/core/client/auth/components/SignIn.tsx @@ -99,7 +99,13 @@ const SignIn: StatelessComponent = props => {
- { /** Internal: Forwarded Ref */ forwardRef?: Ref; + + type?: "submit" | "reset" | "button"; } /** @@ -45,15 +47,17 @@ const BaseButton: StatelessComponent = ({ keyboardFocus, mouseHover, forwardRef, - type: typeProp, + type, ...rest }) => { + console.log("type", type, "Basebutton"); + let Element = "button"; + if (anchor) { Element = "a"; } - let type = typeProp; if (anchor && type) { // tslint:disable:next-line: no-console console.warn( @@ -69,7 +73,9 @@ const BaseButton: StatelessComponent = ({ [classes.mouseHover]: mouseHover, }); - return ; + return ( + + ); }; const enhanced = withForwardRef( diff --git a/src/core/client/ui/components/Button/Button.tsx b/src/core/client/ui/components/Button/Button.tsx index c984c2c5e..e466049dd 100644 --- a/src/core/client/ui/components/Button/Button.tsx +++ b/src/core/client/ui/components/Button/Button.tsx @@ -36,6 +36,8 @@ interface InnerProps extends ButtonHTMLAttributes { /** If set renders active state e.g. to implement toggle buttons */ active?: boolean; + type?: "submit" | "reset" | "button"; + /** Internal: Forwarded Ref */ forwardRef?: Ref; } @@ -57,9 +59,12 @@ export class Button extends React.Component { disabled, forwardRef, variant, + type, ...rest } = this.props; + console.log("type", type, "---------"); + const rootClassName = cn(classes.root, className, { [classes.sizeRegular]: size === "regular", [classes.sizeSmall]: size === "small", @@ -84,6 +89,7 @@ export class Button extends React.Component { classes={pick(classes, "keyboardFocus", "mouseHover")} disabled={disabled} forwardRef={forwardRef} + type={type} {...rest} /> ); From e4ebb2553fa737945dffde044c97e8f2b9db36ab Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Thu, 16 Aug 2018 13:14:47 -0300 Subject: [PATCH 14/27] Handling error messages --- src/core/client/auth/components/SignIn.tsx | 3 ++ src/core/client/auth/components/SignUp.tsx | 11 ++++++- .../auth/containers/SignInContainer.tsx | 25 +++++++++++++--- .../auth/containers/SignUpContainer.tsx | 23 +++++++++++++-- src/core/client/framework/lib/rest.ts | 29 ++++++++++--------- .../ui/components/BaseButton/BaseButton.tsx | 2 -- .../client/ui/components/Button/Button.tsx | 2 -- 7 files changed, 69 insertions(+), 26 deletions(-) diff --git a/src/core/client/auth/components/SignIn.tsx b/src/core/client/auth/components/SignIn.tsx index 5785ea2e9..1934d03a8 100644 --- a/src/core/client/auth/components/SignIn.tsx +++ b/src/core/client/auth/components/SignIn.tsx @@ -9,6 +9,7 @@ import { } from "talk-framework/lib/validation"; import { Button, + CallOut, Flex, FormField, InputLabel, @@ -27,6 +28,7 @@ interface FormProps { export interface SignInForm { onSubmit: OnSubmit; setView: (view: View) => void; + errorMessage: string; } const SignIn: StatelessComponent = props => { @@ -34,6 +36,7 @@ const SignIn: StatelessComponent = props => {
{({ handleSubmit, submitting }) => ( + {props.errorMessage && {props.errorMessage}} Sign in to join the conversation diff --git a/src/core/client/auth/components/SignUp.tsx b/src/core/client/auth/components/SignUp.tsx index 5e4b913e6..071d1affc 100644 --- a/src/core/client/auth/components/SignUp.tsx +++ b/src/core/client/auth/components/SignUp.tsx @@ -12,6 +12,7 @@ import { } from "talk-framework/lib/validation"; import { Button, + CallOut, Flex, FormField, InputDescription, @@ -33,11 +34,13 @@ interface FormProps { export interface SignUpForm { onSubmit: OnSubmit; setView: (view: View) => void; + errorMessage: string; } const SignUp: StatelessComponent = props => { return ( + {props.errorMessage && {props.errorMessage}} {({ handleSubmit, submitting }) => ( @@ -155,7 +158,13 @@ const SignUp: StatelessComponent = props => {
- { +class SignInContainer extends Component< + SignInContainerProps, + SignUpContainerState +> { + public state = { errorMessage: "" }; private setView = (view: View) => { this.props.setView({ view, @@ -23,19 +31,28 @@ class SignInContainer extends Component { }; private onSubmit: SignInForm["onSubmit"] = async (input, form) => { try { - await this.props.signIn(input); - form.reset(); + const res = await this.props.signIn(input); + console.log(res); + // form.reset(); } catch (error) { if (error instanceof BadUserInputError) { return error.invalidArgsLocalized; } + console.log(error); + this.setState({ errorMessage: `Error: ${error}` }); // tslint:disable-next-line:no-console console.error(error); } return undefined; }; public render() { - return ; + return ( + + ); } } diff --git a/src/core/client/auth/containers/SignUpContainer.tsx b/src/core/client/auth/containers/SignUpContainer.tsx index 6cbe4bf02..299bc5c91 100644 --- a/src/core/client/auth/containers/SignUpContainer.tsx +++ b/src/core/client/auth/containers/SignUpContainer.tsx @@ -14,9 +14,17 @@ interface SignUpContainerProps { setView: SetViewMutation; } +interface SignUpContainerState { + errorMessage: string; +} + export type View = "SIGN_IN"; -class SignUpContainer extends Component { +class SignUpContainer extends Component< + SignUpContainerProps, + SignUpContainerState +> { + public state = { errorMessage: "" }; private setView = (view: View) => { this.props.setView({ view, @@ -24,19 +32,28 @@ class SignUpContainer extends Component { }; private onSubmit: SignUpForm["onSubmit"] = async (input, form) => { try { - await this.props.signUp(input); + const res = await this.props.signUp(input); + console.log("response", res); form.reset(); } catch (error) { + console.log("error", error); if (error instanceof BadUserInputError) { return error.invalidArgsLocalized; } + this.setState({ errorMessage: `Something ${error}` }); // tslint:disable-next-line:no-console console.error(error); } return undefined; }; public render() { - return ; + return ( + + ); } } diff --git a/src/core/client/framework/lib/rest.ts b/src/core/client/framework/lib/rest.ts index 9548febea..975c7e282 100644 --- a/src/core/client/framework/lib/rest.ts +++ b/src/core/client/framework/lib/rest.ts @@ -17,20 +17,17 @@ const buildOptions = (inputOptions: RequestInit = {}) => { return options; }; -const handleResp = (res: Response) => { - if (res.status > 399) { - return res.text(); - // TODO (bc): sync error handling with server. - // return res.json().then((err: any) => { - // const message = err.message || err.error || res.status; - // const error = new Error(message); - // throw error; - // }); - } else if (res.status === 204) { - return res.text(); - } else { - return res.json(); +const handleResp = async (res: Response) => { + if (!res.ok) { + const response = await res.json(); + throw new Error(response.error); } + + if (res.status === 204) { + return res.text(); + } + + return res.json(); }; type PartialRequestInit = Overwrite, { body?: any }>; @@ -53,6 +50,10 @@ export class RestClient { }, }); } - return fetch(`${this.uri}${path}`, buildOptions(opts)).then(handleResp); + return fetch(`${this.uri}${path}`, buildOptions(opts)) + .then(handleResp) + .catch(err => { + throw Error(err); + }); } } diff --git a/src/core/client/ui/components/BaseButton/BaseButton.tsx b/src/core/client/ui/components/BaseButton/BaseButton.tsx index b9a95a1ee..8dfa8575b 100644 --- a/src/core/client/ui/components/BaseButton/BaseButton.tsx +++ b/src/core/client/ui/components/BaseButton/BaseButton.tsx @@ -50,8 +50,6 @@ const BaseButton: StatelessComponent = ({ type, ...rest }) => { - console.log("type", type, "Basebutton"); - let Element = "button"; if (anchor) { diff --git a/src/core/client/ui/components/Button/Button.tsx b/src/core/client/ui/components/Button/Button.tsx index e466049dd..424b8bbe5 100644 --- a/src/core/client/ui/components/Button/Button.tsx +++ b/src/core/client/ui/components/Button/Button.tsx @@ -63,8 +63,6 @@ export class Button extends React.Component { ...rest } = this.props; - console.log("type", type, "---------"); - const rootClassName = cn(classes.root, className, { [classes.sizeRegular]: size === "regular", [classes.sizeSmall]: size === "small", From 94ad23ce9bf9361e8b66e67d470aa2748e0dec07 Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Thu, 16 Aug 2018 16:21:52 -0300 Subject: [PATCH 15/27] Working validation from the server using CallOuts --- src/core/client/auth/components/SignIn.tsx | 10 +++++++--- src/core/client/auth/components/SignUp.tsx | 10 +++++++--- .../client/auth/containers/SignInContainer.tsx | 15 +++++++-------- .../client/auth/containers/SignUpContainer.tsx | 12 +++++------- src/core/client/auth/mutations/SignInMutation.ts | 1 + src/core/client/auth/mutations/SignOffMutation.ts | 12 ++++++++---- src/core/client/auth/mutations/SignUpMutation.ts | 1 + .../client/framework/lib/errors/exceptionError.ts | 4 ++++ src/core/client/framework/lib/errors/index.ts | 2 ++ src/core/client/framework/lib/rest.ts | 14 ++++++++------ 10 files changed, 50 insertions(+), 31 deletions(-) create mode 100644 src/core/client/framework/lib/errors/exceptionError.ts diff --git a/src/core/client/auth/components/SignIn.tsx b/src/core/client/auth/components/SignIn.tsx index 1934d03a8..24f2cb2f3 100644 --- a/src/core/client/auth/components/SignIn.tsx +++ b/src/core/client/auth/components/SignIn.tsx @@ -28,20 +28,24 @@ interface FormProps { export interface SignInForm { onSubmit: OnSubmit; setView: (view: View) => void; - errorMessage: string; + error: string | null; } const SignIn: StatelessComponent = props => { + console.log(props); return ( {({ handleSubmit, submitting }) => ( - {props.errorMessage && {props.errorMessage}} Sign in to join the conversation - + {props.error && ( + + {props.error} + + )} ; setView: (view: View) => void; - errorMessage: string; + error: string | null; } const SignUp: StatelessComponent = props => { return ( - {props.errorMessage && {props.errorMessage}} + {!!props.error && {props.error}} {({ handleSubmit, submitting }) => ( @@ -48,7 +48,11 @@ const SignUp: StatelessComponent = props => { Sign up to join the conversation - + {props.error && ( + + {props.error} + + )} { - public state = { errorMessage: "" }; + public state = { error: null }; private setView = (view: View) => { this.props.setView({ view, @@ -31,15 +32,13 @@ class SignInContainer extends Component< }; private onSubmit: SignInForm["onSubmit"] = async (input, form) => { try { - const res = await this.props.signIn(input); - console.log(res); - // form.reset(); + await this.props.signIn(input); + form.reset(); } catch (error) { + this.setState({ error: error.message }); if (error instanceof BadUserInputError) { return error.invalidArgsLocalized; } - console.log(error); - this.setState({ errorMessage: `Error: ${error}` }); // tslint:disable-next-line:no-console console.error(error); } @@ -50,7 +49,7 @@ class SignInContainer extends Component< ); } diff --git a/src/core/client/auth/containers/SignUpContainer.tsx b/src/core/client/auth/containers/SignUpContainer.tsx index 299bc5c91..1a9f91a46 100644 --- a/src/core/client/auth/containers/SignUpContainer.tsx +++ b/src/core/client/auth/containers/SignUpContainer.tsx @@ -15,7 +15,7 @@ interface SignUpContainerProps { } interface SignUpContainerState { - errorMessage: string; + error: string | null; } export type View = "SIGN_IN"; @@ -24,7 +24,7 @@ class SignUpContainer extends Component< SignUpContainerProps, SignUpContainerState > { - public state = { errorMessage: "" }; + public state = { error: "" }; private setView = (view: View) => { this.props.setView({ view, @@ -32,15 +32,13 @@ class SignUpContainer extends Component< }; private onSubmit: SignUpForm["onSubmit"] = async (input, form) => { try { - const res = await this.props.signUp(input); - console.log("response", res); + await this.props.signUp(input); form.reset(); } catch (error) { - console.log("error", error); + this.setState({ error: error.message }); if (error instanceof BadUserInputError) { return error.invalidArgsLocalized; } - this.setState({ errorMessage: `Something ${error}` }); // tslint:disable-next-line:no-console console.error(error); } @@ -51,7 +49,7 @@ class SignUpContainer extends Component< ); } diff --git a/src/core/client/auth/mutations/SignInMutation.ts b/src/core/client/auth/mutations/SignInMutation.ts index 6a88b9313..16fbf52d4 100644 --- a/src/core/client/auth/mutations/SignInMutation.ts +++ b/src/core/client/auth/mutations/SignInMutation.ts @@ -17,6 +17,7 @@ export async function commit( window.close(); } catch (err) { postMessage.send("authError", err.toString(), window.opener); + throw Error(err.message); } } diff --git a/src/core/client/auth/mutations/SignOffMutation.ts b/src/core/client/auth/mutations/SignOffMutation.ts index dcaadfd8f..01aa38cf0 100644 --- a/src/core/client/auth/mutations/SignOffMutation.ts +++ b/src/core/client/auth/mutations/SignOffMutation.ts @@ -11,10 +11,14 @@ export async function commit( input: SignOffInput, { rest, localStorage }: TalkContext ) { - await signOff(rest, input); - // tslint:disable-next-line:no-console - console.log("Signing off"); - localStorage.removeItem("authToken"); + try { + await signOff(rest, input); + localStorage.removeItem("authToken"); + } catch (error) { + localStorage.removeItem("authToken"); + // tslint:disable-next-line:no-console + console.error("error", error); + } } export const withSignOffMutation = createMutationContainer("signOff", commit); diff --git a/src/core/client/auth/mutations/SignUpMutation.ts b/src/core/client/auth/mutations/SignUpMutation.ts index 68beaeae2..76e6442c2 100644 --- a/src/core/client/auth/mutations/SignUpMutation.ts +++ b/src/core/client/auth/mutations/SignUpMutation.ts @@ -17,6 +17,7 @@ export async function commit( window.close(); } catch (err) { postMessage.send("authError", err.toString(), window.opener); + throw Error(err.toString()); } } diff --git a/src/core/client/framework/lib/errors/exceptionError.ts b/src/core/client/framework/lib/errors/exceptionError.ts new file mode 100644 index 000000000..16e79eaa6 --- /dev/null +++ b/src/core/client/framework/lib/errors/exceptionError.ts @@ -0,0 +1,4 @@ +export interface ExceptionError { + message: string; + stack: string; +} diff --git a/src/core/client/framework/lib/errors/index.ts b/src/core/client/framework/lib/errors/index.ts index 85539f649..2d58d8d08 100644 --- a/src/core/client/framework/lib/errors/index.ts +++ b/src/core/client/framework/lib/errors/index.ts @@ -2,4 +2,6 @@ export { default as NetworkError } from "./networkError"; export { default as UnknownServerError } from "./unknownServerError"; export { default as BadUserInputError } from "./badUserInputError"; export { default as GraphQLError } from "./graphqlError"; +export { default as ExceptionError } from "./exceptionError"; + export * from "./graphqlError"; diff --git a/src/core/client/framework/lib/rest.ts b/src/core/client/framework/lib/rest.ts index 975c7e282..b071aedb4 100644 --- a/src/core/client/framework/lib/rest.ts +++ b/src/core/client/framework/lib/rest.ts @@ -18,6 +18,11 @@ const buildOptions = (inputOptions: RequestInit = {}) => { }; const handleResp = async (res: Response) => { + if (res.status === 404) { + const response = await res.text(); + throw new Error(response); + } + if (!res.ok) { const response = await res.json(); throw new Error(response.error); @@ -41,7 +46,7 @@ export class RestClient { this.tokenGetter = tokenGetter; } - public fetch(path: string, options: PartialRequestInit): Promise { + public async fetch(path: string, options: PartialRequestInit): Promise { let opts = options; if (this.tokenGetter) { opts = merge({}, options, { @@ -50,10 +55,7 @@ export class RestClient { }, }); } - return fetch(`${this.uri}${path}`, buildOptions(opts)) - .then(handleResp) - .catch(err => { - throw Error(err); - }); + const response = await fetch(`${this.uri}${path}`, buildOptions(opts)); + return handleResp(response); } } From 6e1b9c99ac56756592a8040d65377ed3f57232b7 Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Thu, 16 Aug 2018 16:24:49 -0300 Subject: [PATCH 16/27] Working validation from the server using CallOuts --- src/core/client/auth/components/SignUp.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/core/client/auth/components/SignUp.tsx b/src/core/client/auth/components/SignUp.tsx index 6fcfc5426..c1454fb16 100644 --- a/src/core/client/auth/components/SignUp.tsx +++ b/src/core/client/auth/components/SignUp.tsx @@ -40,7 +40,6 @@ export interface SignUpForm { const SignUp: StatelessComponent = props => { return ( - {!!props.error && {props.error}} {({ handleSubmit, submitting }) => ( From d1e938112e5df7d49dba8153db5e4da9b7c3d747 Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Fri, 17 Aug 2018 09:06:00 -0300 Subject: [PATCH 17/27] Adding Spinner Component to UI and some corrections to the docs --- .../ui/components/FormField/FormField.mdx | 32 +++------ .../InputDescription/InputDescription.mdx | 6 +- .../client/ui/components/Spinner/Spinner.css | 68 +++++++++++++++++++ .../client/ui/components/Spinner/Spinner.mdx | 15 ++++ .../client/ui/components/Spinner/Spinner.tsx | 46 +++++++++++++ .../client/ui/components/Spinner/index.ts | 1 + src/core/client/ui/components/index.ts | 1 + src/docs/forms.mdx | 19 +++--- 8 files changed, 151 insertions(+), 37 deletions(-) create mode 100644 src/core/client/ui/components/Spinner/Spinner.css create mode 100644 src/core/client/ui/components/Spinner/Spinner.mdx create mode 100644 src/core/client/ui/components/Spinner/Spinner.tsx create mode 100644 src/core/client/ui/components/Spinner/index.ts diff --git a/src/core/client/ui/components/FormField/FormField.mdx b/src/core/client/ui/components/FormField/FormField.mdx index 3dc055085..cfd503953 100644 --- a/src/core/client/ui/components/FormField/FormField.mdx +++ b/src/core/client/ui/components/FormField/FormField.mdx @@ -5,32 +5,18 @@ menu: UI Kit import { Playground, PropsTable } from 'docz' import FormField from './FormField' -import { InputLabel, ValidationMessage, TextField, Typography, Flex, Button} from '../core/client/ui/components' -# Flex +# FormField -`FormField` is to be used with Form Components `flexbox`. +`FormField` is to be used with Form Components: `InputLabel`, `InputDescription`, `TextField`, `ValidationMessage`, etc. +FormField manages the gutters between inner elements. It's a form field wrapper of the `Flex` Component. ## Justify content - - - - -## Align items - - - - - - - - -## Direction - - - - - - + + Username + A unique identifier displayed on your comments. You may use “_” and “.” + + Invalid characters. Try again. + diff --git a/src/core/client/ui/components/InputDescription/InputDescription.mdx b/src/core/client/ui/components/InputDescription/InputDescription.mdx index c27a460be..7a68cc49f 100644 --- a/src/core/client/ui/components/InputDescription/InputDescription.mdx +++ b/src/core/client/ui/components/InputDescription/InputDescription.mdx @@ -4,7 +4,7 @@ menu: UI Kit --- import { Playground, PropsTable } from 'docz' -import { InputDescription } from '../core/client/ui/components' +import InputDescription from './InputDescription' # Flex @@ -12,7 +12,5 @@ import { InputDescription } from '../core/client/ui/components' ## Align items - - This is a description - + This is a description diff --git a/src/core/client/ui/components/Spinner/Spinner.css b/src/core/client/ui/components/Spinner/Spinner.css new file mode 100644 index 000000000..5dd6c6acf --- /dev/null +++ b/src/core/client/ui/components/Spinner/Spinner.css @@ -0,0 +1,68 @@ +.root { + display: block; + text-align: center; +} + +.spinner { + animation: rotator 1.4s linear infinite; +} + +@keyframes rotator { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(270deg); + } +} + +.path { + stroke: var(--palette-primary-main); + stroke-dasharray: 187; + stroke-dashoffset: 0; + transform-origin: center; + animation: dash 1.4s ease-in-out infinite, colors 5.6s ease-in-out infinite; +} + +@keyframes colors { + 0% { + stroke: var(--palette-primary-main); + } + 100% { + stroke: var(--palette-primary-light); + } +} + +@keyframes dash { + 0% { + stroke-dashoffset: 187; + } + 50% { + stroke-dashoffset: 46.75; + transform: rotate(135deg); + } + 100% { + stroke-dashoffset: 187; + transform: rotate(450deg); + } +} + +@keyframes fullRotator { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} + +/* Hack for IE and Edge as they don't support css animations on SVG elements. */ +_:-ms-lang(x), +.path { + stroke-dasharray: 160; +} + +_:-ms-lang(x), +.spinner { + animation: fullRotator 1.4s linear infinite; +} diff --git a/src/core/client/ui/components/Spinner/Spinner.mdx b/src/core/client/ui/components/Spinner/Spinner.mdx new file mode 100644 index 000000000..66ef80309 --- /dev/null +++ b/src/core/client/ui/components/Spinner/Spinner.mdx @@ -0,0 +1,15 @@ +--- +name: Spinner +menu: UI Kit +--- + +import { Playground } from "docz" +import Spinner from "./Spinner" + +# Spinner + +## Basic usage + + + + diff --git a/src/core/client/ui/components/Spinner/Spinner.tsx b/src/core/client/ui/components/Spinner/Spinner.tsx new file mode 100644 index 000000000..2edb5e029 --- /dev/null +++ b/src/core/client/ui/components/Spinner/Spinner.tsx @@ -0,0 +1,46 @@ +import cn from "classnames"; +import React, { StatelessComponent } from "react"; +import { withStyles } from "talk-ui/hocs"; +import * as styles from "./Spinner.css"; + +export interface SpinnerProps { + /** + * Convenient prop to override the root styling. + */ + className?: string; + /** + * Override or extend the styles applied to the component. + */ + classes: typeof styles; +} + +const Spinner: StatelessComponent = props => { + const { className, classes } = props; + + const rootClassName = cn(classes.root, className); + + return ( +
+ + + +
+ ); +}; + +const enhanced = withStyles(styles)(Spinner); +export default enhanced; diff --git a/src/core/client/ui/components/Spinner/index.ts b/src/core/client/ui/components/Spinner/index.ts new file mode 100644 index 000000000..66d1096a0 --- /dev/null +++ b/src/core/client/ui/components/Spinner/index.ts @@ -0,0 +1 @@ +export { default } from "./Spinner"; diff --git a/src/core/client/ui/components/index.ts b/src/core/client/ui/components/index.ts index 76efab87c..52ebe6a79 100644 --- a/src/core/client/ui/components/index.ts +++ b/src/core/client/ui/components/index.ts @@ -16,3 +16,4 @@ export { default as ClickOutside } from "./ClickOutside"; export { default as Popup } from "./Popup"; export { default as FormField } from "./FormField"; export { default as InputDescription } from "./InputDescription"; +export { default as Spinner } from "./Spinner"; diff --git a/src/docs/forms.mdx b/src/docs/forms.mdx index b114cf7e1..547d33701 100644 --- a/src/docs/forms.mdx +++ b/src/docs/forms.mdx @@ -10,13 +10,13 @@ Let's build some forms! We will use the following compoenents `InputLabel`, `Typ ### Examples import { Playground, PropsTable } from 'docz' -import { InputLabel, ValidationMessage, TextField, Typography, Flex, Button} from '../core/client/ui/components' +import { InputLabel, ValidationMessage, TextField, InputDescription, Flex, Button, FormField, Typography } from '../core/client/ui/components' # InputLabel ## Simple Form - + Sign up to join the conversation @@ -26,14 +26,13 @@ import { InputLabel, ValidationMessage, TextField, Typography, Flex, Button} fro Username - A unique identifier displayed on your comments. You may use “_” and “.” + A unique identifier displayed on your comments. You may use “_” and “.” - Password - Must be at least 8 characters + Must be at least 8 characters @@ -42,14 +41,14 @@ import { InputLabel, ValidationMessage, TextField, Typography, Flex, Button} fro - + ## Simple Form with error - + Sign up to join the conversation @@ -59,7 +58,7 @@ import { InputLabel, ValidationMessage, TextField, Typography, Flex, Button} fro Username - A unique identifier displayed on your comments. You may use “_” and “.” + A unique identifier displayed on your comments. You may use “_” and “.” Invalid characters. Try again. @@ -67,7 +66,7 @@ import { InputLabel, ValidationMessage, TextField, Typography, Flex, Button} fro Password - Must be at least 8 characters + Must be at least 8 characters @@ -76,6 +75,6 @@ import { InputLabel, ValidationMessage, TextField, Typography, Flex, Button} fro - + From e1dc50a494f3c36de9b2a037489c122391963469 Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Fri, 17 Aug 2018 09:18:02 -0300 Subject: [PATCH 18/27] Updated docs --- src/docs/forms.mdx | 39 +++++++++++++++++++++++++++++++++++---- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/src/docs/forms.mdx b/src/docs/forms.mdx index 547d33701..2fe5dc191 100644 --- a/src/docs/forms.mdx +++ b/src/docs/forms.mdx @@ -10,9 +10,7 @@ Let's build some forms! We will use the following compoenents `InputLabel`, `Typ ### Examples import { Playground, PropsTable } from 'docz' -import { InputLabel, ValidationMessage, TextField, InputDescription, Flex, Button, FormField, Typography } from '../core/client/ui/components' - -# InputLabel +import { InputLabel, CallOut, ValidationMessage, TextField, InputDescription, Flex, Button, FormField, Typography } from '../core/client/ui/components' ## Simple Form @@ -57,13 +55,46 @@ import { InputLabel, ValidationMessage, TextField, InputDescription, Flex, Butto - Username + Username A unique identifier displayed on your comments. You may use “_” and “.” Invalid characters. Try again. + + Password + Must be at least 8 characters + + + + + Confirm Password + + + + +
+ + +## Simple Form with CallOut + + + + Sign up to join the conversation + The email address or password you entered is incorrect. Try again + + Email Address + + + + + Username + A unique identifier displayed on your comments. You may use “_” and “.” + + + + Password Must be at least 8 characters From 63d43caa2399424bc202e2c8b7cda940c45c846d Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Fri, 17 Aug 2018 09:25:13 -0300 Subject: [PATCH 19/27] Adding test for FormField, InputDescription and Spinner --- .../components/FormField/FormField.spec.tsx | 26 ++++++++++++++++ .../__snapshots__/FormField.spec.tsx.snap | 30 +++++++++++++++++++ .../InputDescription.spec.tsx | 11 +++++++ .../InputDescription.spec.tsx.snap | 9 ++++++ .../ui/components/Spinner/Spinner.spec.tsx | 9 ++++++ .../__snapshots__/Spinner.spec.tsx.snap | 25 ++++++++++++++++ 6 files changed, 110 insertions(+) create mode 100644 src/core/client/ui/components/FormField/FormField.spec.tsx create mode 100644 src/core/client/ui/components/FormField/__snapshots__/FormField.spec.tsx.snap create mode 100644 src/core/client/ui/components/InputDescription/InputDescription.spec.tsx create mode 100644 src/core/client/ui/components/InputDescription/__snapshots__/InputDescription.spec.tsx.snap create mode 100644 src/core/client/ui/components/Spinner/Spinner.spec.tsx create mode 100644 src/core/client/ui/components/Spinner/__snapshots__/Spinner.spec.tsx.snap diff --git a/src/core/client/ui/components/FormField/FormField.spec.tsx b/src/core/client/ui/components/FormField/FormField.spec.tsx new file mode 100644 index 000000000..ec94aa250 --- /dev/null +++ b/src/core/client/ui/components/FormField/FormField.spec.tsx @@ -0,0 +1,26 @@ +import React from "react"; +import TestRenderer from "react-test-renderer"; + +import FormField from "../FormField"; + +import { InputDescription, InputLabel, TextField } from "../../components"; + +it("renders correctly", () => { + const renderer = TestRenderer.create( + Form Components should go here + ); + expect(renderer.toJSON()).toMatchSnapshot(); +}); + +it("works with multiple form components", () => { + const renderer = TestRenderer.create( + + Username + + A unique identifier displayed on your comments. You may use “_” and “.” + + + + ); + expect(renderer.toJSON()).toMatchSnapshot(); +}); diff --git a/src/core/client/ui/components/FormField/__snapshots__/FormField.spec.tsx.snap b/src/core/client/ui/components/FormField/__snapshots__/FormField.spec.tsx.snap new file mode 100644 index 000000000..36cb45582 --- /dev/null +++ b/src/core/client/ui/components/FormField/__snapshots__/FormField.spec.tsx.snap @@ -0,0 +1,30 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders correctly 1`] = ` +
+ Form Components should go here +
+`; + +exports[`works with multiple form components 1`] = ` +
+ +

+ A unique identifier displayed on your comments. You may use “_” and “.” +

+ +
+`; diff --git a/src/core/client/ui/components/InputDescription/InputDescription.spec.tsx b/src/core/client/ui/components/InputDescription/InputDescription.spec.tsx new file mode 100644 index 000000000..25987bf3d --- /dev/null +++ b/src/core/client/ui/components/InputDescription/InputDescription.spec.tsx @@ -0,0 +1,11 @@ +import React from "react"; +import TestRenderer from "react-test-renderer"; + +import InputDescription from "../InputDescription"; + +it("renders correctly", () => { + const renderer = TestRenderer.create( + Form Components should go here + ); + expect(renderer.toJSON()).toMatchSnapshot(); +}); diff --git a/src/core/client/ui/components/InputDescription/__snapshots__/InputDescription.spec.tsx.snap b/src/core/client/ui/components/InputDescription/__snapshots__/InputDescription.spec.tsx.snap new file mode 100644 index 000000000..90078c59e --- /dev/null +++ b/src/core/client/ui/components/InputDescription/__snapshots__/InputDescription.spec.tsx.snap @@ -0,0 +1,9 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders correctly 1`] = ` +

+ Form Components should go here +

+`; diff --git a/src/core/client/ui/components/Spinner/Spinner.spec.tsx b/src/core/client/ui/components/Spinner/Spinner.spec.tsx new file mode 100644 index 000000000..05521bf86 --- /dev/null +++ b/src/core/client/ui/components/Spinner/Spinner.spec.tsx @@ -0,0 +1,9 @@ +import React from "react"; +import TestRenderer from "react-test-renderer"; + +import Spinner from "../Spinner"; + +it("renders correctly", () => { + const renderer = TestRenderer.create(); + expect(renderer.toJSON()).toMatchSnapshot(); +}); diff --git a/src/core/client/ui/components/Spinner/__snapshots__/Spinner.spec.tsx.snap b/src/core/client/ui/components/Spinner/__snapshots__/Spinner.spec.tsx.snap new file mode 100644 index 000000000..c5103e9e6 --- /dev/null +++ b/src/core/client/ui/components/Spinner/__snapshots__/Spinner.spec.tsx.snap @@ -0,0 +1,25 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders correctly 1`] = ` +
+ + + +
+`; From 071cf1ce1d2614c365ebcbd285a5d1eb6cddc937 Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Fri, 17 Aug 2018 10:49:50 -0300 Subject: [PATCH 20/27] User signedIn enters the stream --- src/core/client/stream/queries/StreamQuery.tsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/core/client/stream/queries/StreamQuery.tsx b/src/core/client/stream/queries/StreamQuery.tsx index f6ad272bb..f3083da0e 100644 --- a/src/core/client/stream/queries/StreamQuery.tsx +++ b/src/core/client/stream/queries/StreamQuery.tsx @@ -1,5 +1,4 @@ import * as React from "react"; -import { StatelessComponent } from "react"; import { ReadyState } from "react-relay"; import { @@ -14,6 +13,7 @@ import { import { StreamQueryLocal as Local } from "talk-stream/__generated__/StreamQueryLocal.graphql"; import StreamContainer from "../containers/StreamContainer"; +import { StatelessComponent } from "enzyme"; interface InnerProps { local: Local; @@ -25,6 +25,7 @@ export const render = ({ error, props }: ReadyState) => { } if (props) { + console.log(props); return ; } @@ -32,7 +33,7 @@ export const render = ({ error, props }: ReadyState) => { }; const StreamQuery: StatelessComponent = ({ - local: { assetID }, + local: { assetID, authToken }, }) => ( query={graphql` @@ -40,7 +41,7 @@ const StreamQuery: StatelessComponent = ({ asset(id: $assetID) { ...StreamContainer_asset } - me { + me @include(if: $signedIn) { id username displayName @@ -50,6 +51,7 @@ const StreamQuery: StatelessComponent = ({ `} variables={{ assetID, + signedIn: !!authToken, }} render={render} /> @@ -59,6 +61,7 @@ const enhanced = withLocalStateContainer( graphql` fragment StreamQueryLocal on Local { assetID + authToken } ` )(StreamQuery); From 310f2502120ed102da4bc1d133d09300fc00f9f0 Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Fri, 17 Aug 2018 11:40:31 -0300 Subject: [PATCH 21/27] Updated tests and latest changes --- .../client/auth/mutations/SignOffMutation.ts | 28 +++++++++++-------- .../__snapshots__/Stream.spec.tsx.snap | 3 ++ .../stream/queries/PermalinkViewQuery.tsx | 3 +- .../client/stream/queries/StreamQuery.tsx | 4 +-- .../PermalinkViewQuery.spec.tsx.snap | 6 +--- .../test/__snapshots__/loadMore.spec.tsx.snap | 11 ++++++-- .../__snapshots__/permalinkView.spec.tsx.snap | 6 +++- .../permalinkViewAssetNotFound.spec.tsx.snap | 1 + ...permalinkViewCommentNotFound.spec.tsx.snap | 6 +++- .../__snapshots__/renderReplies.spec.tsx.snap | 5 +++- .../__snapshots__/renderStream.spec.tsx.snap | 5 +++- .../showAllReplies.spec.tsx.snap | 11 ++++++-- .../__snapshots__/BaseButton.spec.tsx.snap | 2 ++ .../ui/components/TextField/TextField.tsx | 1 - 14 files changed, 64 insertions(+), 28 deletions(-) diff --git a/src/core/client/auth/mutations/SignOffMutation.ts b/src/core/client/auth/mutations/SignOffMutation.ts index 01aa38cf0..2f34b73fe 100644 --- a/src/core/client/auth/mutations/SignOffMutation.ts +++ b/src/core/client/auth/mutations/SignOffMutation.ts @@ -1,24 +1,30 @@ -import { Environment } from "relay-runtime"; - +import { LOCAL_ID } from "talk-framework/lib/relay/withLocalStateContainer"; import { TalkContext } from "talk-framework/lib/bootstrap"; import { createMutationContainer } from "talk-framework/lib/relay"; -import { signOff, SignOffInput } from "talk-framework/rest"; +import { SignOffInput } from "talk-framework/rest"; +import { commitLocalUpdate, Environment } from "relay-runtime"; export type SignOffMutation = (input: SignOffInput) => Promise; export async function commit( environment: Environment, input: SignOffInput, - { rest, localStorage }: TalkContext + { localStorage }: TalkContext ) { - try { - await signOff(rest, input); + return commitLocalUpdate(environment, store => { + const record = store.get(LOCAL_ID)!; + record.setValue("", "authToken"); localStorage.removeItem("authToken"); - } catch (error) { - localStorage.removeItem("authToken"); - // tslint:disable-next-line:no-console - console.error("error", error); - } + + // Force gc to trigger. + environment + .retain({ + dataID: "tmp", + node: { selections: [] }, + variables: {}, + }) + .dispose(); + }); } export const withSignOffMutation = createMutationContainer("signOff", commit); diff --git a/src/core/client/stream/components/__snapshots__/Stream.spec.tsx.snap b/src/core/client/stream/components/__snapshots__/Stream.spec.tsx.snap index 84cb64ea1..01026fb09 100644 --- a/src/core/client/stream/components/__snapshots__/Stream.spec.tsx.snap +++ b/src/core/client/stream/components/__snapshots__/Stream.spec.tsx.snap @@ -9,6 +9,7 @@ exports[`renders correctly 1`] = ` ; } - return
Loading
; + return ; }; const PermalinkViewQuery: StatelessComponent = ({ diff --git a/src/core/client/stream/queries/StreamQuery.tsx b/src/core/client/stream/queries/StreamQuery.tsx index f3083da0e..9d03c0d49 100644 --- a/src/core/client/stream/queries/StreamQuery.tsx +++ b/src/core/client/stream/queries/StreamQuery.tsx @@ -14,6 +14,7 @@ import { StreamQueryLocal as Local } from "talk-stream/__generated__/StreamQuery import StreamContainer from "../containers/StreamContainer"; import { StatelessComponent } from "enzyme"; +import { Spinner } from "talk-ui/components"; interface InnerProps { local: Local; @@ -25,11 +26,10 @@ export const render = ({ error, props }: ReadyState) => { } if (props) { - console.log(props); return ; } - return
Loading
; + return ; }; const StreamQuery: StatelessComponent = ({ diff --git a/src/core/client/stream/queries/__snapshots__/PermalinkViewQuery.spec.tsx.snap b/src/core/client/stream/queries/__snapshots__/PermalinkViewQuery.spec.tsx.snap index ef1057221..b7d371bd9 100644 --- a/src/core/client/stream/queries/__snapshots__/PermalinkViewQuery.spec.tsx.snap +++ b/src/core/client/stream/queries/__snapshots__/PermalinkViewQuery.spec.tsx.snap @@ -6,11 +6,7 @@ exports[`renders error 1`] = `
`; -exports[`renders loading 1`] = ` -
- Loading -
-`; +exports[`renders loading 1`] = ``; exports[`renders permalink view container 1`] = ` Sign in @@ -40,6 +41,7 @@ exports[`loads more comments 1`] = ` onMouseOut={[Function]} onMouseOver={[Function]} onTouchEnd={[Function]} + type="button" > Register @@ -68,8 +70,9 @@ exports[`loads more comments 1`] = ` onMouseOut={[Function]} onMouseOver={[Function]} onTouchEnd={[Function]} + type="button" > - Sign In and join the conversation + Sign in and join the conversation
Sign in @@ -252,6 +256,7 @@ exports[`renders comment stream 1`] = ` onMouseOut={[Function]} onMouseOver={[Function]} onTouchEnd={[Function]} + type="button" > Register @@ -280,8 +285,9 @@ exports[`renders comment stream 1`] = ` onMouseOut={[Function]} onMouseOver={[Function]} onTouchEnd={[Function]} + type="button" > - Sign In and join the conversation + Sign in and join the conversation
Load More diff --git a/src/core/client/stream/test/__snapshots__/permalinkView.spec.tsx.snap b/src/core/client/stream/test/__snapshots__/permalinkView.spec.tsx.snap index 666eaa2a2..9a449985f 100644 --- a/src/core/client/stream/test/__snapshots__/permalinkView.spec.tsx.snap +++ b/src/core/client/stream/test/__snapshots__/permalinkView.spec.tsx.snap @@ -22,6 +22,7 @@ exports[`renders permalink view 1`] = ` onMouseOver={[Function]} onTouchEnd={[Function]} target="_parent" + type="button" > Show all Comments @@ -99,6 +100,7 @@ exports[`show all comments 1`] = ` onMouseOut={[Function]} onMouseOver={[Function]} onTouchEnd={[Function]} + type="button" > Sign in @@ -111,6 +113,7 @@ exports[`show all comments 1`] = ` onMouseOut={[Function]} onMouseOver={[Function]} onTouchEnd={[Function]} + type="button" > Register @@ -139,8 +142,9 @@ exports[`show all comments 1`] = ` onMouseOut={[Function]} onMouseOver={[Function]} onTouchEnd={[Function]} + type="button" > - Sign In and join the conversation + Sign in and join the conversation
Show all Comments diff --git a/src/core/client/stream/test/__snapshots__/permalinkViewCommentNotFound.spec.tsx.snap b/src/core/client/stream/test/__snapshots__/permalinkViewCommentNotFound.spec.tsx.snap index 18a0352be..b6f816dbc 100644 --- a/src/core/client/stream/test/__snapshots__/permalinkViewCommentNotFound.spec.tsx.snap +++ b/src/core/client/stream/test/__snapshots__/permalinkViewCommentNotFound.spec.tsx.snap @@ -22,6 +22,7 @@ exports[`renders permalink view with unknown comment 1`] = ` onMouseOver={[Function]} onTouchEnd={[Function]} target="_parent" + type="button" > Show all Comments @@ -63,6 +64,7 @@ exports[`show all comments 1`] = ` onMouseOut={[Function]} onMouseOver={[Function]} onTouchEnd={[Function]} + type="button" > Sign in @@ -75,6 +77,7 @@ exports[`show all comments 1`] = ` onMouseOut={[Function]} onMouseOver={[Function]} onTouchEnd={[Function]} + type="button" > Register @@ -103,8 +106,9 @@ exports[`show all comments 1`] = ` onMouseOut={[Function]} onMouseOver={[Function]} onTouchEnd={[Function]} + type="button" > - Sign In and join the conversation + Sign in and join the conversation
Sign in @@ -40,6 +41,7 @@ exports[`renders comment stream 1`] = ` onMouseOut={[Function]} onMouseOver={[Function]} onTouchEnd={[Function]} + type="button" > Register @@ -68,8 +70,9 @@ exports[`renders comment stream 1`] = ` onMouseOut={[Function]} onMouseOver={[Function]} onTouchEnd={[Function]} + type="button" > - Sign In and join the conversation + Sign in and join the conversation
Sign in @@ -40,6 +41,7 @@ exports[`renders comment stream 1`] = ` onMouseOut={[Function]} onMouseOver={[Function]} onTouchEnd={[Function]} + type="button" > Register @@ -68,8 +70,9 @@ exports[`renders comment stream 1`] = ` onMouseOut={[Function]} onMouseOver={[Function]} onTouchEnd={[Function]} + type="button" > - Sign In and join the conversation + Sign in and join the conversation
Sign in @@ -40,6 +41,7 @@ exports[`renders comment stream 1`] = ` onMouseOut={[Function]} onMouseOver={[Function]} onTouchEnd={[Function]} + type="button" > Register @@ -68,8 +70,9 @@ exports[`renders comment stream 1`] = ` onMouseOut={[Function]} onMouseOver={[Function]} onTouchEnd={[Function]} + type="button" > - Sign In and join the conversation + Sign in and join the conversation
Show All Replies @@ -220,6 +224,7 @@ exports[`show all replies 1`] = ` onMouseOut={[Function]} onMouseOver={[Function]} onTouchEnd={[Function]} + type="button" > Sign in @@ -232,6 +237,7 @@ exports[`show all replies 1`] = ` onMouseOut={[Function]} onMouseOver={[Function]} onTouchEnd={[Function]} + type="button" > Register @@ -260,8 +266,9 @@ exports[`show all replies 1`] = ` onMouseOut={[Function]} onMouseOver={[Function]} onTouchEnd={[Function]} + type="button" > - Sign In and join the conversation + Sign in and join the conversation
Push Me @@ -23,6 +24,7 @@ exports[`renders correctly 1`] = ` onMouseOut={[Function]} onMouseOver={[Function]} onTouchEnd={[Function]} + type="button" > Push Me diff --git a/src/core/client/ui/components/TextField/TextField.tsx b/src/core/client/ui/components/TextField/TextField.tsx index 56894ba2f..05b1f7bc6 100644 --- a/src/core/client/ui/components/TextField/TextField.tsx +++ b/src/core/client/ui/components/TextField/TextField.tsx @@ -84,7 +84,6 @@ const TextField: StatelessComponent = props => { TextField.defaultProps = { color: "regular", - fullWidth: true, placeholder: "", }; From fb2af720ed65cc05cc4f45e1bab7a9a47203e8a6 Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Fri, 17 Aug 2018 11:45:57 -0300 Subject: [PATCH 22/27] Tests updated --- .../stream/queries/__snapshots__/StreamQuery.spec.tsx.snap | 6 +----- .../FormField/__snapshots__/FormField.spec.tsx.snap | 2 +- .../TextField/__snapshots__/TextField.spec.tsx.snap | 2 +- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/src/core/client/stream/queries/__snapshots__/StreamQuery.spec.tsx.snap b/src/core/client/stream/queries/__snapshots__/StreamQuery.spec.tsx.snap index 317eb3f4a..cd2a53005 100644 --- a/src/core/client/stream/queries/__snapshots__/StreamQuery.spec.tsx.snap +++ b/src/core/client/stream/queries/__snapshots__/StreamQuery.spec.tsx.snap @@ -6,11 +6,7 @@ exports[`renders error 1`] = `
`; -exports[`renders loading 1`] = ` -
- Loading -
-`; +exports[`renders loading 1`] = ``; exports[`renders stream container 1`] = `
diff --git a/src/core/client/ui/components/TextField/__snapshots__/TextField.spec.tsx.snap b/src/core/client/ui/components/TextField/__snapshots__/TextField.spec.tsx.snap index 45486b121..a277de918 100644 --- a/src/core/client/ui/components/TextField/__snapshots__/TextField.spec.tsx.snap +++ b/src/core/client/ui/components/TextField/__snapshots__/TextField.spec.tsx.snap @@ -2,7 +2,7 @@ exports[`renders correctly 1`] = ` From 1c135a9ccb6b5cb11ea5f3769058697a3802a7fa Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Fri, 17 Aug 2018 12:02:07 -0300 Subject: [PATCH 23/27] Adding variable to the scope and contemplating undefined from the BE --- src/core/client/stream/components/Stream.tsx | 2 +- src/core/client/stream/containers/StreamContainer.tsx | 2 +- src/core/client/stream/containers/UserBoxContainer.tsx | 2 +- src/core/client/stream/queries/StreamQuery.tsx | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/core/client/stream/components/Stream.tsx b/src/core/client/stream/components/Stream.tsx index 3d113df44..a74435585 100644 --- a/src/core/client/stream/components/Stream.tsx +++ b/src/core/client/stream/components/Stream.tsx @@ -19,7 +19,7 @@ export interface StreamProps { onLoadMore?: () => void; hasMore?: boolean; disableLoadMore?: boolean; - user: User | null; + user: User | null | undefined; } const Stream: StatelessComponent = props => { diff --git a/src/core/client/stream/containers/StreamContainer.tsx b/src/core/client/stream/containers/StreamContainer.tsx index 1a185c902..4df5afaf5 100644 --- a/src/core/client/stream/containers/StreamContainer.tsx +++ b/src/core/client/stream/containers/StreamContainer.tsx @@ -14,7 +14,7 @@ import { User } from "../containers/UserBoxContainer"; interface InnerProps { asset: Data; - user: User | null; + user: User | null | undefined; relay: RelayPaginationProp; } diff --git a/src/core/client/stream/containers/UserBoxContainer.tsx b/src/core/client/stream/containers/UserBoxContainer.tsx index 0a9d72561..51b09e914 100644 --- a/src/core/client/stream/containers/UserBoxContainer.tsx +++ b/src/core/client/stream/containers/UserBoxContainer.tsx @@ -29,7 +29,7 @@ export interface User { interface InnerProps { local: Local; - user: User | null; + user: User | null | undefined; showAuthPopup: ShowAuthPopupMutation; setAuthPopupState: SetAuthPopupStateMutation; } diff --git a/src/core/client/stream/queries/StreamQuery.tsx b/src/core/client/stream/queries/StreamQuery.tsx index 9d03c0d49..abcab7787 100644 --- a/src/core/client/stream/queries/StreamQuery.tsx +++ b/src/core/client/stream/queries/StreamQuery.tsx @@ -37,7 +37,7 @@ const StreamQuery: StatelessComponent = ({ }) => ( query={graphql` - query StreamQuery($assetID: ID!) { + query StreamQuery($assetID: ID!, $signedIn: Boolean!) { asset(id: $assetID) { ...StreamContainer_asset } From fe3f6e8d41d8e2144f4d96fd18d5702420a7011f Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Fri, 17 Aug 2018 12:10:09 -0300 Subject: [PATCH 24/27] Missing console.log --- src/core/client/auth/components/SignIn.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/core/client/auth/components/SignIn.tsx b/src/core/client/auth/components/SignIn.tsx index 24f2cb2f3..98b0d6e70 100644 --- a/src/core/client/auth/components/SignIn.tsx +++ b/src/core/client/auth/components/SignIn.tsx @@ -32,7 +32,6 @@ export interface SignInForm { } const SignIn: StatelessComponent = props => { - console.log(props); return (
{({ handleSubmit, submitting }) => ( From 348ce99828aab391b48a4e2a1436034604fb97d8 Mon Sep 17 00:00:00 2001 From: Belen Curcio Date: Fri, 17 Aug 2018 12:17:22 -0300 Subject: [PATCH 25/27] TSLint imports ordered --- src/core/client/auth/mutations/SignOffMutation.ts | 4 ++-- src/core/client/stream/queries/PermalinkViewQuery.tsx | 2 +- src/core/client/stream/queries/StreamQuery.tsx | 7 ++----- 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/src/core/client/auth/mutations/SignOffMutation.ts b/src/core/client/auth/mutations/SignOffMutation.ts index 2f34b73fe..5a1bf5939 100644 --- a/src/core/client/auth/mutations/SignOffMutation.ts +++ b/src/core/client/auth/mutations/SignOffMutation.ts @@ -1,8 +1,8 @@ -import { LOCAL_ID } from "talk-framework/lib/relay/withLocalStateContainer"; +import { commitLocalUpdate, Environment } from "relay-runtime"; import { TalkContext } from "talk-framework/lib/bootstrap"; import { createMutationContainer } from "talk-framework/lib/relay"; +import { LOCAL_ID } from "talk-framework/lib/relay/withLocalStateContainer"; import { SignOffInput } from "talk-framework/rest"; -import { commitLocalUpdate, Environment } from "relay-runtime"; export type SignOffMutation = (input: SignOffInput) => Promise; diff --git a/src/core/client/stream/queries/PermalinkViewQuery.tsx b/src/core/client/stream/queries/PermalinkViewQuery.tsx index 9c4493423..3a9b22f71 100644 --- a/src/core/client/stream/queries/PermalinkViewQuery.tsx +++ b/src/core/client/stream/queries/PermalinkViewQuery.tsx @@ -13,8 +13,8 @@ import { } from "talk-stream/__generated__/PermalinkViewQuery.graphql"; import { PermalinkViewQueryLocal as Local } from "talk-stream/__generated__/PermalinkViewQueryLocal.graphql"; -import PermalinkViewContainer from "../containers/PermalinkViewContainer"; import { Spinner } from "talk-ui/components"; +import PermalinkViewContainer from "../containers/PermalinkViewContainer"; interface InnerProps { local: Local; diff --git a/src/core/client/stream/queries/StreamQuery.tsx b/src/core/client/stream/queries/StreamQuery.tsx index abcab7787..78c118cdd 100644 --- a/src/core/client/stream/queries/StreamQuery.tsx +++ b/src/core/client/stream/queries/StreamQuery.tsx @@ -1,6 +1,5 @@ -import * as React from "react"; +import React, { StatelessComponent } from "react"; import { ReadyState } from "react-relay"; - import { graphql, QueryRenderer, @@ -11,10 +10,8 @@ import { StreamQueryVariables, } from "talk-stream/__generated__/StreamQuery.graphql"; import { StreamQueryLocal as Local } from "talk-stream/__generated__/StreamQueryLocal.graphql"; - -import StreamContainer from "../containers/StreamContainer"; -import { StatelessComponent } from "enzyme"; import { Spinner } from "talk-ui/components"; +import StreamContainer from "../containers/StreamContainer"; interface InnerProps { local: Local; From 059292e9797a3f3bfcf442a212353e50e2469af6 Mon Sep 17 00:00:00 2001 From: Kiwi Date: Tue, 21 Aug 2018 19:45:11 +0200 Subject: [PATCH 26/27] Compile schema when watching client (#1809) --- config/watcher.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/config/watcher.ts b/config/watcher.ts index b0def88eb..04c4f5f7d 100644 --- a/config/watcher.ts +++ b/config/watcher.ts @@ -77,6 +77,7 @@ const config: Config = { "compileCSSTypes", "compileRelayStream", "compileRelayAuth", + "compileSchema", ], docz: ["runDocz", "compileCSSTypes"], compile: [ From 5aad008261f7ab7f283fd17021586c69de2d64a3 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Tue, 21 Aug 2018 14:22:04 -0600 Subject: [PATCH 27/27] feat: added token blacklisting --- src/core/server/app/handlers/auth/local.ts | 42 +++++++++++++++- .../server/app/middleware/passport/index.ts | 48 +++++++++++++++++++ .../server/app/middleware/passport/jwt.ts | 42 ++++++++++++++-- src/core/server/app/router.ts | 15 ++++-- 4 files changed, 136 insertions(+), 11 deletions(-) diff --git a/src/core/server/app/handlers/auth/local.ts b/src/core/server/app/handlers/auth/local.ts index 48daf4c32..25688d25a 100644 --- a/src/core/server/app/handlers/auth/local.ts +++ b/src/core/server/app/handlers/auth/local.ts @@ -1,8 +1,12 @@ import { RequestHandler } from "express"; +import { Redis } from "ioredis"; import Joi from "joi"; import { Db } from "mongodb"; -import { handleSuccessfulLogin } from "talk-server/app/middleware/passport"; +import { + handleLogout, + handleSuccessfulLogin, +} from "talk-server/app/middleware/passport"; import { JWTSigningConfig } from "talk-server/app/middleware/passport/jwt"; import { validate } from "talk-server/app/request/body"; import { GQLUSER_ROLE } from "talk-server/graph/tenant/schema/__generated__/types"; @@ -74,3 +78,39 @@ export const signupHandler = (options: SignupOptions): RequestHandler => async ( return next(err); } }; + +export interface LogoutOptions { + redis: Redis; +} + +export const logoutHandler = (options: LogoutOptions): RequestHandler => async ( + req: Request, + res, + next +) => { + try { + // TODO: rate limit based on the IP address and user agent. + + // Tenant is guaranteed at this point. + const tenant = req.tenant!; + + // Check to ensure that the local integration has been enabled. + if (!tenant.auth.integrations.local.enabled) { + // TODO: replace with better error. + return next(new Error("integration is disabled")); + } + + // Get the user on the request. + const user = req.user; + if (!user) { + return next( + new Error("cannot logout when there is no user on the request") + ); + } + + // Delegate to the logout handler. + return handleLogout(options.redis, req, res); + } catch (err) { + return next(err); + } +}; diff --git a/src/core/server/app/middleware/passport/index.ts b/src/core/server/app/middleware/passport/index.ts index ae1a3406f..b5f06ba03 100644 --- a/src/core/server/app/middleware/passport/index.ts +++ b/src/core/server/app/middleware/passport/index.ts @@ -1,10 +1,15 @@ import { NextFunction, RequestHandler, Response } from "express"; +import { Redis } from "ioredis"; +import Joi from "joi"; +import jwt from "jsonwebtoken"; import { Db } from "mongodb"; import passport, { Authenticator } from "passport"; import { Config } from "talk-common/config"; import { + blacklistJWT, createJWTStrategy, + extractJWTFromRequest, JWTSigningConfig, SigningTokenOptions, signTokenString, @@ -12,6 +17,7 @@ import { import { createLocalStrategy } from "talk-server/app/middleware/passport/local"; import { createOIDCStrategy } from "talk-server/app/middleware/passport/oidc"; import { createSSOStrategy } from "talk-server/app/middleware/passport/sso"; +import { validate } from "talk-server/app/request/body"; import { User } from "talk-server/models/user"; import TenantCache from "talk-server/services/tenant/cache"; import { Request } from "talk-server/types/express"; @@ -25,6 +31,7 @@ export type VerifyCallback = ( export interface PassportOptions { config: Config; mongo: Db; + redis: Redis; signingConfig: JWTSigningConfig; tenantCache: TenantCache; } @@ -50,6 +57,47 @@ export function createPassport( return auth; } +interface LogoutToken { + jti: string; + exp: number; +} + +const LogoutTokenSchema = Joi.object().keys({ + jti: Joi.string(), + exp: Joi.number(), +}); + +export async function handleLogout(redis: Redis, req: Request, res: Response) { + // Extract the token from the request. + const token = extractJWTFromRequest(req); + if (!token) { + // TODO: (wyattjoh) return a better error. + throw new Error("logout requires a token on the request, none was found"); + } + + // Decode the token. + const decoded = jwt.decode(token, {}); + if (!decoded) { + // TODO: (wyattjoh) return a better error. + throw new Error( + "logout requires a token on the request, token was invalid" + ); + } + + // Grab the JTI from the decoded token. + const { jti, exp }: LogoutToken = validate(LogoutTokenSchema, decoded); + + // Compute the number of seconds that the token will be valid for. + const validFor = exp - Date.now() / 1000; + if (validFor > 0) { + // Invalidate the token, the expiry is in the future and it needs to be + // blacklisted. + await blacklistJWT(redis, jti, validFor); + } + + return res.sendStatus(204); +} + export async function handleSuccessfulLogin( user: User, signingConfig: JWTSigningConfig, diff --git a/src/core/server/app/middleware/passport/jwt.ts b/src/core/server/app/middleware/passport/jwt.ts index da4d0be11..257b67d1c 100644 --- a/src/core/server/app/middleware/passport/jwt.ts +++ b/src/core/server/app/middleware/passport/jwt.ts @@ -1,8 +1,9 @@ +import { Redis } from "ioredis"; import jwt, { SignOptions } from "jsonwebtoken"; -import uuid from "uuid"; - import { Db } from "mongodb"; import { Strategy } from "passport-strategy"; +import uuid from "uuid"; + import { Config } from "talk-common/config"; import { retrieveUser, User } from "talk-server/models/user"; import { Request } from "talk-server/types/express"; @@ -38,6 +39,31 @@ export function extractJWTFromRequest(req: Request) { return null; } +function generateJTIBlacklistKey(jti: string) { + // jtib: JTI Blacklist namespace. + return `jtib:${jti}`; +} + +export async function blacklistJWT( + redis: Redis, + jti: string, + validFor: number +) { + await redis.setex( + generateJTIBlacklistKey(jti), + Math.ceil(validFor), + Date.now() + ); +} + +export async function checkBlacklistJWT(redis: Redis, jti: string) { + const expiredAtString = await redis.get(generateJTIBlacklistKey(jti)); + if (expiredAtString) { + // TODO: (wyattjoh) return a better error. + throw new Error("JWT exists in blacklist"); + } +} + export enum AsymmetricSigningAlgorithm { RS256 = "RS256", RS384 = "RS384", @@ -139,6 +165,7 @@ export interface JWTToken { export interface JWTStrategyOptions { signingConfig: JWTSigningConfig; mongo: Db; + redis: Redis; } export class JWTStrategy extends Strategy { @@ -146,12 +173,14 @@ export class JWTStrategy extends Strategy { private signingConfig: JWTSigningConfig; private mongo: Db; + private redis: Redis; - constructor({ signingConfig, mongo }: JWTStrategyOptions) { + constructor({ signingConfig, mongo, redis }: JWTStrategyOptions) { super(); this.signingConfig = signingConfig; this.mongo = mongo; + this.redis = redis; } public authenticate(req: Request) { @@ -179,13 +208,16 @@ export class JWTStrategy extends Strategy { // Use the algorithm specified in the configuration. algorithms: [this.signingConfig.algorithm], }, - async (err: Error | undefined, { sub }: JWTToken) => { + async (err: Error | undefined, { jti, sub }: JWTToken) => { if (err) { return this.fail(err, 401); } try { - // Find the user. + // Check to see if the token has been blacklisted. + await checkBlacklistJWT(this.redis, jti); + + // Find the user referenced by the token. const user = await retrieveUser(this.mongo, tenant.id, sub); // Return them! The user may be null, but that's ok here. diff --git a/src/core/server/app/router.ts b/src/core/server/app/router.ts index 75b195ab3..c298a40aa 100644 --- a/src/core/server/app/router.ts +++ b/src/core/server/app/router.ts @@ -1,7 +1,10 @@ import express from "express"; import passport from "passport"; -import { signupHandler } from "talk-server/app/handlers/auth/local"; +import { + logoutHandler, + signupHandler, +} from "talk-server/app/handlers/auth/local"; import { streamHandler } from "talk-server/app/handlers/embed/stream"; import { apiErrorHandler } from "talk-server/app/middleware/error"; import { errorLogger } from "talk-server/app/middleware/logging"; @@ -64,15 +67,17 @@ function createNewAuthRouter(app: AppOptions, options: RouterOptions) { const router = express.Router(); // Mount the passport routes. + router.delete( + "/", + options.passport.authenticate("jwt", { session: false }), + logoutHandler({ redis: app.redis }) + ); + router.post( "/local", express.json(), wrapAuthn(options.passport, app.signingConfig, "local") ); - - // TODO (bc) - add delete user serverside - router.delete("/local", express.json()); - router.post( "/local/signup", express.json(),