diff --git a/rest-api/db/migrations/20240825133549_adjust_upload_function.sql b/rest-api/db/migrations/20240825133549_adjust_upload_function.sql new file mode 100644 index 0000000..b15e618 --- /dev/null +++ b/rest-api/db/migrations/20240825133549_adjust_upload_function.sql @@ -0,0 +1,74 @@ +-- migrate:up +CREATE OR REPLACE FUNCTION api.upload_file (BYTEA, OUT file_id UUID) +AS $$ +DECLARE + _headers JSON := CURRENT_SETTING('request.headers', TRUE)::JSON; + _website_id UUID := (_headers ->> 'x-website-id')::UUID; + _mimetype TEXT := _headers ->> 'x-mimetype'; + _original_filename TEXT := _headers ->> 'x-original-filename'; + _allowed_mimetypes TEXT[] := ARRAY['image/png', 'image/jpeg', 'image/webp']; + _max_file_size INT := 5 * 1024 * 1024; +BEGIN + IF OCTET_LENGTH($1) = 0 THEN + RAISE invalid_parameter_value + USING message = 'No file data was provided'; + END IF; + IF _mimetype IS NULL OR _mimetype NOT IN ( + SELECT + UNNEST(_allowed_mimetypes)) THEN + RAISE invalid_parameter_value + USING message = 'Invalid MIME type. Allowed types are: png, jpg, webp'; + END IF; + IF OCTET_LENGTH($1) > _max_file_size THEN + RAISE program_limit_exceeded + USING message = FORMAT('File size exceeds the maximum limit of %s MB', _max_file_size / (1024 * 1024)); + END IF; + INSERT INTO internal.media (website_id, blob, mimetype, original_name) + VALUES (_website_id, $1, _mimetype, _original_filename) + RETURNING + id INTO file_id; +END; +$$ +LANGUAGE plpgsql +SECURITY DEFINER; + +GRANT EXECUTE ON FUNCTION api.upload_file (BYTEA) TO authenticated_user; + +-- migrate:down + +DROP FUNCTION api.upload_file(BYTEA); + +CREATE FUNCTION api.upload_file (BYTEA, OUT file_id UUID) +AS $$ +DECLARE + _headers JSON := CURRENT_SETTING('request.headers', TRUE)::JSON; + _website_id UUID := (_headers ->> 'x-website-id')::UUID; + _mimetype TEXT := _headers ->> 'x-mimetype'; + _original_filename TEXT := _headers ->> 'x-original-filename'; + _allowed_mimetypes TEXT[] := ARRAY['image/png', 'image/jpeg', 'image/webp']; + _max_file_size INT := 5 * 1024 * 1024; +BEGIN + IF OCTET_LENGTH($1) = 0 THEN + RAISE invalid_parameter_value + USING message = 'No file data was provided'; + END IF; + IF _mimetype IS NULL OR _mimetype NOT IN ( + SELECT + UNNEST(_allowed_mimetypes)) THEN + RAISE invalid_parameter_value + USING message = 'Invalid MIME type. Allowed types are: png, svg, jpg, webp'; + END IF; + IF OCTET_LENGTH($1) > _max_file_size THEN + RAISE program_limit_exceeded + USING message = FORMAT('File size exceeds the maximum limit of %s MB', _max_file_size / (1024 * 1024)); + END IF; + INSERT INTO internal.media (website_id, blob, mimetype, original_name) + VALUES (_website_id, $1, _mimetype, _original_filename) + RETURNING + id INTO file_id; +END; +$$ +LANGUAGE plpgsql +SECURITY DEFINER; + +GRANT EXECUTE ON FUNCTION api.upload_file (BYTEA) TO authenticated_user; \ No newline at end of file diff --git a/web-app/src/lib/templates/blog/BlogArticle.svelte b/web-app/src/lib/templates/blog/BlogArticle.svelte index 3ce2ebf..9e076d1 100644 --- a/web-app/src/lib/templates/blog/BlogArticle.svelte +++ b/web-app/src/lib/templates/blog/BlogArticle.svelte @@ -4,6 +4,7 @@ import BlogFooter from "./common/BlogFooter.svelte"; const { + favicon, title, logoType, logo, @@ -12,6 +13,7 @@ publicationDate, footerAdditionalText }: { + favicon: string; title: string; logoType: "text" | "image"; logo: string; @@ -22,7 +24,7 @@ } = $props(); - + diff --git a/web-app/src/lib/templates/blog/BlogIndex.svelte b/web-app/src/lib/templates/blog/BlogIndex.svelte index 5852c4a..9035ec9 100644 --- a/web-app/src/lib/templates/blog/BlogIndex.svelte +++ b/web-app/src/lib/templates/blog/BlogIndex.svelte @@ -4,6 +4,7 @@ import BlogFooter from "./common/BlogFooter.svelte"; const { + favicon, title, logoType, logo, @@ -11,6 +12,7 @@ articles, footerAdditionalText }: { + favicon: string; title: string; logoType: "text" | "image"; logo: string; @@ -20,7 +22,7 @@ } = $props(); - + diff --git a/web-app/src/lib/templates/blog/common/BlogHead.svelte b/web-app/src/lib/templates/blog/common/BlogHead.svelte index 353c445..c0dece4 100644 --- a/web-app/src/lib/templates/blog/common/BlogHead.svelte +++ b/web-app/src/lib/templates/blog/common/BlogHead.svelte @@ -1,5 +1,9 @@ @@ -8,5 +12,8 @@ {title} + {#if favicon} + + {/if} diff --git a/web-app/src/lib/templates/blog/common/BlogNav.svelte b/web-app/src/lib/templates/blog/common/BlogNav.svelte index 6542061..f1f7218 100644 --- a/web-app/src/lib/templates/blog/common/BlogNav.svelte +++ b/web-app/src/lib/templates/blog/common/BlogNav.svelte @@ -10,7 +10,7 @@ {logo}

{:else} - + {/if} diff --git a/web-app/src/routes/(authenticated)/website/[websiteId]/publish/+page.server.ts b/web-app/src/routes/(authenticated)/website/[websiteId]/publish/+page.server.ts index 3867897..814a4dc 100644 --- a/web-app/src/routes/(authenticated)/website/[websiteId]/publish/+page.server.ts +++ b/web-app/src/routes/(authenticated)/website/[websiteId]/publish/+page.server.ts @@ -57,9 +57,15 @@ const generateStaticFiles = async (websiteData: any, isPreview: boolean = true) { ({ head, body } = render(BlogIndex, { props: { + favicon: websiteData.favicon_image + ? `${API_BASE_PREFIX}/rpc/retrieve_file?id=${websiteData.favicon_image}` + : "", title: websiteData.title, logoType: websiteData.logo_type, - logo: websiteData.logo_text, + logo: + websiteData.logo_type === "text" + ? websiteData.logo_text + : `${API_BASE_PREFIX}/rpc/retrieve_file?id=${websiteData.logo_image}`, mainContent: md(websiteData.main_content ?? "", false), articles: websiteData.articles ?? [], footerAdditionalText: md(websiteData.additional_text ?? "") @@ -110,9 +116,15 @@ const generateStaticFiles = async (websiteData: any, isPreview: boolean = true) { ({ head, body } = render(BlogArticle, { props: { + favicon: websiteData.favicon_image + ? `${API_BASE_PREFIX}/rpc/retrieve_file?id=${websiteData.favicon_image}` + : "", title: article.title, logoType: websiteData.logo_type, - logo: websiteData.logo_text, + logo: + websiteData.logo_type === "text" + ? websiteData.logo_text + : `${API_BASE_PREFIX}/rpc/retrieve_file?id=${websiteData.logo_image}`, coverImage: article.cover_image ? `${API_BASE_PREFIX}/rpc/retrieve_file?id=${article.cover_image}` : "", @@ -160,5 +172,21 @@ const generateStaticFiles = async (websiteData: any, isPreview: boolean = true) const specificStyles = await readFile(`${process.cwd()}/template-styles/blog-styles.css`, { encoding: "utf-8" }); - await writeFile(join(uploadDir, "styles.css"), commonStyles.concat(specificStyles)); + await writeFile( + join(uploadDir, "styles.css"), + commonStyles + .concat(specificStyles) + .replace( + /--color-accent:\s*(.*?);/, + `--color-accent: ${websiteData.accent_color_dark_theme};` + ) + .replace( + /@media\s*\(prefers-color-scheme:\s*dark\)\s*{[^}]*--color-accent:\s*(.*?);/, + (match) => + match.replace( + /--color-accent:\s*(.*?);/, + `--color-accent: ${websiteData.accent_color_light_theme};` + ) + ) + ); }; diff --git a/web-app/template-styles/common-styles.css b/web-app/template-styles/common-styles.css index ba8ee76..631d7f8 100644 --- a/web-app/template-styles/common-styles.css +++ b/web-app/template-styles/common-styles.css @@ -166,7 +166,7 @@ summary { background-color: var(--bg-tertiary); } -:is(button, input, textarea, select, a, summary):focus, +:is(button, input, textarea, select, a, summary, pre):focus, #toggle-mobile-preview:checked + label { outline: 0.125rem solid var(--color-accent); outline-offset: 0.25rem;