diff --git a/.prettierignore b/.prettierignore index 004c815..c6145fd 100644 --- a/.prettierignore +++ b/.prettierignore @@ -2,3 +2,4 @@ src/shared/translations lemmy-translations src/assets/css/themes/*.css stats.json +dist diff --git a/.woodpecker.yml b/.woodpecker.yml index 656903a..a55d39a 100644 --- a/.woodpecker.yml +++ b/.woodpecker.yml @@ -32,3 +32,14 @@ pipeline: auto_tag: true when: event: tag + + nightly_build: + image: woodpeckerci/plugin-docker-buildx + secrets: [docker_username, docker_password] + settings: + repo: dessalines/lemmy-ui + dockerfile: Dockerfile + platforms: linux/amd64 + tag: dev + when: + event: cron diff --git a/Dockerfile b/Dockerfile index 2b36581..92b3f7e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -27,7 +27,7 @@ COPY .git .git RUN echo "export const VERSION = '$(git describe --tag)';" > "src/shared/version.ts" RUN yarn --production --prefer-offline -RUN yarn build:prod +RUN NODE_OPTIONS="--max-old-space-size=8192" yarn build:prod # Prune the image RUN node-prune /usr/src/app/node_modules diff --git a/dev.dockerfile b/dev.dockerfile index 3bfc10d..881d9bc 100644 --- a/dev.dockerfile +++ b/dev.dockerfile @@ -20,6 +20,7 @@ COPY generate_translations.js \ COPY lemmy-translations lemmy-translations COPY src src +COPY .git .git # Set UI version RUN echo "export const VERSION = 'dev';" > "src/shared/version.ts" diff --git a/package.json b/package.json index 7eab4cb..9acaba4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "lemmy-ui", - "version": "0.18.1-rc.1", + "version": "0.18.1-rc.5", "description": "An isomorphic UI for lemmy", "repository": "https://github.com/LemmyNet/lemmy-ui", "license": "AGPL-3.0", @@ -8,9 +8,9 @@ "scripts": { "analyze": "webpack --mode=none", "prebuild:dev": "yarn clean && node generate_translations.js", - "build:dev": "webpack --mode=development", + "build:dev": "webpack --env COMMIT_HASH=$(git rev-parse --short HEAD) --mode=development", "prebuild:prod": "yarn clean && node generate_translations.js", - "build:prod": "webpack --mode=production", + "build:prod": "webpack --env COMMIT_HASH=$(git rev-parse --short HEAD) --mode=production", "clean": "yarn run rimraf dist", "dev": "yarn build:dev --watch", "lint": "yarn translations:generate && tsc --noEmit && eslint --report-unused-disable-directives --ext .js,.ts,.tsx \"src/**\" && prettier --check \"src/**/*.{ts,tsx,js,css,scss}\"", diff --git a/src/assets/css/main.css b/src/assets/css/main.css index 63c1b47..e5c163b 100644 --- a/src/assets/css/main.css +++ b/src/assets/css/main.css @@ -198,9 +198,9 @@ blockquote { .thumbnail { object-fit: cover; - aspect-ratio: 4/3; - width: 100%; - max-height: 6rem; + aspect-ratio: 1/1; + width: 5rem; + height: 5rem; } .thumbnail svg { diff --git a/src/server/handlers/catch-all-handler.tsx b/src/server/handlers/catch-all-handler.tsx index f22b8a1..d485429 100644 --- a/src/server/handlers/catch-all-handler.tsx +++ b/src/server/handlers/catch-all-handler.tsx @@ -75,7 +75,12 @@ export default async (req: Request, res: Response) => { routeData = await activeRoute.fetchInitialData(initialFetchReq); } + + if (!activeRoute) { + res.status(404); + } } else if (try_site.state === "failed") { + res.status(500); errorPageData = getErrorPageData(new Error(try_site.msg), site); } @@ -89,6 +94,7 @@ export default async (req: Request, res: Response) => { if (error.msg === "instance_is_private") { return res.redirect(`/signup`); } else { + res.status(500); errorPageData = getErrorPageData(new Error(error.msg), site); } } diff --git a/src/server/handlers/security-handler.ts b/src/server/handlers/security-handler.ts new file mode 100644 index 0000000..0aed0cd --- /dev/null +++ b/src/server/handlers/security-handler.ts @@ -0,0 +1,17 @@ +import type { Response } from "express"; + +export default async ({ res }: { res: Response }) => { + res.setHeader("content-type", "text/plain; charset=utf-8"); + + res.send( + `Contact: mailto:security@lemmy.ml + Contact: mailto:admin@` + + process.env.LEMMY_UI_LEMMY_EXTERNAL_HOST + + ` + Contact: mailto:security@` + + process.env.LEMMY_UI_LEMMY_EXTERNAL_HOST + + ` + Expires: 2024-01-01T04:59:00.000Z + ` + ); +}; diff --git a/src/server/index.tsx b/src/server/index.tsx index aed8bca..458d7f0 100644 --- a/src/server/index.tsx +++ b/src/server/index.tsx @@ -1,10 +1,12 @@ import { setupDateFns } from "@utils/app"; +import { getStaticDir } from "@utils/env"; import express from "express"; import path from "path"; import process from "process"; import CatchAllHandler from "./handlers/catch-all-handler"; import ManifestHandler from "./handlers/manifest-handler"; import RobotsHandler from "./handlers/robots-handler"; +import SecurityHandler from "./handlers/security-handler"; import ServiceWorkerHandler from "./handlers/service-worker-handler"; import ThemeHandler from "./handlers/theme-handler"; import ThemesListHandler from "./handlers/themes-list-handler"; @@ -18,13 +20,20 @@ const [hostname, port] = process.env["LEMMY_UI_HOST"] server.use(express.json()); server.use(express.urlencoded({ extended: false })); -server.use("/static", express.static(path.resolve("./dist"))); +server.use( + getStaticDir(), + express.static(path.resolve("./dist"), { + maxAge: 24 * 60 * 60 * 1000, // 1 day + immutable: true, + }) +); server.use(setCacheControl); if (!process.env["LEMMY_UI_DISABLE_CSP"] && !process.env["LEMMY_UI_DEBUG"]) { server.use(setDefaultCsp); } +server.get("/.well-known/security.txt", SecurityHandler); server.get("/robots.txt", RobotsHandler); server.get("/service-worker.js", ServiceWorkerHandler); server.get("/manifest.webmanifest", ManifestHandler); diff --git a/src/server/middleware.ts b/src/server/middleware.ts index a7cc6f2..235f072 100644 --- a/src/server/middleware.ts +++ b/src/server/middleware.ts @@ -1,4 +1,4 @@ -import type { NextFunction, Response } from "express"; +import type { NextFunction, Request, Response } from "express"; import { UserService } from "../shared/services"; export function setDefaultCsp({ @@ -10,7 +10,7 @@ export function setDefaultCsp({ }) { res.setHeader( "Content-Security-Policy", - `default-src 'self'; manifest-src *; connect-src *; img-src * data:; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; form-action 'self'; base-uri 'self'; frame-src *; media-src *` + `default-src 'self'; manifest-src *; connect-src *; img-src * data:; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; form-action 'self'; base-uri 'self'; frame-src *; media-src * data:` ); next(); @@ -18,24 +18,33 @@ export function setDefaultCsp({ // Set cache-control headers. If user is logged in, set `private` to prevent storing data in // shared caches (eg nginx) and leaking of private data. If user is not logged in, allow caching -// all responses for 60 seconds to reduce load on backend and database. The specific cache +// all responses for 5 seconds to reduce load on backend and database. The specific cache // interval is rather arbitrary and could be set higher (less server load) or lower (fresher data). // // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control -export function setCacheControl({ - res, - next, -}: { - res: Response; - next: NextFunction; -}) { +export function setCacheControl( + req: Request, + res: Response, + next: NextFunction +) { const user = UserService.Instance; - let caching; - if (user.auth()) { - caching = "private"; + let caching: string; + + if ( + process.env.NODE_ENV === "production" && + (req.path.match(/\.(js|css|txt|manifest\.webmanifest)\/?$/) || + req.path.includes("/css/themelist")) + ) { + // Static content gets cached publicly for a day + caching = "public, max-age=86400"; } else { - caching = "public, max-age=60"; + if (user.auth()) { + caching = "private"; + } else { + caching = "public, max-age=5"; + } } + res.setHeader("Cache-Control", caching); next(); diff --git a/src/server/utils/create-ssr-html.tsx b/src/server/utils/create-ssr-html.tsx index 1377598..ba85228 100644 --- a/src/server/utils/create-ssr-html.tsx +++ b/src/server/utils/create-ssr-html.tsx @@ -1,3 +1,4 @@ +import { getStaticDir } from "@utils/env"; import { Helmet } from "inferno-helmet"; import { renderToString } from "inferno-server"; import serialize from "serialize-javascript"; @@ -23,7 +24,7 @@ export async function createSsrHtml( if (!appleTouchIcon) { appleTouchIcon = site?.site_view.site.icon - ? `data:image/png;base64,${sharp( + ? `data:image/png;base64,${await sharp( await fetchIconPng(site.site_view.site.icon) ) .resize(180, 180) @@ -87,7 +88,7 @@ export async function createSsrHtml( - + ${helmet.link.toString() || fallbackTheme} @@ -102,7 +103,7 @@ export async function createSsrHtml(