From 6c314970bdb502ad3b7fb3ee0e2dd2ccbd3ca9b6 Mon Sep 17 00:00:00 2001 From: thiloho <123883702+thiloho@users.noreply.github.com> Date: Sun, 29 Sep 2024 15:00:19 +0200 Subject: [PATCH 01/12] Create template for docker image --- flake.nix | 2 ++ nix/docker.nix | 45 +++++++++++++++++++++++++ web-app/template-styles/blog-styles.css | 5 +++ web-app/template-styles/docs-styles.css | 5 +++ 4 files changed, 57 insertions(+) create mode 100644 nix/docker.nix diff --git a/flake.nix b/flake.nix index 929608e..178caf4 100644 --- a/flake.nix +++ b/flake.nix @@ -49,6 +49,8 @@ dev-vm = self.nixosConfigurations.dev-vm.config.system.build.vm; default = pkgs.callPackage ./nix/package.nix { }; + + docker = pkgs.callPackage ./nix/docker.nix { }; } ); diff --git a/nix/docker.nix b/nix/docker.nix new file mode 100644 index 0000000..4393df0 --- /dev/null +++ b/nix/docker.nix @@ -0,0 +1,45 @@ +{ + pkgs, + ... +}: + +# Behaviour of the Nix module needs to be replicated, which includes PostgreSQL, NGINX, ACME (DNS01), env variables, etc. +# Basic initialisation template can be found below +let + archtika = pkgs.callPackage ./package.nix { }; + + postgresConf = pkgs.writeText "postgres.conf" '' + + ''; + + nginxConf = pkgs.writeText "nginx.conf" '' + + ''; + + entrypoint = pkgs.writeShellScriptBin "entrypoint" '' + + ''; +in +pkgs.dockerTools.buildLayeredImage { + name = "archtika"; + tag = "latest"; + contents = [ + archtika + entrypoint + pkgs.postgresql_16 + pkgs.nginx + pkgs.acme-sh + pkgs.bash + pkgs.coreutils + ]; + config = { + Cmd = [ "${entrypoint}/bin/entrypoint" ]; + ExposedPorts = { + "80" = { }; + "443" = { }; + }; + Volumes = { + "/var/lib/postgresql/data" = { }; + }; + }; +} diff --git a/web-app/template-styles/blog-styles.css b/web-app/template-styles/blog-styles.css index d5b5b4c..0c524f6 100644 --- a/web-app/template-styles/blog-styles.css +++ b/web-app/template-styles/blog-styles.css @@ -33,6 +33,11 @@ footer { padding-block: var(--space-s); } +footer { + margin-block-start: auto; + text-align: center; +} + .articles ul { display: flex; flex-direction: column; diff --git a/web-app/template-styles/docs-styles.css b/web-app/template-styles/docs-styles.css index 59d1d01..29ca9b8 100644 --- a/web-app/template-styles/docs-styles.css +++ b/web-app/template-styles/docs-styles.css @@ -32,6 +32,11 @@ footer { padding-block: var(--space-s); } +footer { + margin-block-start: auto; + text-align: center; +} + section { scroll-margin-block-start: var(--space-xl); } From f2d114dac4a53a64d2c8a316f4f67218b6ad2ec6 Mon Sep 17 00:00:00 2001 From: thiloho <123883702+thiloho@users.noreply.github.com> Date: Thu, 3 Oct 2024 18:51:30 +0200 Subject: [PATCH 02/12] Add theme toggle for templates --- flake.nix | 5 +- nix/dev-vm.nix | 15 +- nix/module.nix | 1 + .../migrations/20240719071602_main_tables.sql | 4 +- ...73454_automatic_schema_cache_reloading.sql | 6 +- ...240720074103_user_management_roles_jwt.sql | 7 +- ...20240720132802_exposed_views_functions.sql | 12 +- .../20240805132306_last_modified_triggers.sql | 6 +- .../20240810115846_image_upload_function.sql | 4 +- .../migrations/20240911070907_change_log.sql | 18 +- .../src/lib/components/WebsiteEditor.svelte | 47 +-- web-app/src/lib/db-schema.ts | 10 +- web-app/src/lib/templates/common/Head.svelte | 2 +- web-app/src/lib/templates/common/Nav.svelte | 27 ++ web-app/src/lib/utils.ts | 1 - .../routes/(anonymous)/login/+page.server.ts | 2 +- .../routes/(authenticated)/+page.server.ts | 2 +- .../[websiteId]/articles/+page.server.ts | 2 +- .../website/[websiteId]/logs/+page.svelte | 11 +- web-app/template-styles/blog-styles.css | 6 + web-app/template-styles/common-styles.css | 296 +++++++++++++++--- web-app/template-styles/docs-styles.css | 5 - 22 files changed, 366 insertions(+), 123 deletions(-) diff --git a/flake.nix b/flake.nix index 178caf4..364073a 100644 --- a/flake.nix +++ b/flake.nix @@ -23,7 +23,10 @@ in { api = pkgs.mkShell { - packages = with pkgs; [ postgresql_16 ]; + packages = with pkgs; [ + postgresql_16 + postgrest + ]; shellHook = '' alias dbmate="${pkgs.dbmate}/bin/dbmate --no-dump-schema --url postgres://postgres@localhost:15432/archtika?sslmode=disable" alias formatsql="${pkgs.pgformatter}/bin/pg_format -s 2 -f 2 -U 2 -i db/migrations/*.sql" diff --git a/nix/dev-vm.nix b/nix/dev-vm.nix index 33518f4..bbb3ceb 100644 --- a/nix/dev-vm.nix +++ b/nix/dev-vm.nix @@ -24,6 +24,8 @@ virtualisation = { graphics = false; + memorySize = 2048; + cores = 2; sharedDirectories = { websites = { source = "/var/www/archtika-websites"; @@ -59,6 +61,11 @@ }; nginx = { enable = true; + recommendedProxySettings = true; + recommendedTlsSettings = true; + recommendedZstdSettings = true; + recommendedOptimisation = true; + virtualHosts."_" = { listen = [ { @@ -67,13 +74,15 @@ } ]; locations = { + "/previews/" = { + alias = "/var/www/archtika-websites/previews/"; + index = "index.html"; + tryFiles = "$uri $uri/ $uri.html =404"; + }; "/" = { root = "/var/www/archtika-websites"; index = "index.html"; tryFiles = "$uri $uri/ $uri.html =404"; - extraConfig = '' - autoindex on; - ''; }; }; }; diff --git a/nix/module.nix b/nix/module.nix index dcf73b1..e14181d 100644 --- a/nix/module.nix +++ b/nix/module.nix @@ -105,6 +105,7 @@ in User = cfg.user; Group = cfg.group; Restart = "always"; + WorkingDirectory = "${cfg.package}/rest-api"; }; script = '' diff --git a/rest-api/db/migrations/20240719071602_main_tables.sql b/rest-api/db/migrations/20240719071602_main_tables.sql index 2c023ab..532564f 100644 --- a/rest-api/db/migrations/20240719071602_main_tables.sql +++ b/rest-api/db/migrations/20240719071602_main_tables.sql @@ -37,8 +37,7 @@ CREATE TABLE internal.website ( is_published BOOLEAN NOT NULL DEFAULT FALSE, created_at TIMESTAMPTZ NOT NULL DEFAULT CLOCK_TIMESTAMP(), last_modified_at TIMESTAMPTZ NOT NULL DEFAULT CLOCK_TIMESTAMP(), - last_modified_by UUID REFERENCES internal.user (id) ON DELETE SET NULL, - title_search TSVECTOR GENERATED ALWAYS AS (TO_TSVECTOR('english', title)) STORED + last_modified_by UUID REFERENCES internal.user (id) ON DELETE SET NULL ); CREATE TABLE internal.media ( @@ -107,7 +106,6 @@ CREATE TABLE internal.article ( created_at TIMESTAMPTZ NOT NULL DEFAULT CLOCK_TIMESTAMP(), last_modified_at TIMESTAMPTZ NOT NULL DEFAULT CLOCK_TIMESTAMP(), last_modified_by UUID REFERENCES internal.user (id) ON DELETE SET NULL, - title_description_search TSVECTOR GENERATED ALWAYS AS (TO_TSVECTOR('english', COALESCE(title, '') || ' ' || COALESCE(meta_description, ''))) STORED, UNIQUE (website_id, category, article_weight) ); diff --git a/rest-api/db/migrations/20240720073454_automatic_schema_cache_reloading.sql b/rest-api/db/migrations/20240720073454_automatic_schema_cache_reloading.sql index 83b1001..b7b7859 100644 --- a/rest-api/db/migrations/20240720073454_automatic_schema_cache_reloading.sql +++ b/rest-api/db/migrations/20240720073454_automatic_schema_cache_reloading.sql @@ -1,5 +1,5 @@ -- migrate:up -CREATE FUNCTION pgrst_watch () +CREATE FUNCTION internal.pgrst_watch () RETURNS EVENT_TRIGGER AS $$ BEGIN @@ -10,10 +10,10 @@ $$ LANGUAGE plpgsql; CREATE EVENT TRIGGER pgrst_watch ON ddl_command_end - EXECUTE FUNCTION pgrst_watch (); + EXECUTE FUNCTION internal.pgrst_watch (); -- migrate:down DROP EVENT TRIGGER pgrst_watch; -DROP FUNCTION pgrst_watch (); +DROP FUNCTION internal.pgrst_watch (); diff --git a/rest-api/db/migrations/20240720074103_user_management_roles_jwt.sql b/rest-api/db/migrations/20240720074103_user_management_roles_jwt.sql index a3c4505..3504dee 100644 --- a/rest-api/db/migrations/20240720074103_user_management_roles_jwt.sql +++ b/rest-api/db/migrations/20240720074103_user_management_roles_jwt.sql @@ -48,7 +48,7 @@ CREATE FUNCTION internal.user_role (username TEXT, pass TEXT, OUT role_name NAME AS $$ BEGIN SELECT - ROLE INTO role_name + u.role INTO role_name FROM internal.user AS u WHERE @@ -111,7 +111,7 @@ AS $$ DECLARE _role NAME; _user_id UUID; - _exp INTEGER; + _exp INTEGER := EXTRACT(EPOCH FROM CLOCK_TIMESTAMP())::INTEGER + 86400; BEGIN SELECT internal.user_role (login.username, login.pass) INTO _role; @@ -120,12 +120,11 @@ BEGIN USING message = 'Invalid username or password'; ELSE SELECT - id INTO _user_id + u.id INTO _user_id FROM internal.user AS u WHERE u.username = login.username; - _exp := EXTRACT(EPOCH FROM CLOCK_TIMESTAMP())::INTEGER + 86400; SELECT SIGN(JSON_BUILD_OBJECT('role', _role, 'user_id', _user_id, 'username', login.username, 'exp', _exp), CURRENT_SETTING('app.jwt_secret')) INTO token; END IF; diff --git a/rest-api/db/migrations/20240720132802_exposed_views_functions.sql b/rest-api/db/migrations/20240720132802_exposed_views_functions.sql index 0e7e4aa..1ce0252 100644 --- a/rest-api/db/migrations/20240720132802_exposed_views_functions.sql +++ b/rest-api/db/migrations/20240720132802_exposed_views_functions.sql @@ -99,17 +99,7 @@ BEGIN INSERT INTO internal.home (website_id, main_content) VALUES (_website_id, '## About -archtika is a FLOSS, modern, performant and lightweight CMS (Content Mangement System) in the form of a web application. It allows you to easily create, manage and publish minimal, responsive and SEO friendly blogging and documentation websites with official, professionally designed templates. - -It is also possible to add contributors to your sites, which is very useful for larger projects where, for example, several people are constantly working on the documentation. - -## How it works - -For the backend, PostgreSQL is used in combination with PostgREST to create a RESTful API. JSON web tokens along with row-level security control authentication and authorisation flows. - -The web application uses SvelteKit with SSR (Server Side Rendering) and Svelte version 5, currently in beta. - -NGINX is used to deploy the websites, serving the static site files from the `/var/www/archtika-websites` directory. The static files can be found in this directory via the path `/`, which is dynamically created by the web application.'); +archtika is a FLOSS, modern, performant and lightweight CMS (Content Mangement System) in the form of a web application. It allows you to easily create, manage and publish minimal, responsive and SEO friendly blogging and documentation websites with official, professionally designed templates. It is also possible to add contributors to your sites, which is very useful for larger projects where, for example, several people are constantly working on the documentation.'); INSERT INTO internal.footer (website_id, additional_text) VALUES (_website_id, 'archtika is a free, open, modern, performant and lightweight CMS'); website_id := _website_id; diff --git a/rest-api/db/migrations/20240805132306_last_modified_triggers.sql b/rest-api/db/migrations/20240805132306_last_modified_triggers.sql index bcc55e1..8dedd0b 100644 --- a/rest-api/db/migrations/20240805132306_last_modified_triggers.sql +++ b/rest-api/db/migrations/20240805132306_last_modified_triggers.sql @@ -7,11 +7,11 @@ DECLARE BEGIN IF (NOT EXISTS ( SELECT - id + u.id FROM - internal.user + internal.user AS u WHERE - id = _user_id)) THEN + u.id = _user_id)) THEN RETURN COALESCE(NEW, OLD); END IF; IF TG_OP != 'DELETE' THEN diff --git a/rest-api/db/migrations/20240810115846_image_upload_function.sql b/rest-api/db/migrations/20240810115846_image_upload_function.sql index dd9af74..d7214da 100644 --- a/rest-api/db/migrations/20240810115846_image_upload_function.sql +++ b/rest-api/db/migrations/20240810115846_image_upload_function.sql @@ -21,7 +21,7 @@ BEGIN SELECT UNNEST(_allowed_mimetypes))) THEN RAISE invalid_parameter_value - USING message = 'Invalid MIME type. Allowed types are: png, jpg, webp'; + USING message = 'Invalid MIME type. Allowed types are: png, jpg, webp, avif, gif, svg'; ELSIF 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)); @@ -56,7 +56,7 @@ BEGIN SELECT m.blob FROM - internal.media m + internal.media AS m WHERE m.id = retrieve_file.id INTO _blob; IF FOUND THEN diff --git a/rest-api/db/migrations/20240911070907_change_log.sql b/rest-api/db/migrations/20240911070907_change_log.sql index be5574e..1011bce 100644 --- a/rest-api/db/migrations/20240911070907_change_log.sql +++ b/rest-api/db/migrations/20240911070907_change_log.sql @@ -22,11 +22,11 @@ DECLARE BEGIN IF (NOT EXISTS ( SELECT - id + u.id FROM - internal.user + internal.user AS u WHERE - id = _user_id) OR (to_jsonb (OLD.*) - 'last_modified_at' - 'last_modified_by') = (to_jsonb (NEW.*) - 'last_modified_at' - 'last_modified_by')) THEN + u.id = _user_id) OR (to_jsonb (OLD.*) - 'last_modified_at' - 'last_modified_by') = (to_jsonb (NEW.*) - 'last_modified_at' - 'last_modified_by')) THEN RETURN NULL; END IF; IF TG_TABLE_NAME = 'website' THEN @@ -40,21 +40,21 @@ BEGIN ELSIF (TG_OP = 'UPDATE' AND EXISTS ( SELECT - id + w.id FROM - internal.website + internal.website AS w WHERE - id = _website_id)) THEN + w.id = _website_id)) THEN INSERT INTO internal.change_log (website_id, table_name, operation, old_value, new_value) VALUES (_website_id, TG_TABLE_NAME, TG_OP, HSTORE (OLD) - HSTORE (NEW), HSTORE (NEW) - HSTORE (OLD)); ELSIF (TG_OP = 'DELETE' AND EXISTS ( SELECT - id + w.id FROM - internal.website + internal.website AS w WHERE - id = _website_id)) THEN + w.id = _website_id)) THEN INSERT INTO internal.change_log (website_id, table_name, operation, old_value) VALUES (_website_id, TG_TABLE_NAME, TG_OP, HSTORE (OLD)); END IF; diff --git a/web-app/src/lib/components/WebsiteEditor.svelte b/web-app/src/lib/components/WebsiteEditor.svelte index 267831d..85f40a4 100644 --- a/web-app/src/lib/components/WebsiteEditor.svelte +++ b/web-app/src/lib/components/WebsiteEditor.svelte @@ -24,6 +24,16 @@ const scrollHeight = previewElement.scrollHeight - previewElement.clientHeight; previewElement.scrollTop = (textareaScrollTop.value / 100) * scrollHeight; }); + + const tabs = [ + "settings", + "articles", + "categories", + "collaborators", + "legal-information", + "publish", + "logs" + ]; @@ -34,27 +44,17 @@ @@ -117,6 +117,11 @@ gap: var(--space-s); } + .active { + text-underline-offset: 0.375rem; + text-decoration-thickness: 0.25rem; + } + @media (min-width: 640px) { label[for="toggle-mobile-preview"] { display: none; diff --git a/web-app/src/lib/db-schema.ts b/web-app/src/lib/db-schema.ts index a02fb53..33ae85a 100644 --- a/web-app/src/lib/db-schema.ts +++ b/web-app/src/lib/db-schema.ts @@ -27,7 +27,6 @@ export interface Article { created_at: Date; last_modified_at: Date; last_modified_by: string | null; - title_description_search: any | null; } export interface ArticleInput { id?: string; @@ -44,7 +43,6 @@ export interface ArticleInput { created_at?: Date; last_modified_at?: Date; last_modified_by?: string | null; - title_description_search?: any | null; } const article = { tableName: "article", @@ -62,8 +60,7 @@ const article = { "article_weight", "created_at", "last_modified_at", - "last_modified_by", - "title_description_search" + "last_modified_by" ], requiredForInsert: ["website_id", "title"], primaryKey: "id", @@ -463,7 +460,6 @@ export interface Website { created_at: Date; last_modified_at: Date; last_modified_by: string | null; - title_search: any | null; } export interface WebsiteInput { id?: string; @@ -474,7 +470,6 @@ export interface WebsiteInput { created_at?: Date; last_modified_at?: Date; last_modified_by?: string | null; - title_search?: any | null; } const website = { tableName: "website", @@ -486,8 +481,7 @@ const website = { "is_published", "created_at", "last_modified_at", - "last_modified_by", - "title_search" + "last_modified_by" ], requiredForInsert: ["content_type", "title"], primaryKey: "id", diff --git a/web-app/src/lib/templates/common/Head.svelte b/web-app/src/lib/templates/common/Head.svelte index b7416c3..f9cd5cd 100644 --- a/web-app/src/lib/templates/common/Head.svelte +++ b/web-app/src/lib/templates/common/Head.svelte @@ -19,7 +19,7 @@ - {title} + {websiteOverview.title === title ? title : `${websiteOverview.title} | ${title}`} {#if websiteOverview.settings.favicon_image} diff --git a/web-app/src/lib/templates/common/Nav.svelte b/web-app/src/lib/templates/common/Nav.svelte index 5d66a8e..58bd3da 100644 --- a/web-app/src/lib/templates/common/Nav.svelte +++ b/web-app/src/lib/templates/common/Nav.svelte @@ -82,5 +82,32 @@ /> {/if} + diff --git a/web-app/src/lib/utils.ts b/web-app/src/lib/utils.ts index 9a03ad3..db03f0d 100644 --- a/web-app/src/lib/utils.ts +++ b/web-app/src/lib/utils.ts @@ -3,7 +3,6 @@ import type { Renderer, Token } from "marked"; import { markedHighlight } from "marked-highlight"; import hljs from "highlight.js"; import DOMPurify from "isomorphic-dompurify"; -import { applyAction, deserialize } from "$app/forms"; import type { Website, Settings, diff --git a/web-app/src/routes/(anonymous)/login/+page.server.ts b/web-app/src/routes/(anonymous)/login/+page.server.ts index 747b6a7..e31083e 100644 --- a/web-app/src/routes/(anonymous)/login/+page.server.ts +++ b/web-app/src/routes/(anonymous)/login/+page.server.ts @@ -18,7 +18,7 @@ export const actions: Actions = { return response; } - cookies.set("session_token", response.data.token, { path: "/" }); + cookies.set("session_token", response.data.token, { path: "/", maxAge: 86400 }); return response; } }; diff --git a/web-app/src/routes/(authenticated)/+page.server.ts b/web-app/src/routes/(authenticated)/+page.server.ts index fe26335..1d43df6 100644 --- a/web-app/src/routes/(authenticated)/+page.server.ts +++ b/web-app/src/routes/(authenticated)/+page.server.ts @@ -14,7 +14,7 @@ export const load: PageServerLoad = async ({ fetch, url, locals }) => { const baseFetchUrl = `${API_BASE_PREFIX}/website?order=last_modified_at.desc,created_at.desc`; if (searchQuery) { - params.append("title_search", `wfts(english).${searchQuery}`); + params.append("title", `wfts.${searchQuery}`); } switch (filterBy) { diff --git a/web-app/src/routes/(authenticated)/website/[websiteId]/articles/+page.server.ts b/web-app/src/routes/(authenticated)/website/[websiteId]/articles/+page.server.ts index ac124fb..a78c410 100644 --- a/web-app/src/routes/(authenticated)/website/[websiteId]/articles/+page.server.ts +++ b/web-app/src/routes/(authenticated)/website/[websiteId]/articles/+page.server.ts @@ -21,7 +21,7 @@ export const load: PageServerLoad = async ({ params, fetch, url, parent, locals const parameters = new URLSearchParams(); if (searchQuery) { - parameters.append("title_description_search", `wfts(english).${searchQuery}`); + parameters.append("title", `wfts.${searchQuery}`); } switch (filterBy) { diff --git a/web-app/src/routes/(authenticated)/website/[websiteId]/logs/+page.svelte b/web-app/src/routes/(authenticated)/website/[websiteId]/logs/+page.svelte index 3bcf0ba..6cc079e 100644 --- a/web-app/src/routes/(authenticated)/website/[websiteId]/logs/+page.svelte +++ b/web-app/src/routes/(authenticated)/website/[websiteId]/logs/+page.svelte @@ -7,7 +7,7 @@ import { page } from "$app/stores"; import { tables } from "$lib/db-schema"; import { previewContent } from "$lib/runes.svelte"; - import { sanitize } from "isomorphic-dompurify"; + import DOMPurify from "isomorphic-dompurify"; const { data }: { data: PageServerData } = $props(); @@ -156,9 +156,12 @@

