[next] Email (#2261)

* feat: suspending, banning, now propogation

* feat: added email rendering + localization support

* fix: fix related to lib

* refactor: moved juicer to queue task

* refactor: cleanup of job processor

* refactor: improved error messaging around failed email

* feat: initial forgot passwor impl

* fix: fixed rebase errors

* feat: send back Content-Language header with requests

* feat: added ban email

* feat: implemented forgotten password API

* fix: linting

* feat: support more emails

* fix: promise patches

* feat: initial confirm email API

* feat: added rate limiting

* feat: added URL support

* feat: added email docs

* fix: updated docs

* chore: documentation review

* fix: fixed build bug

* feat: implement forgot password in auth popup

* test: add tests + fixes

* chore: rename StatelessComponent to FunctionComponent

* fix: types and test fixes

* chore: upgrade deps

* fix: THANK YOU TESTS FOR SAVING MY A**

* chore: reorder imports

* chore: remove obsolete !

* feat: implement accounts bundle

* refactor: review suggestion

* fix: rebase upgrade error

* fix: rebase bug

* feat: reset password link support

* test: add tests for account password reset page

* fix: remove redirect uri

* fix: revert local state changes
This commit is contained in:
Wyatt Johnson
2019-05-09 22:54:56 +02:00
committed by Kiwi
parent 945bd7f2b0
commit df57b4eb17
487 changed files with 6794 additions and 2431 deletions
@@ -2,7 +2,7 @@ import { EventEmitter2 } from "eventemitter2";
import { LocalizationProvider } from "fluent-react/compat";
import { FluentBundle } from "fluent/compat";
import { Child as PymChild } from "pym.js";
import React, { StatelessComponent } from "react";
import React, { FunctionComponent } from "react";
import { MediaQueryMatchers } from "react-responsive";
import { Formatter } from "react-timeago";
import { Environment } from "relay-runtime";
@@ -37,7 +37,7 @@ export interface TalkContext {
/** media query values for testing purposes */
mediaQueryValues?: MediaQueryMatchers;
/** Rest client */
/** Rest Client */
rest: RestClient;
/** postMessage service */
@@ -81,7 +81,7 @@ export const TalkContextConsumer = TalkReactContext.Consumer;
* In addition to just providing the context, TalkContextProvider also
* renders the `LocalizationProvider` with the appropite data.
*/
export const TalkContextProvider: StatelessComponent<{
export const TalkContextProvider: FunctionComponent<{
value: TalkContext;
}> = ({ value, children }) => (
<TalkReactContext.Provider value={value}>
@@ -104,7 +104,7 @@ function createRelayEnvironment() {
return { environment, tokenGetter };
}
function createRestAPI(tokenGetter: () => string) {
function createRestClient(tokenGetter: () => string) {
return new RestClient("/api", tokenGetter);
}
@@ -144,7 +144,7 @@ function createMangedTalkContextProvider(
const newContext = {
...this.state.context,
relayEnvironment: newEnvironment,
rest: createRestAPI(newTokenGetter),
rest: createRestClient(newTokenGetter),
};
// Initialize local state.
@@ -244,7 +244,7 @@ export default async function createManaged({
pym,
eventEmitter,
registerClickFarAway,
rest: createRestAPI(tokenGetter),
rest: createRestClient(tokenGetter),
postMessage: new PostMessageService(),
localStorage,
sessionStorage,
@@ -1,8 +1,8 @@
import React, { StatelessComponent } from "react";
import React, { FunctionComponent } from "react";
import styles from "./ExternalLink.css";
const ExternalLink: StatelessComponent<{
const ExternalLink: FunctionComponent<{
href?: string;
children?: string;
}> = ({ href, children }) => (
@@ -1,12 +1,12 @@
import { Middleware } from "react-relay-network-modern/es";
import extractError from "./extractError";
import extractGraphQLError from "./extractGraphQLError";
const customErrorMiddleware: Middleware = next => async req => {
const res = await next(req);
if (res.errors) {
// Extract custom error.
const error = extractError(res.errors);
const error = extractGraphQLError(res.errors);
if (error) {
throw error;
}
@@ -6,21 +6,28 @@ import {
UnknownServerError,
} from "../errors";
export default function extractError(errors: Error[]): Error | null {
if (errors.length > 1 || !(errors[0] as any).extensions) {
// Multiple errors are GraphQL errors.
// TODO: (cvle) Is this assumption correct?
// No extensions == GraphQL error.
// TODO: (cvle) harmonize with server.
return null;
interface TalkError {
type: string;
code: string;
message: string;
}
function isTalkError(err: any): err is TalkError {
return err.type && err.code && err.message;
}
export default function extractError(
err: TalkError,
unknownErrorMessage: string = "Unknown error"
): Error {
if (!isTalkError(err)) {
return new UnknownServerError(unknownErrorMessage, err);
}
// Handle custom errors here.
const err = errors[0];
if ((err as any).extensions.type === ERROR_TYPES.INVALID_REQUEST_ERROR) {
return new InvalidRequestError((err as any).extensions);
if (err.type === ERROR_TYPES.INVALID_REQUEST_ERROR) {
return new InvalidRequestError(err as any);
}
if ((err as any).extensions.type === ERROR_TYPES.MODERATION_NUDGE_ERROR) {
return new ModerationNudgeError((err as any).extensions);
if (err.type === ERROR_TYPES.MODERATION_NUDGE_ERROR) {
return new ModerationNudgeError(err as any);
}
return new UnknownServerError(err.message, (err as any).extensions);
return new UnknownServerError(err.message, err);
}
@@ -0,0 +1,26 @@
import extractError from "./extractError";
interface GraphQLErrorWithExtension {
extensions: any;
}
function isGraphQLErrorWithExtension(
err: any
): err is GraphQLErrorWithExtension {
return Boolean(err && err.extensions);
}
export default function extractGraphQLError(errors: Error[]): Error | null {
if (errors.length > 1) {
// Multiple errors are GraphQL errors.
// TODO: (cvle) Is this assumption correct?
return null;
}
const err = errors[0];
if (!isGraphQLErrorWithExtension(err)) {
// No extensions == GraphQL error.
// TODO: (cvle) harmonize with server.
return null;
}
return extractError(err.extensions, err.message);
}
@@ -1,2 +1,3 @@
export { default as createNetwork, TokenGetter } from "./createNetwork";
export { default as extractGraphQLError } from "./extractGraphQLError";
export { default as extractError } from "./extractError";
@@ -1,9 +1,9 @@
import React, { ComponentType, StatelessComponent } from "react";
import React, { ComponentType, FunctionComponent } from "react";
// TODO: (cvle) This currently a workaround to hide ForwardRef from
// enzyme which does not support it well yet.
export default function hideForwardRef(component: ComponentType) {
const wrapped: StatelessComponent = (props: any) =>
const wrapped: FunctionComponent = (props: any) =>
React.createElement(component, props);
wrapped.displayName = component.displayName;
return wrapped;
+16 -5
View File
@@ -1,6 +1,8 @@
import { merge } from "lodash";
import { Overwrite } from "talk-framework/types";
import { extractError } from "./network";
const buildOptions = (inputOptions: RequestInit = {}) => {
const defaultOptions: RequestInit = {
method: "GET",
@@ -26,8 +28,14 @@ const handleResp = async (res: Response) => {
}
if (!res.ok) {
const response = await res.json();
throw new Error(response.error.message);
const ctype = res.headers.get("content-type");
if (ctype && ctype.includes("application/json")) {
const response = await res.json();
throw extractError(response.error);
} else {
const response = await res.text();
throw new Error(response);
}
}
if (res.status === 204) {
@@ -37,7 +45,9 @@ const handleResp = async (res: Response) => {
return res.json();
};
type PartialRequestInit = Overwrite<Partial<RequestInit>, { body?: any }>;
type PartialRequestInit = Overwrite<Partial<RequestInit>, { body?: any }> & {
token?: string;
};
export class RestClient {
public readonly uri: string;
@@ -53,10 +63,11 @@ export class RestClient {
options: PartialRequestInit
): Promise<T> {
let opts = options;
if (this.tokenGetter) {
const token = options.token || (this.tokenGetter && this.tokenGetter());
if (token) {
opts = merge({}, options, {
headers: {
Authorization: `Bearer ${this.tokenGetter()}`,
Authorization: `Bearer ${token}`,
},
});
}
@@ -1,13 +1,7 @@
import { RouteMatch, RouteProps } from "found";
import * as React from "react";
interface InjectedProps<T> {
error?: Error | null;
data: T | null | undefined;
retry?: Error | null;
}
type RouteConfig<QueryResponse> = Partial<
type RouteConfig<Props = any, QueryResponse = undefined> = Partial<
Pick<RouteProps, "query" | "getQuery">
> &
Partial<Pick<RouteProps, "data" | "getData" | "defer">> & {
@@ -23,14 +17,17 @@ type RouteConfig<QueryResponse> = Partial<
data: QueryResponse | null;
retry: () => void;
match: RouteMatch;
Component: React.ComponentType<any>;
Component: React.ComponentType<Partial<Props>>;
}) => React.ReactElement;
};
function withRouteConfig<QueryResponse>(config: RouteConfig<QueryResponse>) {
const hoc = <T extends InjectedProps<QueryResponse>>(
component: React.ComponentType<T>
) => {
function withRouteConfig<
Props = any,
QueryResponse = Props extends { data: infer T | null | undefined }
? T
: undefined
>(config: RouteConfig<Props, QueryResponse>) {
const hoc = <T>(component: React.ComponentType<T>) => {
(component as any).routeConfig = {
...config,
Component: component,