mirror of
https://github.com/wassname/talk.git
synced 2026-08-03 13:20:59 +08:00
[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:
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user