{table_name} — {operation}

-
{@html sanitize(htmlDiff(oldValue, newValue), {
-                      ALLOWED_TAGS: ["ins", "del"]
-                    })}
+
{@html DOMPurify.sanitize(
+                      htmlDiff(oldValue, newValue),
+                      {
+                        ALLOWED_TAGS: ["ins", "del"]
+                      }
+                    )}
diff --git a/web-app/template-styles/blog-styles.css b/web-app/template-styles/blog-styles.css index 0c524f6..a9618a9 100644 --- a/web-app/template-styles/blog-styles.css +++ b/web-app/template-styles/blog-styles.css @@ -13,6 +13,12 @@ nav { border-block-end: var(--border-primary); } +nav > .container { + display: flex; + align-items: center; + gap: var(--space-2xs); +} + header > .container { display: flex; flex-direction: column; diff --git a/web-app/template-styles/common-styles.css b/web-app/template-styles/common-styles.css index 5b83a37..446e03c 100644 --- a/web-app/template-styles/common-styles.css +++ b/web-app/template-styles/common-styles.css @@ -1,41 +1,4 @@ -@import url("https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.10.0/styles/github.min.css") - screen and (prefers-color-scheme: light); -@import url("https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.10.0/styles/github-dark.min.css") - screen and (prefers-color-scheme: dark); - -@font-face { - font-family: "JetBrains Mono"; - font-style: normal; - font-display: swap; - font-weight: 400; - src: - url(https://cdn.jsdelivr.net/fontsource/fonts/jetbrains-mono@latest/latin-400-normal.woff2) - format("woff2"), - url(https://cdn.jsdelivr.net/fontsource/fonts/jetbrains-mono@latest/latin-400-normal.woff) - format("woff"); - unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, - U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, - U+FFFD; -} - -:root { - --bg-primary-h: /* BACKGROUND_COLOR_LIGHT_THEME_H */ 0; - --bg-primary-s: /* BACKGROUND_COLOR_LIGHT_THEME_S */ 0%; - --bg-primary-l: /* BACKGROUND_COLOR_LIGHT_THEME_L */ 100%; - --bg-primary: hsl(var(--bg-primary-h) var(--bg-primary-s) var(--bg-primary-l)); - --bg-secondary: hsl(var(--bg-primary-h) var(--bg-primary-s) calc(var(--bg-primary-l) - 5%)); - --bg-tertiary: hsl(var(--bg-primary-h) var(--bg-primary-s) calc(var(--bg-primary-l) - 10%)); - --bg-blurred: hsla( - var(--bg-primary-h) var(--bg-primary-s) var(--bg-primary-l) / calc(var(--bg-primary-l) - 20%) - ); - - --color-text: hsl(var(--bg-primary-h) var(--bg-primary-s) 0%); - --color-text-invert: var(--bg-primary); - --color-border: hsl(var(--bg-primary-h) var(--bg-primary-s) calc(var(--bg-primary-l) - 50%)); - --color-accent: /* ACCENT_COLOR_LIGHT_THEME */ hsl(210 100% 30%); - --color-success: hsl(105 100% 30%); - --color-error: hsl(0 100% 30%); - +html { --border-primary: 0.0625rem solid var(--color-border); --border-radius: 0.125rem; @@ -72,12 +35,92 @@ --space-2xl: clamp(4rem, 3.7368rem + 1.3158cqi, 5rem); /* Space 3xl: 96px → 120px */ --space-3xl: clamp(6rem, 5.6053rem + 1.9737cqi, 7.5rem); +} + +html { + --bg-primary-h: /* BACKGROUND_COLOR_LIGHT_THEME_H */ 0; + --bg-primary-s: /* BACKGROUND_COLOR_LIGHT_THEME_S */ 0%; + --bg-primary-l: /* BACKGROUND_COLOR_LIGHT_THEME_L */ 100%; + --bg-primary: hsl(var(--bg-primary-h) var(--bg-primary-s) var(--bg-primary-l)); + --bg-secondary: hsl(var(--bg-primary-h) var(--bg-primary-s) calc(var(--bg-primary-l) - 5%)); + --bg-tertiary: hsl(var(--bg-primary-h) var(--bg-primary-s) calc(var(--bg-primary-l) - 10%)); + --bg-blurred: hsla( + var(--bg-primary-h) var(--bg-primary-s) var(--bg-primary-l) / calc(var(--bg-primary-l) - 20%) + ); + + --color-text: hsl(var(--bg-primary-h) var(--bg-primary-s) 0%); + --color-text-invert: var(--bg-primary); + --color-border: hsl(var(--bg-primary-h) var(--bg-primary-s) calc(var(--bg-primary-l) - 50%)); + --color-accent: /* ACCENT_COLOR_LIGHT_THEME */ hsl(210 100% 30%); + --color-success: hsl(105 100% 30%); + --color-error: hsl(0 100% 30%); + + --display-light: none; + --display-dark: initial; + + --hl-bg: #fff; + --hl-color: #24292e; + --hl-keyword: #d73a49; + --hl-title: #6f42c1; + --hl-attr: #005cc5; + --hl-string: #032f62; + --hl-built-in: #e36209; + --hl-comment: #6a737d; + --hl-tag: #22863a; + --hl-section: #005cc5; + --hl-bullet: #735c0f; + --hl-emphasis: #24292e; + --hl-addition-bg: #f0fff4; + --hl-addition-text: #22863a; + --hl-deletion-bg: #ffeef0; + --hl-deletion-text: #b31d28; color-scheme: light; } +html:has(#toggle-theme:checked) { + --bg-primary-h: /* BACKGROUND_COLOR_DARK_THEME_H */ 0; + --bg-primary-s: /* BACKGROUND_COLOR_DARK_THEME_S */ 0%; + --bg-primary-l: /* BACKGROUND_COLOR_DARK_THEME_L */ 15%; + --bg-primary: hsl(var(--bg-primary-h) var(--bg-primary-s) var(--bg-primary-l)); + --bg-secondary: hsl(var(--bg-primary-h) var(--bg-primary-s) calc(var(--bg-primary-l) + 5%)); + --bg-tertiary: hsl(var(--bg-primary-h) var(--bg-primary-s) calc(var(--bg-primary-l) + 10%)); + --bg-blurred: hsla( + var(--bg-primary-h) var(--bg-primary-s) var(--bg-primary-l) / calc(var(--bg-primary-l) + 20%) + ); + + --color-text: hsl(var(--bg-primary-h) var(--bg-primary-s) 100%); + --color-text-invert: var(--bg-primary); + --color-border: hsl(var(--bg-primary-h) var(--bg-primary-s) calc(var(--bg-primary-l) + 50%)); + --color-accent: /* ACCENT_COLOR_DARK_THEME */ hsl(210 100% 80%); + --color-success: hsl(105 100% 80%); + --color-error: hsl(0 100% 80%); + + --display-light: initial; + --display-dark: none; + + --hl-bg: #0d1117; + --hl-color: #c9d1d9; + --hl-keyword: #ff7b72; + --hl-title: #d2a8ff; + --hl-attr: #79c0ff; + --hl-string: #a5d6ff; + --hl-built-in: #ffa657; + --hl-comment: #8b949e; + --hl-tag: #7ee787; + --hl-section: #1f6feb; + --hl-bullet: #f2cc60; + --hl-emphasis: #c9d1d9; + --hl-addition-bg: #033a16; + --hl-addition-text: #aff5b4; + --hl-deletion-bg: #67060c; + --hl-deletion-text: #ffdcd7; + + color-scheme: dark; +} + @media (prefers-color-scheme: dark) { - :root { + html { --bg-primary-h: /* BACKGROUND_COLOR_DARK_THEME_H */ 0; --bg-primary-s: /* BACKGROUND_COLOR_DARK_THEME_S */ 0%; --bg-primary-l: /* BACKGROUND_COLOR_DARK_THEME_L */ 15%; @@ -95,8 +138,69 @@ --color-success: hsl(105 100% 80%); --color-error: hsl(0 100% 80%); + --display-light: initial; + --display-dark: none; + + --hl-bg: #0d1117; + --hl-color: #c9d1d9; + --hl-keyword: #ff7b72; + --hl-title: #d2a8ff; + --hl-attr: #79c0ff; + --hl-string: #a5d6ff; + --hl-built-in: #ffa657; + --hl-comment: #8b949e; + --hl-tag: #7ee787; + --hl-section: #1f6feb; + --hl-bullet: #f2cc60; + --hl-emphasis: #c9d1d9; + --hl-addition-bg: #033a16; + --hl-addition-text: #aff5b4; + --hl-deletion-bg: #67060c; + --hl-deletion-text: #ffdcd7; + color-scheme: dark; } + + html:has(#toggle-theme:checked) { + --bg-primary-h: /* BACKGROUND_COLOR_LIGHT_THEME_H */ 0; + --bg-primary-s: /* BACKGROUND_COLOR_LIGHT_THEME_S */ 0%; + --bg-primary-l: /* BACKGROUND_COLOR_LIGHT_THEME_L */ 100%; + --bg-primary: hsl(var(--bg-primary-h) var(--bg-primary-s) var(--bg-primary-l)); + --bg-secondary: hsl(var(--bg-primary-h) var(--bg-primary-s) calc(var(--bg-primary-l) - 5%)); + --bg-tertiary: hsl(var(--bg-primary-h) var(--bg-primary-s) calc(var(--bg-primary-l) - 10%)); + --bg-blurred: hsla( + var(--bg-primary-h) var(--bg-primary-s) var(--bg-primary-l) / calc(var(--bg-primary-l) - 20%) + ); + + --color-text: hsl(var(--bg-primary-h) var(--bg-primary-s) 0%); + --color-text-invert: var(--bg-primary); + --color-border: hsl(var(--bg-primary-h) var(--bg-primary-s) calc(var(--bg-primary-l) - 50%)); + --color-accent: /* ACCENT_COLOR_LIGHT_THEME */ hsl(210 100% 30%); + --color-success: hsl(105 100% 30%); + --color-error: hsl(0 100% 30%); + + --display-light: none; + --display-dark: initial; + + --hl-bg: #fff; + --hl-color: #24292e; + --hl-keyword: #d73a49; + --hl-title: #6f42c1; + --hl-attr: #005cc5; + --hl-string: #032f62; + --hl-built-in: #e36209; + --hl-comment: #6a737d; + --hl-tag: #22863a; + --hl-section: #005cc5; + --hl-bullet: #735c0f; + --hl-emphasis: #24292e; + --hl-addition-bg: #f0fff4; + --hl-addition-text: #22863a; + --hl-deletion-bg: #ffeef0; + --hl-deletion-text: #b31d28; + + color-scheme: light; + } } *, @@ -109,11 +213,12 @@ body { line-height: 1.5; - font-family: system-ui, sans-serif; + font-family: system-ui; background-color: var(--bg-primary); display: flex; flex-direction: column; min-block-size: 100vh; + color: var(--color-text); } button, @@ -123,6 +228,7 @@ select, [role="option"], label[for="toggle-mobile-preview"], label[for="toggle-sidebar"], +label[for="toggle-theme"], summary { cursor: pointer; } @@ -134,6 +240,7 @@ select, a[role="button"], label[for="toggle-mobile-preview"], label[for="toggle-sidebar"], +label[for="toggle-theme"], summary { font: inherit; color: inherit; @@ -174,15 +281,30 @@ button, a[role="button"], label[for="toggle-mobile-preview"], label[for="toggle-sidebar"], +label[for="toggle-theme"], summary { background-color: var(--bg-secondary); } +label:has(svg) { + display: inline-grid; + place-content: center; +} + +label[for="toggle-theme"] svg:first-of-type { + display: var(--display-light); +} + +label[for="toggle-theme"] svg:last-of-type { + display: var(--display-dark); +} + :is( button, a[role="button"], label[for="toggle-mobile-preview"], label[for="toggle-sidebar"], + label[for="toggle-theme"], summary ):hover { background-color: var(--bg-tertiary); @@ -304,7 +426,7 @@ pre { } code { - font-family: "JetBrains Mono", monospace; + font-family: monospace; font-size: var(--font-size--1); } @@ -354,3 +476,95 @@ del { background-color: var(--color-error); color: var(--color-text-invert); } + +.hljs { + color: var(--hl-color); + background: var(--hl-bg); +} + +.hljs-doctag, +.hljs-keyword, +.hljs-meta .hljs-keyword, +.hljs-template-tag, +.hljs-template-variable, +.hljs-type, +.hljs-variable.language_ { + color: var(--hl-keyword); +} + +.hljs-title, +.hljs-title.class_, +.hljs-title.class_.inherited__, +.hljs-title.function_ { + color: var(--hl-title); +} + +.hljs-attr, +.hljs-attribute, +.hljs-literal, +.hljs-meta, +.hljs-number, +.hljs-operator, +.hljs-selector-attr, +.hljs-selector-class, +.hljs-selector-id, +.hljs-variable { + color: var(--hl-attr); +} + +.hljs-meta .hljs-string, +.hljs-regexp, +.hljs-string { + color: var(--hl-string); +} + +.hljs-built_in, +.hljs-symbol { + color: var(--hl-built-in); +} + +.hljs-code, +.hljs-comment, +.hljs-formula { + color: var(--hl-comment); +} + +.hljs-name, +.hljs-quote, +.hljs-selector-pseudo, +.hljs-selector-tag { + color: var(--hl-tag); +} + +.hljs-subst { + color: var(--hl-color); +} + +.hljs-section { + color: var(--hl-section); + font-weight: bold; +} + +.hljs-bullet { + color: var(--hl-bullet); +} + +.hljs-emphasis { + color: var(--hl-emphasis); + font-style: italic; +} + +.hljs-strong { + color: var(--hl-emphasis); + font-weight: bold; +} + +.hljs-addition { + color: var(--hl-addition-text); + background-color: var(--hl-addition-bg); +} + +.hljs-deletion { + color: var(--hl-deletion-text); + background-color: var(--hl-deletion-bg); +} diff --git a/web-app/template-styles/docs-styles.css b/web-app/template-styles/docs-styles.css index 29ca9b8..14910d4 100644 --- a/web-app/template-styles/docs-styles.css +++ b/web-app/template-styles/docs-styles.css @@ -41,11 +41,6 @@ section { scroll-margin-block-start: var(--space-xl); } -label[for="toggle-sidebar"] { - display: inline-grid; - place-content: center; -} - .docs-navigation { display: none; position: fixed; From e96b78b7ce7a08268ed0b532ceedeb5132ebe482 Mon Sep 17 00:00:00 2001 From: thiloho <123883702+thiloho@users.noreply.github.com> Date: Fri, 4 Oct 2024 17:09:51 +0200 Subject: [PATCH 03/12] Add OpenGraph head tags and more tests --- nix/dev-vm.nix | 1 + .../migrations/20240719071602_main_tables.sql | 1 + ...20240720132802_exposed_views_functions.sql | 2 +- .../20240724191017_row_level_security.sql | 2 +- .../migrations/20240911070907_change_log.sql | 28 ++- web-app/src/lib/db-schema.ts | 10 +- .../src/lib/templates/blog/BlogArticle.svelte | 7 +- .../src/lib/templates/blog/BlogIndex.svelte | 12 +- web-app/src/lib/templates/common/Head.svelte | 63 +++++- web-app/src/lib/templates/common/Nav.svelte | 4 +- .../src/lib/templates/docs/DocsArticle.svelte | 7 +- .../src/lib/templates/docs/DocsIndex.svelte | 12 +- .../website/[websiteId]/+page.server.ts | 3 +- .../website/[websiteId]/+page.svelte | 6 + .../[websiteId]/publish/+page.server.ts | 101 +++++---- web-app/src/routes/+layout.svelte | 1 + web-app/template-styles/common-styles.css | 205 ------------------ web-app/template-styles/variables.css | 204 +++++++++++++++++ web-app/tests/collaborator.spec.ts | 38 +++- web-app/tests/website.spec.ts | 34 +++ 20 files changed, 468 insertions(+), 273 deletions(-) create mode 100644 web-app/template-styles/variables.css diff --git a/nix/dev-vm.nix b/nix/dev-vm.nix index bbb3ceb..4e8d313 100644 --- a/nix/dev-vm.nix +++ b/nix/dev-vm.nix @@ -26,6 +26,7 @@ graphics = false; memorySize = 2048; cores = 2; + diskSize = 10240; sharedDirectories = { websites = { source = "/var/www/archtika-websites"; diff --git a/rest-api/db/migrations/20240719071602_main_tables.sql b/rest-api/db/migrations/20240719071602_main_tables.sql index 532564f..cca5554 100644 --- a/rest-api/db/migrations/20240719071602_main_tables.sql +++ b/rest-api/db/migrations/20240719071602_main_tables.sql @@ -74,6 +74,7 @@ CREATE TABLE internal.header ( CREATE TABLE internal.home ( website_id UUID PRIMARY KEY REFERENCES internal.website (id) ON DELETE CASCADE, main_content TEXT NOT NULL CHECK (TRIM(main_content) != ''), + meta_description VARCHAR(250) CHECK (TRIM(meta_description) != ''), last_modified_at TIMESTAMPTZ NOT NULL DEFAULT CLOCK_TIMESTAMP(), last_modified_by UUID REFERENCES internal.user (id) ON DELETE SET NULL ); diff --git a/rest-api/db/migrations/20240720132802_exposed_views_functions.sql b/rest-api/db/migrations/20240720132802_exposed_views_functions.sql index 1ce0252..48aaf39 100644 --- a/rest-api/db/migrations/20240720132802_exposed_views_functions.sql +++ b/rest-api/db/migrations/20240720132802_exposed_views_functions.sql @@ -129,7 +129,7 @@ GRANT SELECT, UPDATE (logo_type, logo_text, logo_image) ON internal.header TO au GRANT SELECT, UPDATE ON api.header TO authenticated_user; -GRANT SELECT, UPDATE (main_content) ON internal.home TO authenticated_user; +GRANT SELECT, UPDATE (main_content, meta_description) ON internal.home TO authenticated_user; GRANT SELECT, UPDATE ON api.home TO authenticated_user; diff --git a/rest-api/db/migrations/20240724191017_row_level_security.sql b/rest-api/db/migrations/20240724191017_row_level_security.sql index ae41e55..5acc1a9 100644 --- a/rest-api/db/migrations/20240724191017_row_level_security.sql +++ b/rest-api/db/migrations/20240724191017_row_level_security.sql @@ -75,7 +75,7 @@ CREATE POLICY view_websites ON internal.website CREATE POLICY update_website ON internal.website FOR UPDATE - USING (internal.user_has_website_access (id, 20)); + USING (internal.user_has_website_access (id, 30)); CREATE POLICY delete_website ON internal.website FOR DELETE diff --git a/rest-api/db/migrations/20240911070907_change_log.sql b/rest-api/db/migrations/20240911070907_change_log.sql index 1011bce..cc61118 100644 --- a/rest-api/db/migrations/20240911070907_change_log.sql +++ b/rest-api/db/migrations/20240911070907_change_log.sql @@ -13,6 +13,23 @@ CREATE TABLE internal.change_log ( new_value HSTORE ); +CREATE VIEW api.change_log WITH ( security_invoker = ON +) AS +SELECT + * +FROM + internal.change_log; + +GRANT SELECT ON internal.change_log TO authenticated_user; + +GRANT SELECT ON api.change_log TO authenticated_user; + +ALTER TABLE internal.change_log ENABLE ROW LEVEL SECURITY; + +CREATE POLICY view_change_log ON internal.change_log + FOR SELECT + USING (internal.user_has_website_access (website_id, 10)); + CREATE FUNCTION internal.track_changes () RETURNS TRIGGER AS $$ @@ -109,17 +126,6 @@ CREATE TRIGGER collab_track_changes FOR EACH ROW EXECUTE FUNCTION internal.track_changes (); -CREATE VIEW api.change_log WITH ( security_invoker = ON -) AS -SELECT - * -FROM - internal.change_log; - -GRANT SELECT ON internal.change_log TO authenticated_user; - -GRANT SELECT ON api.change_log TO authenticated_user; - -- migrate:down DROP TRIGGER website_track_changes ON internal.website; diff --git a/web-app/src/lib/db-schema.ts b/web-app/src/lib/db-schema.ts index 33ae85a..4b67699 100644 --- a/web-app/src/lib/db-schema.ts +++ b/web-app/src/lib/db-schema.ts @@ -299,18 +299,26 @@ const header = { export interface Home { website_id: string; main_content: string; + meta_description: string | null; last_modified_at: Date; last_modified_by: string | null; } export interface HomeInput { website_id: string; main_content: string; + meta_description?: string | null; last_modified_at?: Date; last_modified_by?: string | null; } const home = { tableName: "home", - columns: ["website_id", "main_content", "last_modified_at", "last_modified_by"], + columns: [ + "website_id", + "main_content", + "meta_description", + "last_modified_at", + "last_modified_by" + ], requiredForInsert: ["website_id", "main_content"], primaryKey: "website_id", foreignKeys: { diff --git a/web-app/src/lib/templates/blog/BlogArticle.svelte b/web-app/src/lib/templates/blog/BlogArticle.svelte index 4a119f5..a2f334b 100644 --- a/web-app/src/lib/templates/blog/BlogArticle.svelte +++ b/web-app/src/lib/templates/blog/BlogArticle.svelte @@ -8,8 +8,10 @@ const { websiteOverview, article, - apiUrl - }: { websiteOverview: WebsiteOverview; article: Article; apiUrl: string } = $props(); + apiUrl, + websiteUrl + }: { websiteOverview: WebsiteOverview; article: Article; apiUrl: string; websiteUrl: string } = + $props();