From bcee6aad5b85b81f6fc7ad5680533d1a443ddb01 Mon Sep 17 00:00:00 2001 From: abias <abias1122@gmail.com> Date: Sun, 14 May 2023 11:08:06 -0400 Subject: [PATCH] Set up logic for handling errors --- src/server/index.tsx | 59 ++++++++++++-------- src/shared/components/app/app.tsx | 32 ++++++++--- src/shared/components/app/error-page.tsx | 56 +++++++++++++++++++ src/shared/components/app/no-match.tsx | 26 --------- src/shared/components/common/auth-guard.tsx | 13 +++++ src/shared/components/common/error-guard.tsx | 25 +++++++++ src/shared/utils.ts | 6 ++ src/shared/version.ts | 2 +- 8 files changed, 163 insertions(+), 56 deletions(-) create mode 100644 src/shared/components/app/error-page.tsx delete mode 100644 src/shared/components/app/no-match.tsx create mode 100644 src/shared/components/common/auth-guard.tsx create mode 100644 src/shared/components/common/error-guard.tsx diff --git a/src/server/index.tsx b/src/server/index.tsx index 05988cf..8088288 100644 --- a/src/server/index.tsx +++ b/src/server/index.tsx @@ -19,7 +19,14 @@ import { IsoData, } from "../shared/interfaces"; import { routes } from "../shared/routes"; -import { favIconPngUrl, favIconUrl, initializeSite } from "../shared/utils"; +import { + ErrorPageData, + favIconPngUrl, + favIconUrl, + initializeSite, + isAuthPath, +} from "../shared/utils"; +import { VERSION } from "../shared/version"; const server = express(); const [hostname, port] = process.env["LEMMY_UI_HOST"] @@ -109,7 +116,6 @@ server.get("/css/themelist", async (_req, res) => { server.get("/*", async (req, res) => { try { const activeRoute = routes.find(route => matchPath(req.path, route)); - const context = {} as any; let auth: string | undefined = IsomorphicCookie.load("jwt", req); const getSiteForm: GetSite = { auth }; @@ -119,6 +125,8 @@ server.get("/*", async (req, res) => { const headers = setForwardedHeaders(req.headers); const client = new LemmyHttp(getHttpBaseInternal(), headers); + const { path, url, query } = req; + // Get site data first // This bypasses errors, so that the client can hit the error on its own, // in order to remove the jwt on the browser. Necessary for wrong jwts @@ -131,14 +139,18 @@ server.get("/*", async (req, res) => { auth = undefined; try_site = await client.getSite(getSiteForm); } + + if (!auth && isAuthPath(path)) { + res.redirect("/"); + } const site: GetSiteResponse = try_site; initializeSite(site); const initialFetchReq: InitialFetchRequest = { client, auth, - path: req.path, - query: req.query, + path, + query, site, }; @@ -146,7 +158,7 @@ server.get("/*", async (req, res) => { promises.push(...activeRoute.fetchInitialData(initialFetchReq)); } - const routeData = await Promise.all(promises); + let routeData = await Promise.all(promises); // Redirect to the 404 if there's an API error if (routeData[0] && routeData[0].error) { @@ -155,24 +167,36 @@ server.get("/*", async (req, res) => { if (error === "instance_is_private") { return res.redirect(`/signup`); } else { - return res.send(`404: ${removeAuthParam(error)}`); + const errorPageData: ErrorPageData = { type: "error" }; + + // Exact error should only be seen in a development environment. Users + // in production will get a more generic message. + if (VERSION === "dev") { + errorPageData.error = error; + } + + const adminMatrixIds = site.admins + .map(({ person: { matrix_user_id } }) => matrix_user_id) + .filter(id => id) as string[]; + if (adminMatrixIds.length > 0) { + errorPageData.adminMatrixIds = adminMatrixIds; + } + + routeData = [errorPageData]; } } const isoData: IsoData = { - path: req.path, + path, site_res: site, routeData, }; const wrapper = ( - <StaticRouter location={req.url} context={isoData}> + <StaticRouter location={url} context={isoData}> <App /> </StaticRouter> ); - if (context.url) { - return res.redirect(context.url); - } const eruda = ( <> @@ -260,7 +284,8 @@ server.get("/*", async (req, res) => { `); } catch (err) { console.error(err); - return res.send(`404: ${removeAuthParam(err)}`); + res.statusCode = 500; + return res.send(VERSION === "dev" ? err.message : "Server error"); } }); @@ -292,16 +317,6 @@ process.on("SIGINT", () => { process.exit(0); }); -function removeAuthParam(err: any): string { - return removeParam(err.toString(), "auth"); -} - -function removeParam(url: string, parameter: string): string { - return url - .replace(new RegExp("[?&]" + parameter + "=[^&#]*(#.*)?$"), "$1") - .replace(new RegExp("([?&])" + parameter + "=[^&]*&"), "$1"); -} - const iconSizes = [72, 96, 128, 144, 152, 192, 384, 512]; const defaultLogoPathDirectory = path.join( process.cwd(), diff --git a/src/shared/components/app/app.tsx b/src/shared/components/app/app.tsx index 624d6e5..1251a5e 100644 --- a/src/shared/components/app/app.tsx +++ b/src/shared/components/app/app.tsx @@ -3,10 +3,12 @@ import { Provider } from "inferno-i18next-dess"; import { Route, Switch } from "inferno-router"; import { i18n } from "../../i18next"; import { routes } from "../../routes"; -import { setIsoData } from "../../utils"; +import { isAuthPath, setIsoData } from "../../utils"; +import AuthGuard from "../common/auth-guard"; +import ErrorGuard from "../common/error-guard"; +import { ErrorPage } from "./error-page"; import { Footer } from "./footer"; import { Navbar } from "./navbar"; -import { NoMatch } from "./no-match"; import "./styles.scss"; import { Theme } from "./theme"; @@ -16,8 +18,8 @@ export class App extends Component<any, any> { super(props, context); } render() { - let siteRes = this.isoData.site_res; - let siteView = siteRes.site_view; + const siteRes = this.isoData.site_res; + const siteView = siteRes.site_view; return ( <> @@ -27,10 +29,26 @@ export class App extends Component<any, any> { <Navbar siteRes={siteRes} /> <div className="mt-4 p-0 fl-1"> <Switch> - {routes.map(({ path, component }) => ( - <Route key={path} path={path} exact component={component} /> + {routes.map(({ path, component: RouteComponent }) => ( + <Route + key={path} + path={path} + exact + component={routeProps => ( + <ErrorGuard> + {RouteComponent && + (isAuthPath(path ?? "") ? ( + <AuthGuard> + <RouteComponent {...routeProps} /> + </AuthGuard> + ) : ( + <RouteComponent {...routeProps} /> + ))} + </ErrorGuard> + )} + /> ))} - <Route component={NoMatch} /> + <Route component={ErrorPage} /> </Switch> </div> <Footer site={siteRes} /> diff --git a/src/shared/components/app/error-page.tsx b/src/shared/components/app/error-page.tsx new file mode 100644 index 0000000..3b1628b --- /dev/null +++ b/src/shared/components/app/error-page.tsx @@ -0,0 +1,56 @@ +import { Component } from "inferno"; +import { Link } from "inferno-router"; +import { ErrorPageData, setIsoData } from "../../utils"; + +export class ErrorPage extends Component<any, any> { + private isoData = setIsoData(this.context); + + constructor(props: any, context: any) { + super(props, context); + } + + render() { + const errorPageData = this.getErrorPageData(); + + return ( + <div className="container-lg"> + <h1>{errorPageData ? "Error!" : "Page Not Found"}</h1> + <p> + {errorPageData + ? "There was an error on the server. Try refreshing your browser of coming back at a later time" + : "The page you are looking for does not exist"} + </p> + {!errorPageData && ( + <Link to="/">Click here to return to your home page</Link> + )} + {errorPageData?.adminMatrixIds && + errorPageData.adminMatrixIds.length > 0 && ( + <div> + <div> + If you would like to reach out to one of{" "} + {this.isoData.site_res.site_view.site.name}'s admins for + support, try the following Matrix addresses: + </div> + <ul> + {errorPageData.adminMatrixIds.map(matrixId => ( + <li key={matrixId}>{matrixId}</li> + ))} + </ul> + </div> + )} + {errorPageData?.error && <code>{errorPageData.error.message}</code>} + </div> + ); + } + + private getErrorPageData() { + const errorPageData = this.isoData.routeData[0] as + | ErrorPageData + | undefined; + if (errorPageData?.type === "error") { + return errorPageData; + } + + return undefined; + } +} diff --git a/src/shared/components/app/no-match.tsx b/src/shared/components/app/no-match.tsx deleted file mode 100644 index 6781e35..0000000 --- a/src/shared/components/app/no-match.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { NoOptionI18nKeys } from "i18next"; -import { Component } from "inferno"; -import { i18n } from "../../i18next"; - -export class NoMatch extends Component<any, any> { - private errCode = new URLSearchParams(this.props.location.search).get( - "err" - ) as NoOptionI18nKeys; - - constructor(props: any, context: any) { - super(props, context); - } - - render() { - return ( - <div className="container-lg"> - <h1>404</h1> - {this.errCode && ( - <h3> - {i18n.t("code")}: {i18n.t(this.errCode)} - </h3> - )} - </div> - ); - } -} diff --git a/src/shared/components/common/auth-guard.tsx b/src/shared/components/common/auth-guard.tsx new file mode 100644 index 0000000..ea7510a --- /dev/null +++ b/src/shared/components/common/auth-guard.tsx @@ -0,0 +1,13 @@ +import { InfernoNode } from "inferno"; +import { Redirect } from "inferno-router"; +import { UserService } from "../../services"; + +function AuthGuard(props: { children?: InfernoNode }) { + if (!UserService.Instance.myUserInfo) { + return <Redirect to="/" />; + } else { + return <>{props.children}</>; + } +} + +export default AuthGuard; diff --git a/src/shared/components/common/error-guard.tsx b/src/shared/components/common/error-guard.tsx new file mode 100644 index 0000000..791fe47 --- /dev/null +++ b/src/shared/components/common/error-guard.tsx @@ -0,0 +1,25 @@ +import { Component } from "inferno"; +import { ErrorPageData, setIsoData } from "../../utils"; +import { ErrorPage } from "../app/error-page"; + +class ErrorGuard extends Component<any, any> { + private isoData = setIsoData(this.context); + + constructor(props: any, context: any) { + super(props, context); + } + + render() { + const errorPageData = this.isoData.routeData[0] as + | ErrorPageData + | undefined; + + if (errorPageData?.type === "error") { + return <ErrorPage />; + } else { + return this.props.children; + } + } +} + +export default ErrorGuard; diff --git a/src/shared/utils.ts b/src/shared/utils.ts index 821d22f..30c89b5 100644 --- a/src/shared/utils.ts +++ b/src/shared/utils.ts @@ -105,6 +105,12 @@ export type ThemeColor = | "gray" | "gray-dark"; +export interface ErrorPageData { + type: "error"; + error?: Error; + adminMatrixIds?: string[]; +} + let customEmojis: EmojiMartCategory[] = []; export let customEmojisLookup: Map<string, CustomEmojiView> = new Map< string, diff --git a/src/shared/version.ts b/src/shared/version.ts index c1dba35..149d272 100644 --- a/src/shared/version.ts +++ b/src/shared/version.ts @@ -1 +1 @@ -export const VERSION = "unknown version"; +export const VERSION = "unknown version" as string; -- 2.44.1