mirror of
https://github.com/LemmyNet/lemmy-ui.git
synced 2024-12-23 11:21:26 +00:00
Set up logic for handling errors
This commit is contained in:
parent
ab3fed3ddf
commit
bcee6aad5b
8 changed files with 163 additions and 56 deletions
|
@ -19,7 +19,14 @@ import {
|
||||||
IsoData,
|
IsoData,
|
||||||
} from "../shared/interfaces";
|
} from "../shared/interfaces";
|
||||||
import { routes } from "../shared/routes";
|
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 server = express();
|
||||||
const [hostname, port] = process.env["LEMMY_UI_HOST"]
|
const [hostname, port] = process.env["LEMMY_UI_HOST"]
|
||||||
|
@ -109,7 +116,6 @@ server.get("/css/themelist", async (_req, res) => {
|
||||||
server.get("/*", async (req, res) => {
|
server.get("/*", async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const activeRoute = routes.find(route => matchPath(req.path, route));
|
const activeRoute = routes.find(route => matchPath(req.path, route));
|
||||||
const context = {} as any;
|
|
||||||
let auth: string | undefined = IsomorphicCookie.load("jwt", req);
|
let auth: string | undefined = IsomorphicCookie.load("jwt", req);
|
||||||
|
|
||||||
const getSiteForm: GetSite = { auth };
|
const getSiteForm: GetSite = { auth };
|
||||||
|
@ -119,6 +125,8 @@ server.get("/*", async (req, res) => {
|
||||||
const headers = setForwardedHeaders(req.headers);
|
const headers = setForwardedHeaders(req.headers);
|
||||||
const client = new LemmyHttp(getHttpBaseInternal(), headers);
|
const client = new LemmyHttp(getHttpBaseInternal(), headers);
|
||||||
|
|
||||||
|
const { path, url, query } = req;
|
||||||
|
|
||||||
// Get site data first
|
// Get site data first
|
||||||
// This bypasses errors, so that the client can hit the error on its own,
|
// 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
|
// in order to remove the jwt on the browser. Necessary for wrong jwts
|
||||||
|
@ -131,14 +139,18 @@ server.get("/*", async (req, res) => {
|
||||||
auth = undefined;
|
auth = undefined;
|
||||||
try_site = await client.getSite(getSiteForm);
|
try_site = await client.getSite(getSiteForm);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!auth && isAuthPath(path)) {
|
||||||
|
res.redirect("/");
|
||||||
|
}
|
||||||
const site: GetSiteResponse = try_site;
|
const site: GetSiteResponse = try_site;
|
||||||
initializeSite(site);
|
initializeSite(site);
|
||||||
|
|
||||||
const initialFetchReq: InitialFetchRequest = {
|
const initialFetchReq: InitialFetchRequest = {
|
||||||
client,
|
client,
|
||||||
auth,
|
auth,
|
||||||
path: req.path,
|
path,
|
||||||
query: req.query,
|
query,
|
||||||
site,
|
site,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -146,7 +158,7 @@ server.get("/*", async (req, res) => {
|
||||||
promises.push(...activeRoute.fetchInitialData(initialFetchReq));
|
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
|
// Redirect to the 404 if there's an API error
|
||||||
if (routeData[0] && routeData[0].error) {
|
if (routeData[0] && routeData[0].error) {
|
||||||
|
@ -155,24 +167,36 @@ server.get("/*", async (req, res) => {
|
||||||
if (error === "instance_is_private") {
|
if (error === "instance_is_private") {
|
||||||
return res.redirect(`/signup`);
|
return res.redirect(`/signup`);
|
||||||
} else {
|
} 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 = {
|
const isoData: IsoData = {
|
||||||
path: req.path,
|
path,
|
||||||
site_res: site,
|
site_res: site,
|
||||||
routeData,
|
routeData,
|
||||||
};
|
};
|
||||||
|
|
||||||
const wrapper = (
|
const wrapper = (
|
||||||
<StaticRouter location={req.url} context={isoData}>
|
<StaticRouter location={url} context={isoData}>
|
||||||
<App />
|
<App />
|
||||||
</StaticRouter>
|
</StaticRouter>
|
||||||
);
|
);
|
||||||
if (context.url) {
|
|
||||||
return res.redirect(context.url);
|
|
||||||
}
|
|
||||||
|
|
||||||
const eruda = (
|
const eruda = (
|
||||||
<>
|
<>
|
||||||
|
@ -260,7 +284,8 @@ server.get("/*", async (req, res) => {
|
||||||
`);
|
`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(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);
|
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 iconSizes = [72, 96, 128, 144, 152, 192, 384, 512];
|
||||||
const defaultLogoPathDirectory = path.join(
|
const defaultLogoPathDirectory = path.join(
|
||||||
process.cwd(),
|
process.cwd(),
|
||||||
|
|
|
@ -3,10 +3,12 @@ import { Provider } from "inferno-i18next-dess";
|
||||||
import { Route, Switch } from "inferno-router";
|
import { Route, Switch } from "inferno-router";
|
||||||
import { i18n } from "../../i18next";
|
import { i18n } from "../../i18next";
|
||||||
import { routes } from "../../routes";
|
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 { Footer } from "./footer";
|
||||||
import { Navbar } from "./navbar";
|
import { Navbar } from "./navbar";
|
||||||
import { NoMatch } from "./no-match";
|
|
||||||
import "./styles.scss";
|
import "./styles.scss";
|
||||||
import { Theme } from "./theme";
|
import { Theme } from "./theme";
|
||||||
|
|
||||||
|
@ -16,8 +18,8 @@ export class App extends Component<any, any> {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
}
|
}
|
||||||
render() {
|
render() {
|
||||||
let siteRes = this.isoData.site_res;
|
const siteRes = this.isoData.site_res;
|
||||||
let siteView = siteRes.site_view;
|
const siteView = siteRes.site_view;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -27,10 +29,26 @@ export class App extends Component<any, any> {
|
||||||
<Navbar siteRes={siteRes} />
|
<Navbar siteRes={siteRes} />
|
||||||
<div className="mt-4 p-0 fl-1">
|
<div className="mt-4 p-0 fl-1">
|
||||||
<Switch>
|
<Switch>
|
||||||
{routes.map(({ path, component }) => (
|
{routes.map(({ path, component: RouteComponent }) => (
|
||||||
<Route key={path} path={path} exact component={component} />
|
<Route
|
||||||
|
key={path}
|
||||||
|
path={path}
|
||||||
|
exact
|
||||||
|
component={routeProps => (
|
||||||
|
<ErrorGuard>
|
||||||
|
{RouteComponent &&
|
||||||
|
(isAuthPath(path ?? "") ? (
|
||||||
|
<AuthGuard>
|
||||||
|
<RouteComponent {...routeProps} />
|
||||||
|
</AuthGuard>
|
||||||
|
) : (
|
||||||
|
<RouteComponent {...routeProps} />
|
||||||
))}
|
))}
|
||||||
<Route component={NoMatch} />
|
</ErrorGuard>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
<Route component={ErrorPage} />
|
||||||
</Switch>
|
</Switch>
|
||||||
</div>
|
</div>
|
||||||
<Footer site={siteRes} />
|
<Footer site={siteRes} />
|
||||||
|
|
56
src/shared/components/app/error-page.tsx
Normal file
56
src/shared/components/app/error-page.tsx
Normal file
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
13
src/shared/components/common/auth-guard.tsx
Normal file
13
src/shared/components/common/auth-guard.tsx
Normal file
|
@ -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;
|
25
src/shared/components/common/error-guard.tsx
Normal file
25
src/shared/components/common/error-guard.tsx
Normal file
|
@ -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;
|
|
@ -105,6 +105,12 @@ export type ThemeColor =
|
||||||
| "gray"
|
| "gray"
|
||||||
| "gray-dark";
|
| "gray-dark";
|
||||||
|
|
||||||
|
export interface ErrorPageData {
|
||||||
|
type: "error";
|
||||||
|
error?: Error;
|
||||||
|
adminMatrixIds?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
let customEmojis: EmojiMartCategory[] = [];
|
let customEmojis: EmojiMartCategory[] = [];
|
||||||
export let customEmojisLookup: Map<string, CustomEmojiView> = new Map<
|
export let customEmojisLookup: Map<string, CustomEmojiView> = new Map<
|
||||||
string,
|
string,
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
export const VERSION = "unknown version";
|
export const VERSION = "unknown version" as string;
|
||||||
|
|
Loading…
Reference in a new issue