From: abias Date: Tue, 23 May 2023 23:14:44 +0000 (-0400) Subject: Merge branch 'main' into rate-limiting-tab X-Git-Url: http://these/git/?a=commitdiff_plain;h=7face53efd85e7f62fad4734becc26aade39e090;hp=f51cc697a7a8c4791bae2bdfd2f8b597dcc9bba9;p=lemmy-ui.git Merge branch 'main' into rate-limiting-tab --- diff --git a/.babelrc b/.babelrc index 2da0dea..b96976f 100644 --- a/.babelrc +++ b/.babelrc @@ -10,11 +10,11 @@ } } ], - ["@babel/typescript", {"isTSX": true, "allExtensions": true}] + ["@babel/typescript", { "isTSX": true, "allExtensions": true }] ], "plugins": [ "@babel/plugin-transform-runtime", - ["babel-plugin-inferno", { "imports": true }], - ["@babel/plugin-proposal-class-properties", { "loose": true }], + ["babel-plugin-inferno", { "imports": true }], + ["@babel/plugin-proposal-class-properties", { "loose": true }] ] } diff --git a/.eslintignore b/.eslintignore index 439fa03..9e14b5a 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,3 +1,7 @@ generate_translations.js webpack.config.js src/api_tests +**/*.png +**/*.css +**/*.scss +**/*.svg diff --git a/.eslintrc.json b/.eslintrc.json index 0c9a5f4..cc1bff1 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -3,9 +3,7 @@ "env": { "browser": true }, - "plugins": [ - "@typescript-eslint" - ], + "plugins": ["@typescript-eslint"], "extends": [ "eslint:recommended", "plugin:@typescript-eslint/recommended", diff --git a/.github/ISSUE_TEMPLATE/BUG_REPORT.md b/.github/ISSUE_TEMPLATE/BUG_REPORT.md index 83c2ffa..69b116f 100644 --- a/.github/ISSUE_TEMPLATE/BUG_REPORT.md +++ b/.github/ISSUE_TEMPLATE/BUG_REPORT.md @@ -1,10 +1,9 @@ --- name: "\U0001F41E Bug Report" about: Create a report to help us improve Lemmy -title: '' +title: "" labels: bug -assignees: '' - +assignees: "" --- Found a bug? Please fill out the sections below. 👍 @@ -15,7 +14,6 @@ For backend issues, use [lemmy](https://github.com/LemmyNet/lemmy) A summary of the bug. - ### Steps to Reproduce 1. (for example) I clicked login, and an endless spinner show up. @@ -24,6 +22,6 @@ A summary of the bug. ### Technical details -* Please post your log: `sudo docker-compose logs > lemmy_log.out`. -* What OS are you trying to install lemmy on? -* Any browser console errors? +- Please post your log: `sudo docker-compose logs > lemmy_log.out`. +- What OS are you trying to install lemmy on? +- Any browser console errors? diff --git a/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md b/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md index 9886d8a..bfeca29 100644 --- a/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md +++ b/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md @@ -1,10 +1,9 @@ --- name: "\U0001F680 Feature request" about: Suggest an idea for improving Lemmy -title: '' +title: "" labels: enhancement -assignees: '' - +assignees: "" --- For backend issues, use [lemmy](https://github.com/LemmyNet/lemmy) diff --git a/.github/ISSUE_TEMPLATE/QUESTION.md b/.github/ISSUE_TEMPLATE/QUESTION.md index b45f8f1..1532587 100644 --- a/.github/ISSUE_TEMPLATE/QUESTION.md +++ b/.github/ISSUE_TEMPLATE/QUESTION.md @@ -1,10 +1,9 @@ --- name: "? Question" about: General questions about Lemmy -title: '' +title: "" labels: question -assignees: '' - +assignees: "" --- What's the question you have about lemmy? diff --git a/.github/ISSUE_TEMPLATE/hexbear.md b/.github/ISSUE_TEMPLATE/hexbear.md index 3bb0623..65483df 100644 --- a/.github/ISSUE_TEMPLATE/hexbear.md +++ b/.github/ISSUE_TEMPLATE/hexbear.md @@ -1,10 +1,9 @@ --- name: Hexbear about: For hexbear issues -title: '' +title: "" labels: hexbear -assignees: '' - +assignees: "" --- For hexbear-related issues diff --git a/.prettierignore b/.prettierignore index a14ae90..e7a0d20 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1 +1,2 @@ -src/shared/translations \ No newline at end of file +src/shared/translations +lemmy-translations \ No newline at end of file diff --git a/.woodpecker.yml b/.woodpecker.yml index d9e3fa7..8d3c6f1 100644 --- a/.woodpecker.yml +++ b/.woodpecker.yml @@ -69,7 +69,7 @@ pipeline: publish_release_docker_manifest: image: plugins/manifest - settings: + settings: username: from_secret: docker_username password: @@ -85,7 +85,7 @@ pipeline: publish_latest_release_docker_manifest: image: plugins/manifest - settings: + settings: username: from_secret: docker_username password: diff --git a/README.md b/README.md index e1e6e1f..6c9ef63 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,21 @@ -# lemmy-ui - -The official web app for [Lemmy](https://github.com/LemmyNet/lemmy), written in inferno. - -Based off of MrFoxPro's [inferno-isomorphic-template](https://github.com/MrFoxPro/inferno-isomorphic-template). - -## Configuration - -The following environment variables can be used to configure lemmy-ui: - -`ENV_VAR` | type | default | description ---- | --- | --- | --- -`LEMMY_UI_HOST` | `string` | `0.0.0.0:1234` | The IP / port that the lemmy-ui isomorphic node server is hosted at. -`LEMMY_UI_LEMMY_INTERNAL_HOST` | `string` | `0.0.0.0:8536` | The internal IP / port that lemmy is hosted at. Often `lemmy:8536` if using docker. -`LEMMY_UI_LEMMY_EXTERNAL_HOST` | `string` | `0.0.0.0:8536` | The external IP / port that lemmy is hosted at. Often `DOMAIN.TLD`. -`LEMMY_UI_LEMMY_WS_HOST` | `string` | `0.0.0.0:8536` | An alternate location for lemmy's websocket address. Not usually necessary. -`LEMMY_UI_HTTPS` | `bool` | `false` | Whether to use https. -`LEMMY_UI_EXTRA_THEMES_FOLDER` | `string` | `./extra_themes` | A location for additional lemmy css themes. -`LEMMY_UI_DEBUG` | `bool` | `false` | Loads the [Eruda](https://github.com/liriliri/eruda) debugging utility. -`LEMMY_UI_DISABLE_CSP` | `bool` | `false` | Disables CSP security headers -`LEMMY_UI_CUSTOM_HTML_HEADER` | `string` | | Injects a custom script into ``. +# lemmy-ui + +The official web app for [Lemmy](https://github.com/LemmyNet/lemmy), written in inferno. + +Based off of MrFoxPro's [inferno-isomorphic-template](https://github.com/MrFoxPro/inferno-isomorphic-template). + +## Configuration + +The following environment variables can be used to configure lemmy-ui: + +| `ENV_VAR` | type | default | description | +| ------------------------------ | -------- | ---------------- | ----------------------------------------------------------------------------------- | +| `LEMMY_UI_HOST` | `string` | `0.0.0.0:1234` | The IP / port that the lemmy-ui isomorphic node server is hosted at. | +| `LEMMY_UI_LEMMY_INTERNAL_HOST` | `string` | `0.0.0.0:8536` | The internal IP / port that lemmy is hosted at. Often `lemmy:8536` if using docker. | +| `LEMMY_UI_LEMMY_EXTERNAL_HOST` | `string` | `0.0.0.0:8536` | The external IP / port that lemmy is hosted at. Often `DOMAIN.TLD`. | +| `LEMMY_UI_LEMMY_WS_HOST` | `string` | `0.0.0.0:8536` | An alternate location for lemmy's websocket address. Not usually necessary. | +| `LEMMY_UI_HTTPS` | `bool` | `false` | Whether to use https. | +| `LEMMY_UI_EXTRA_THEMES_FOLDER` | `string` | `./extra_themes` | A location for additional lemmy css themes. | +| `LEMMY_UI_DEBUG` | `bool` | `false` | Loads the [Eruda](https://github.com/liriliri/eruda) debugging utility. | +| `LEMMY_UI_DISABLE_CSP` | `bool` | `false` | Disables CSP security headers | +| `LEMMY_UI_CUSTOM_HTML_HEADER` | `string` | | Injects a custom script into ``. | diff --git a/package.json b/package.json index 2f16b8d..43b8883 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "build:prod": "webpack --mode=production", "clean": "yarn run rimraf dist", "dev": "yarn start", - "lint": "node generate_translations.js && tsc --noEmit && eslint --report-unused-disable-directives --ext .js,.ts,.tsx src && prettier --check \"src/**/*.{ts,tsx,js,css,scss}\"", + "lint": "node generate_translations.js && tsc --noEmit && eslint --report-unused-disable-directives --ext .js,.ts,.tsx \"src/**\" && prettier --check \"src/**/*.{ts,tsx,js,css,scss}\"", "prepare": "husky install", "start": "yarn build:dev --watch" }, @@ -61,6 +61,7 @@ "isomorphic-cookie": "^1.2.4", "jwt-decode": "^3.1.2", "lemmy-js-client": "0.17.2-rc.17", + "lodash": "^4.17.21", "markdown-it": "^13.0.1", "markdown-it-container": "^3.0.0", "markdown-it-emoji": "^2.0.2", @@ -77,6 +78,7 @@ "sass": "^1.62.1", "sass-loader": "^13.2.2", "serialize-javascript": "^6.0.1", + "service-worker-webpack": "^1.0.0", "sharp": "^0.32.1", "tippy.js": "^6.3.7", "toastify-js": "^1.12.0", @@ -89,6 +91,7 @@ "devDependencies": { "@babel/core": "^7.21.8", "@types/autosize": "^4.0.0", + "@types/bootstrap": "^5.2.6", "@types/express": "^4.17.17", "@types/html-to-text": "^9.0.0", "@types/markdown-it": "^12.2.3", @@ -111,7 +114,6 @@ "prettier-plugin-organize-imports": "^3.2.2", "prettier-plugin-packagejson": "^2.4.3", "rimraf": "^5.0.0", - "service-worker-webpack": "^1.0.0", "sortpack": "^2.3.4", "style-loader": "^3.3.2", "terser": "^5.17.3", diff --git a/src/assets/css/main.css b/src/assets/css/main.css index 9cff2c7..a0d7239 100644 --- a/src/assets/css/main.css +++ b/src/assets/css/main.css @@ -1,7 +1,3 @@ -.navbar-toggler { - border: 0px; -} - .navbar-expand-lg .navbar-nav .nav-link { padding-right: 0.75rem !important; padding-left: 0.75rem !important; @@ -201,14 +197,6 @@ } } -.dropdown-content { - position: absolute; - background-color: var(--light); - min-width: 160px; - box-shadow: 0px 8px 16px 0px rgba(0, 0, 0, 0.2); - z-index: 2000; -} - blockquote { border-left: 2px solid var(--secondary); margin: 0.5em 5px; diff --git a/src/client/index.tsx b/src/client/index.tsx index d3a5b62..99f1237 100644 --- a/src/client/index.tsx +++ b/src/client/index.tsx @@ -3,6 +3,7 @@ import { BrowserRouter } from "inferno-router"; import { App } from "../shared/components/app/app"; import { initializeSite } from "../shared/utils"; +import "bootstrap/js/dist/collapse"; import "bootstrap/js/dist/dropdown"; const site = window.isoData.site_res; diff --git a/src/server/index.tsx b/src/server/index.tsx index 05988cf..a93595e 100644 --- a/src/server/index.tsx +++ b/src/server/index.tsx @@ -16,10 +16,16 @@ import { getHttpBase, getHttpBaseInternal } from "../shared/env"; import { ILemmyConfig, InitialFetchRequest, - IsoData, + IsoDataOptionalSite, } from "../shared/interfaces"; import { routes } from "../shared/routes"; -import { favIconPngUrl, favIconUrl, initializeSite } from "../shared/utils"; +import { + ErrorPageData, + favIconPngUrl, + favIconUrl, + initializeSite, + isAuthPath, +} from "../shared/utils"; const server = express(); const [hostname, port] = process.env["LEMMY_UI_HOST"] @@ -70,6 +76,7 @@ server.get("/css/themes/:name", async (req, res) => { res.contentType("text/css"); const theme = req.params.name; if (!theme.endsWith(".css")) { + res.statusCode = 400; res.send("Theme must be a css file"); } @@ -109,44 +116,58 @@ 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 }; - const promises: Promise[] = []; - 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 - let try_site: any = await client.getSite(getSiteForm); - if (try_site.error == "not_logged_in") { - console.error( - "Incorrect JWT token, skipping auth so frontend can remove jwt cookie" - ); - getSiteForm.auth = undefined; - auth = undefined; - try_site = await client.getSite(getSiteForm); - } - const site: GetSiteResponse = try_site; - initializeSite(site); - - const initialFetchReq: InitialFetchRequest = { - client, - auth, - path: req.path, - query: req.query, - site, - }; + let site: GetSiteResponse | undefined = undefined; + let routeData: any[] = []; + let errorPageData: ErrorPageData | undefined; + try { + let try_site: any = await client.getSite(getSiteForm); + if (try_site.error == "not_logged_in") { + console.error( + "Incorrect JWT token, skipping auth so frontend can remove jwt cookie" + ); + getSiteForm.auth = undefined; + auth = undefined; + try_site = await client.getSite(getSiteForm); + } - if (activeRoute?.fetchInitialData) { - promises.push(...activeRoute.fetchInitialData(initialFetchReq)); - } + if (!auth && isAuthPath(path)) { + res.redirect("/login"); + return; + } - const routeData = await Promise.all(promises); + site = try_site; + initializeSite(site); + + if (site) { + const initialFetchReq: InitialFetchRequest = { + client, + auth, + path, + query, + site, + }; + + if (activeRoute?.fetchInitialData) { + routeData = await Promise.all([ + ...activeRoute.fetchInitialData(initialFetchReq), + ]); + } + } + } catch (error) { + errorPageData = getErrorPageData(error, site); + } // Redirect to the 404 if there's an API error if (routeData[0] && routeData[0].error) { @@ -155,112 +176,33 @@ server.get("/*", async (req, res) => { if (error === "instance_is_private") { return res.redirect(`/signup`); } else { - return res.send(`404: ${removeAuthParam(error)}`); + errorPageData = getErrorPageData(error, site); } } - const isoData: IsoData = { - path: req.path, + const isoData: IsoDataOptionalSite = { + path, site_res: site, routeData, + errorPageData, }; const wrapper = ( - + ); - if (context.url) { - return res.redirect(context.url); - } - - const eruda = ( - <> - - - - ); - const erudaStr = process.env["LEMMY_UI_DEBUG"] ? renderToString(eruda) : ""; const root = renderToString(wrapper); - const helmet = Helmet.renderStatic(); - - const config: ILemmyConfig = { wsHost: process.env.LEMMY_UI_LEMMY_WS_HOST }; - - const appleTouchIcon = site.site_view.site.icon - ? `data:image/png;base64,${sharp( - await fetchIconPng(site.site_view.site.icon) - ) - .resize(180, 180) - .extend({ - bottom: 20, - top: 20, - left: 20, - right: 20, - background: "#222222", - }) - .png() - .toBuffer() - .then(buf => buf.toString("base64"))}` - : favIconPngUrl; - - res.send(` - - - - - - - - ${erudaStr} - - - ${customHtmlHeader} - - ${helmet.title.toString()} - ${helmet.meta.toString()} - - - - - - - - - - - - - - - - - ${helmet.link.toString()} - - - - - - -
${root}
- - - -`); + + res.send(await createSsrHtml(root, isoData)); } catch (err) { + // If an error is caught here, the error page couldn't even be rendered console.error(err); - return res.send(`404: ${removeAuthParam(err)}`); + res.statusCode = 500; + return res.send( + process.env.NODE_ENV === "development" ? err.message : "Server error" + ); } }); @@ -292,16 +234,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(), @@ -356,8 +288,119 @@ export async function generateManifestBase64(site: Site) { async function fetchIconPng(iconUrl: string) { return await fetch( - iconUrl.replace(/https?:\/\/localhost:\d+/g, getHttpBaseInternal()) + iconUrl.replace(/https?:\/\/[^\/]+/g, getHttpBaseInternal()) ) .then(res => res.blob()) .then(blob => blob.arrayBuffer()); } + +function getErrorPageData(error: Error, site?: GetSiteResponse) { + const errorPageData: ErrorPageData = {}; + + if (site) { + errorPageData.error = error.message; + } + + const adminMatrixIds = site?.admins + .map(({ person: { matrix_user_id } }) => matrix_user_id) + .filter(id => id) as string[] | undefined; + if (adminMatrixIds && adminMatrixIds.length > 0) { + errorPageData.adminMatrixIds = adminMatrixIds; + } + + return errorPageData; +} + +async function createSsrHtml(root: string, isoData: IsoDataOptionalSite) { + const site = isoData.site_res; + const appleTouchIcon = site?.site_view.site.icon + ? `data:image/png;base64,${sharp( + await fetchIconPng(site.site_view.site.icon) + ) + .resize(180, 180) + .extend({ + bottom: 20, + top: 20, + left: 20, + right: 20, + background: "#222222", + }) + .png() + .toBuffer() + .then(buf => buf.toString("base64"))}` + : favIconPngUrl; + + const eruda = ( + <> + + + + ); + + const erudaStr = process.env["LEMMY_UI_DEBUG"] ? renderToString(eruda) : ""; + + const helmet = Helmet.renderStatic(); + + const config: ILemmyConfig = { wsHost: process.env.LEMMY_UI_LEMMY_WS_HOST }; + + return ` + + + + + + + + ${erudaStr} + + + ${customHtmlHeader} + + ${helmet.title.toString()} + ${helmet.meta.toString()} + + + + + + + + + ${ + site && + `` + } + + + + + + + + ${helmet.link.toString()} + + + + + + +
${root}
+ + + +`; +} diff --git a/src/shared/components/app/app.tsx b/src/shared/components/app/app.tsx index 624d6e5..9e6e9bd 100644 --- a/src/shared/components/app/app.tsx +++ b/src/shared/components/app/app.tsx @@ -1,36 +1,57 @@ import { Component } from "inferno"; import { Provider } from "inferno-i18next-dess"; import { Route, Switch } from "inferno-router"; +import { IsoDataOptionalSite } from "shared/interfaces"; 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"; export class App extends Component { - private isoData = setIsoData(this.context); + private isoData: IsoDataOptionalSite = setIsoData(this.context); constructor(props: any, context: 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 ( <>
- + {siteView && ( + + )}
- {routes.map(({ path, component }) => ( - + {routes.map(({ path, component: RouteComponent }) => ( + ( + + {RouteComponent && + (isAuthPath(path ?? "") ? ( + + + + ) : ( + + ))} + + )} + /> ))} - +