From bcee6aad5b85b81f6fc7ad5680533d1a443ddb01 Mon Sep 17 00:00:00 2001 From: abias 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 05988cf7..80882887 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 = ( - + ); - 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 624d6e52..1251a5e4 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 { 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 {
- {routes.map(({ path, component }) => ( - + {routes.map(({ path, component: RouteComponent }) => ( + ( + + {RouteComponent && + (isAuthPath(path ?? "") ? ( + + + + ) : ( + + ))} + + )} + /> ))} - +