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}&apos;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