mirror of
https://github.com/thiloho/archtika.git
synced 2025-11-22 02:41:35 +01:00
Add TypeScript definitions via pg-to-ts and refactor migrations
This commit is contained in:
@@ -1,21 +1,25 @@
|
||||
-- migrate:up
|
||||
CREATE SCHEMA internal;
|
||||
|
||||
CREATE SCHEMA api;
|
||||
|
||||
CREATE ROLE authenticator LOGIN NOINHERIT NOCREATEDB NOCREATEROLE NOSUPERUSER;
|
||||
|
||||
CREATE ROLE anon NOLOGIN NOINHERIT;
|
||||
|
||||
GRANT USAGE ON SCHEMA api TO anon;
|
||||
|
||||
CREATE ROLE authenticated_user NOLOGIN NOINHERIT;
|
||||
|
||||
GRANT USAGE ON SCHEMA api TO authenticated_user;
|
||||
|
||||
CREATE ROLE authenticator LOGIN NOINHERIT NOCREATEDB NOCREATEROLE NOSUPERUSER;
|
||||
|
||||
GRANT anon TO authenticator;
|
||||
|
||||
GRANT authenticated_user TO authenticator;
|
||||
|
||||
CREATE SCHEMA internal;
|
||||
GRANT USAGE ON SCHEMA api TO anon;
|
||||
|
||||
GRANT USAGE ON SCHEMA api TO authenticated_user;
|
||||
|
||||
GRANT USAGE ON SCHEMA internal TO authenticated_user;
|
||||
|
||||
ALTER DEFAULT PRIVILEGES REVOKE EXECUTE ON FUNCTIONS FROM PUBLIC;
|
||||
|
||||
CREATE TABLE internal.user (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid (),
|
||||
@@ -30,8 +34,10 @@ CREATE TABLE internal.website (
|
||||
content_type VARCHAR(10) CHECK (content_type IN ('Blog', 'Docs')) NOT NULL,
|
||||
title VARCHAR(50) NOT NULL CHECK (TRIM(title) != ''),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT CLOCK_TIMESTAMP(),
|
||||
is_published BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
last_modified_at TIMESTAMPTZ NOT NULL DEFAULT CLOCK_TIMESTAMP(),
|
||||
last_modified_by UUID REFERENCES internal.user (id) ON DELETE SET NULL
|
||||
last_modified_by UUID REFERENCES internal.user (id) ON DELETE SET NULL,
|
||||
title_search TSVECTOR GENERATED ALWAYS AS (TO_TSVECTOR('english', title)) STORED
|
||||
);
|
||||
|
||||
CREATE TABLE internal.media (
|
||||
@@ -70,19 +76,32 @@ CREATE TABLE internal.home (
|
||||
last_modified_by UUID REFERENCES internal.user (id) ON DELETE SET NULL
|
||||
);
|
||||
|
||||
CREATE TABLE internal.docs_category (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid (),
|
||||
website_id UUID REFERENCES internal.website (id) ON DELETE CASCADE NOT NULL,
|
||||
user_id UUID REFERENCES internal.user (id) ON DELETE SET NULL DEFAULT (CURRENT_SETTING('request.jwt.claims', TRUE)::JSON ->> 'user_id') ::UUID,
|
||||
category_name VARCHAR(50) NOT NULL CHECK (TRIM(category_name) != ''),
|
||||
category_weight INTEGER CHECK (category_weight >= 0) NOT NULL,
|
||||
UNIQUE (website_id, category_name),
|
||||
UNIQUE (website_id, category_weight)
|
||||
);
|
||||
|
||||
CREATE TABLE internal.article (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid (),
|
||||
website_id UUID REFERENCES internal.website (id) ON DELETE CASCADE NOT NULL,
|
||||
user_id UUID REFERENCES internal.user (id) ON DELETE SET NULL,
|
||||
user_id UUID REFERENCES internal.user (id) ON DELETE SET NULL DEFAULT (CURRENT_SETTING('request.jwt.claims', TRUE)::JSON ->> 'user_id') ::UUID,
|
||||
title VARCHAR(100) NOT NULL CHECK (TRIM(title) != ''),
|
||||
meta_description VARCHAR(250) CHECK (TRIM(meta_description) != ''),
|
||||
meta_author VARCHAR(100) CHECK (TRIM(meta_author) != ''),
|
||||
cover_image UUID REFERENCES internal.media (id) ON DELETE SET NULL,
|
||||
publication_date DATE NOT NULL DEFAULT CURRENT_DATE,
|
||||
publication_date DATE DEFAULT CURRENT_DATE,
|
||||
main_content TEXT CHECK (TRIM(main_content) != ''),
|
||||
category UUID REFERENCES internal.docs_category (id) ON DELETE SET NULL,
|
||||
article_weight INTEGER CHECK (article_weight IS NULL OR article_weight >= 0),
|
||||
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
|
||||
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
|
||||
);
|
||||
|
||||
CREATE TABLE internal.footer (
|
||||
@@ -92,6 +111,13 @@ CREATE TABLE internal.footer (
|
||||
last_modified_by UUID REFERENCES internal.user (id) ON DELETE SET NULL
|
||||
);
|
||||
|
||||
CREATE TABLE internal.legal_information (
|
||||
website_id UUID PRIMARY KEY REFERENCES internal.website (id) ON DELETE CASCADE,
|
||||
main_content TEXT NOT NULL CHECK (TRIM(main_content) != ''),
|
||||
last_modified_at TIMESTAMPTZ NOT NULL DEFAULT CLOCK_TIMESTAMP(),
|
||||
last_modified_by UUID REFERENCES internal.user (id) ON DELETE SET NULL
|
||||
);
|
||||
|
||||
CREATE TABLE internal.collab (
|
||||
website_id UUID REFERENCES internal.website (id) ON DELETE CASCADE,
|
||||
user_id UUID REFERENCES internal.user (id) ON DELETE CASCADE,
|
||||
@@ -117,10 +143,14 @@ DROP TABLE internal.change_log;
|
||||
|
||||
DROP TABLE internal.collab;
|
||||
|
||||
DROP TABLE internal.legal_information;
|
||||
|
||||
DROP TABLE internal.footer;
|
||||
|
||||
DROP TABLE internal.article;
|
||||
|
||||
DROP TABLE internal.docs_category;
|
||||
|
||||
DROP TABLE internal.home;
|
||||
|
||||
DROP TABLE internal.header;
|
||||
@@ -131,15 +161,17 @@ DROP TABLE internal.media;
|
||||
|
||||
DROP TABLE internal.website;
|
||||
|
||||
DROP SCHEMA api;
|
||||
|
||||
DROP TABLE internal.user;
|
||||
|
||||
DROP SCHEMA internal;
|
||||
DROP SCHEMA api;
|
||||
|
||||
DROP ROLE authenticator;
|
||||
DROP SCHEMA internal;
|
||||
|
||||
DROP ROLE anon;
|
||||
|
||||
DROP ROLE authenticated_user;
|
||||
|
||||
DROP ROLE authenticator;
|
||||
|
||||
ALTER DEFAULT PRIVILEGES GRANT EXECUTE ON FUNCTIONS TO PUBLIC;
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
-- migrate:up
|
||||
CREATE FUNCTION pgrst_watch ()
|
||||
RETURNS event_trigger
|
||||
RETURNS EVENT_TRIGGER
|
||||
AS $$
|
||||
BEGIN
|
||||
NOTIFY pgrst,
|
||||
|
||||
@@ -45,23 +45,21 @@ CREATE TRIGGER encrypt_pass
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION internal.encrypt_pass ();
|
||||
|
||||
CREATE FUNCTION internal.user_role (username TEXT, PASSWORD TEXT)
|
||||
RETURNS NAME
|
||||
AS $$
|
||||
CREATE FUNCTION internal.user_role (username TEXT, pass TEXT, OUT role_name NAME)
|
||||
AS $$
|
||||
BEGIN
|
||||
RETURN (
|
||||
SELECT
|
||||
ROLE
|
||||
ROLE INTO role_name
|
||||
FROM
|
||||
internal.user AS u
|
||||
WHERE
|
||||
u.username = user_role.username
|
||||
AND u.password_hash = CRYPT(user_role.password, u.password_hash));
|
||||
AND u.password_hash = CRYPT(user_role.pass, u.password_hash);
|
||||
END;
|
||||
$$
|
||||
LANGUAGE plpgsql;
|
||||
|
||||
CREATE FUNCTION api.register (username TEXT, PASSWORD TEXT, OUT user_id UUID)
|
||||
CREATE FUNCTION api.register (username TEXT, pass TEXT, OUT user_id UUID)
|
||||
AS $$
|
||||
DECLARE
|
||||
_username_length_min CONSTANT INT := 3;
|
||||
@@ -69,12 +67,11 @@ DECLARE
|
||||
_password_length_min CONSTANT INT := 12;
|
||||
_password_length_max CONSTANT INT := 128;
|
||||
BEGIN
|
||||
IF LENGTH(register.username)
|
||||
CASE WHEN LENGTH(register.username)
|
||||
NOT BETWEEN _username_length_min AND _username_length_max THEN
|
||||
RAISE string_data_length_mismatch
|
||||
USING message = FORMAT('Username must be between %s and %s characters long', _username_length_min, _username_length_max);
|
||||
END IF;
|
||||
IF EXISTS (
|
||||
WHEN EXISTS (
|
||||
SELECT
|
||||
1
|
||||
FROM
|
||||
@@ -83,38 +80,35 @@ BEGIN
|
||||
u.username = register.username) THEN
|
||||
RAISE unique_violation
|
||||
USING message = 'Username is already taken';
|
||||
END IF;
|
||||
IF LENGTH(register.password)
|
||||
NOT BETWEEN _password_length_min AND _password_length_max THEN
|
||||
WHEN LENGTH(register.pass) NOT BETWEEN _password_length_min AND _password_length_max THEN
|
||||
RAISE string_data_length_mismatch
|
||||
USING message = FORMAT('Password must be between %s and %s characters long', _password_length_min, _password_length_max);
|
||||
END IF;
|
||||
IF register.password !~ '[a-z]' THEN
|
||||
WHEN register.pass !~ '[a-z]' THEN
|
||||
RAISE invalid_parameter_value
|
||||
USING message = 'Password must contain at least one lowercase letter';
|
||||
END IF;
|
||||
IF register.password !~ '[A-Z]' THEN
|
||||
WHEN register.pass !~ '[A-Z]' THEN
|
||||
RAISE invalid_parameter_value
|
||||
USING message = 'Password must contain at least one uppercase letter';
|
||||
END IF;
|
||||
IF register.password !~ '[0-9]' THEN
|
||||
WHEN register.pass !~ '[0-9]' THEN
|
||||
RAISE invalid_parameter_value
|
||||
USING message = 'Password must contain at least one number';
|
||||
END IF;
|
||||
IF register.password !~ '[!@#$%^&*(),.?":{}|<>]' THEN
|
||||
WHEN register.pass !~ '[!@#$%^&*(),.?":{}|<>]' THEN
|
||||
RAISE invalid_parameter_value
|
||||
USING message = 'Password must contain at least one special character';
|
||||
END IF;
|
||||
INSERT INTO internal.user (username, password_hash)
|
||||
VALUES (register.username, register.password)
|
||||
ELSE
|
||||
INSERT
|
||||
INTO internal.user (username, password_hash)
|
||||
VALUES (register.username, register.pass)
|
||||
RETURNING
|
||||
id INTO user_id;
|
||||
END;
|
||||
END
|
||||
CASE;
|
||||
END;
|
||||
$$
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER;
|
||||
|
||||
CREATE FUNCTION api.login (username TEXT, PASSWORD TEXT, OUT token TEXT)
|
||||
CREATE FUNCTION api.login (username TEXT, pass TEXT, OUT token TEXT)
|
||||
AS $$
|
||||
DECLARE
|
||||
_role NAME;
|
||||
@@ -122,7 +116,7 @@ DECLARE
|
||||
_exp INTEGER;
|
||||
BEGIN
|
||||
SELECT
|
||||
internal.user_role (login.username, login.password) INTO _role;
|
||||
internal.user_role (login.username, login.pass) INTO _role;
|
||||
IF _role IS NULL THEN
|
||||
RAISE invalid_password
|
||||
USING message = 'Invalid username or password';
|
||||
@@ -141,14 +135,14 @@ $$
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER;
|
||||
|
||||
CREATE FUNCTION api.delete_account (PASSWORD TEXT, OUT was_deleted BOOLEAN)
|
||||
CREATE FUNCTION api.delete_account (pass TEXT, OUT was_deleted BOOLEAN)
|
||||
AS $$
|
||||
DECLARE
|
||||
_username TEXT := CURRENT_SETTING('request.jwt.claims', TRUE)::JSON ->> 'username';
|
||||
_role NAME;
|
||||
BEGIN
|
||||
SELECT
|
||||
internal.user_role (_username, delete_account.password) INTO _role;
|
||||
internal.user_role (_username, delete_account.pass) INTO _role;
|
||||
IF _role IS NULL THEN
|
||||
RAISE invalid_password
|
||||
USING message = 'Invalid password';
|
||||
@@ -165,7 +159,13 @@ GRANT EXECUTE ON FUNCTION api.register (TEXT, TEXT) TO anon;
|
||||
|
||||
GRANT EXECUTE ON FUNCTION api.login (TEXT, TEXT) TO anon;
|
||||
|
||||
GRANT EXECUTE ON FUNCTION api.delete_account (TEXT) TO authenticated_user;
|
||||
|
||||
-- migrate:down
|
||||
DROP TRIGGER encrypt_pass ON internal.user;
|
||||
|
||||
DROP TRIGGER ensure_user_role_exists ON internal.user;
|
||||
|
||||
DROP FUNCTION api.register (TEXT, TEXT);
|
||||
|
||||
DROP FUNCTION api.login (TEXT, TEXT);
|
||||
@@ -174,12 +174,8 @@ DROP FUNCTION api.delete_account (TEXT);
|
||||
|
||||
DROP FUNCTION internal.user_role (TEXT, TEXT);
|
||||
|
||||
DROP TRIGGER encrypt_pass ON internal.user;
|
||||
|
||||
DROP FUNCTION internal.encrypt_pass ();
|
||||
|
||||
DROP TRIGGER ensure_user_role_exists ON internal.user;
|
||||
|
||||
DROP FUNCTION internal.check_role_exists ();
|
||||
|
||||
DROP EXTENSION pgjwt;
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
CREATE VIEW api.account WITH ( security_invoker = ON
|
||||
) AS
|
||||
SELECT
|
||||
id,
|
||||
username
|
||||
*
|
||||
FROM
|
||||
internal.user
|
||||
WHERE
|
||||
@@ -23,99 +22,70 @@ FROM
|
||||
CREATE VIEW api.website WITH ( security_invoker = ON
|
||||
) AS
|
||||
SELECT
|
||||
id,
|
||||
user_id,
|
||||
content_type,
|
||||
title,
|
||||
created_at,
|
||||
last_modified_at,
|
||||
last_modified_by
|
||||
*
|
||||
FROM
|
||||
internal.website;
|
||||
|
||||
CREATE VIEW api.settings WITH ( security_invoker = ON
|
||||
) AS
|
||||
SELECT
|
||||
website_id,
|
||||
accent_color_light_theme,
|
||||
accent_color_dark_theme,
|
||||
favicon_image,
|
||||
last_modified_at,
|
||||
last_modified_by
|
||||
*
|
||||
FROM
|
||||
internal.settings;
|
||||
|
||||
CREATE VIEW api.header WITH ( security_invoker = ON
|
||||
) AS
|
||||
SELECT
|
||||
website_id,
|
||||
logo_type,
|
||||
logo_text,
|
||||
logo_image,
|
||||
last_modified_at,
|
||||
last_modified_by
|
||||
*
|
||||
FROM
|
||||
internal.header;
|
||||
|
||||
CREATE VIEW api.home WITH ( security_invoker = ON
|
||||
) AS
|
||||
SELECT
|
||||
website_id,
|
||||
main_content,
|
||||
last_modified_at,
|
||||
last_modified_by
|
||||
*
|
||||
FROM
|
||||
internal.home;
|
||||
|
||||
CREATE VIEW api.article WITH ( security_invoker = ON
|
||||
) AS
|
||||
SELECT
|
||||
id,
|
||||
website_id,
|
||||
user_id,
|
||||
title,
|
||||
meta_description,
|
||||
meta_author,
|
||||
cover_image,
|
||||
publication_date,
|
||||
main_content,
|
||||
created_at,
|
||||
last_modified_at,
|
||||
last_modified_by
|
||||
*
|
||||
FROM
|
||||
internal.article;
|
||||
|
||||
CREATE VIEW api.docs_category WITH ( security_invoker = ON
|
||||
) AS
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
internal.docs_category;
|
||||
|
||||
CREATE VIEW api.footer WITH ( security_invoker = ON
|
||||
) AS
|
||||
SELECT
|
||||
website_id,
|
||||
additional_text,
|
||||
last_modified_at,
|
||||
last_modified_by
|
||||
*
|
||||
FROM
|
||||
internal.footer;
|
||||
|
||||
CREATE VIEW api.legal_information WITH ( security_invoker = ON
|
||||
) AS
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
internal.legal_information;
|
||||
|
||||
CREATE VIEW api.collab WITH ( security_invoker = ON
|
||||
) AS
|
||||
SELECT
|
||||
website_id,
|
||||
user_id,
|
||||
permission_level,
|
||||
added_at,
|
||||
last_modified_at,
|
||||
last_modified_by
|
||||
*
|
||||
FROM
|
||||
internal.collab;
|
||||
|
||||
CREATE VIEW api.change_log WITH ( security_invoker = ON
|
||||
) AS
|
||||
SELECT
|
||||
website_id,
|
||||
user_id,
|
||||
change_summary,
|
||||
previous_value,
|
||||
new_value,
|
||||
timestamp
|
||||
*
|
||||
FROM
|
||||
internal.change_log;
|
||||
|
||||
@@ -123,9 +93,8 @@ CREATE FUNCTION api.create_website (content_type VARCHAR(10), title VARCHAR(50),
|
||||
AS $$
|
||||
DECLARE
|
||||
_website_id UUID;
|
||||
_user_id UUID;
|
||||
_user_id UUID := (CURRENT_SETTING('request.jwt.claims', TRUE)::JSON ->> 'user_id')::UUID;
|
||||
BEGIN
|
||||
_user_id := (CURRENT_SETTING('request.jwt.claims', TRUE)::JSON ->> 'user_id')::UUID;
|
||||
INSERT INTO internal.website (content_type, title)
|
||||
VALUES (create_website.content_type, create_website.title)
|
||||
RETURNING
|
||||
@@ -135,8 +104,7 @@ BEGIN
|
||||
INSERT INTO internal.header (website_id, logo_text)
|
||||
VALUES (_website_id, 'archtika ' || create_website.content_type);
|
||||
INSERT INTO internal.home (website_id, main_content)
|
||||
VALUES (_website_id, '
|
||||
## About
|
||||
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.
|
||||
|
||||
@@ -148,8 +116,7 @@ For the backend, PostgreSQL is used in combination with PostgREST to create a RE
|
||||
|
||||
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 `<user_id>/<website_id>`, which is dynamically created by the web application.
|
||||
');
|
||||
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 `<user_id>/<website_id>`, which is dynamically created by the web application.');
|
||||
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;
|
||||
@@ -187,10 +154,18 @@ GRANT SELECT, INSERT, UPDATE, DELETE ON internal.article TO authenticated_user;
|
||||
|
||||
GRANT SELECT, INSERT, UPDATE, DELETE ON api.article TO authenticated_user;
|
||||
|
||||
GRANT SELECT, INSERT, UPDATE, DELETE ON internal.docs_category TO authenticated_user;
|
||||
|
||||
GRANT SELECT, INSERT, UPDATE, DELETE ON api.docs_category TO authenticated_user;
|
||||
|
||||
GRANT SELECT, UPDATE ON internal.footer TO authenticated_user;
|
||||
|
||||
GRANT SELECT, UPDATE ON api.footer TO authenticated_user;
|
||||
|
||||
GRANT SELECT, INSERT, UPDATE, DELETE ON internal.legal_information TO authenticated_user;
|
||||
|
||||
GRANT SELECT, INSERT, UPDATE, DELETE ON api.legal_information TO authenticated_user;
|
||||
|
||||
GRANT SELECT, INSERT, UPDATE, DELETE ON internal.collab TO authenticated_user;
|
||||
|
||||
GRANT SELECT, INSERT, UPDATE, DELETE ON api.collab TO authenticated_user;
|
||||
@@ -206,10 +181,14 @@ DROP VIEW api.change_log;
|
||||
|
||||
DROP VIEW api.collab;
|
||||
|
||||
DROP VIEW api.legal_information;
|
||||
|
||||
DROP VIEW api.footer;
|
||||
|
||||
DROP VIEW api.home;
|
||||
|
||||
DROP VIEW api.docs_category;
|
||||
|
||||
DROP VIEW api.article;
|
||||
|
||||
DROP VIEW api.header;
|
||||
|
||||
@@ -13,29 +13,30 @@ ALTER TABLE internal.home ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
ALTER TABLE internal.article ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
ALTER TABLE internal.docs_category ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
ALTER TABLE internal.footer ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
ALTER TABLE internal.legal_information ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
ALTER TABLE internal.collab ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
CREATE FUNCTION internal.user_has_website_access (website_id UUID, required_permission INTEGER, collaborator_permission_level INTEGER DEFAULT NULL, collaborator_user_id UUID DEFAULT NULL, article_user_id UUID DEFAULT NULL, raise_error BOOLEAN DEFAULT TRUE)
|
||||
RETURNS BOOLEAN
|
||||
AS $$
|
||||
CREATE FUNCTION internal.user_has_website_access (website_id UUID, required_permission INTEGER, collaborator_permission_level INTEGER DEFAULT NULL, collaborator_user_id UUID DEFAULT NULL, article_user_id UUID DEFAULT NULL, raise_error BOOLEAN DEFAULT TRUE, OUT has_access BOOLEAN)
|
||||
AS $$
|
||||
DECLARE
|
||||
_user_id UUID;
|
||||
_has_access BOOLEAN;
|
||||
_user_id UUID := (CURRENT_SETTING('request.jwt.claims', TRUE)::JSON ->> 'user_id')::UUID;
|
||||
BEGIN
|
||||
_user_id := (CURRENT_SETTING('request.jwt.claims', TRUE)::JSON ->> 'user_id')::UUID;
|
||||
SELECT
|
||||
EXISTS (
|
||||
SELECT
|
||||
1
|
||||
FROM
|
||||
internal.website
|
||||
internal.website AS w
|
||||
WHERE
|
||||
id = website_id
|
||||
AND user_id = _user_id) INTO _has_access;
|
||||
IF _has_access THEN
|
||||
RETURN _has_access;
|
||||
w.id = user_has_website_access.website_id
|
||||
AND w.user_id = _user_id) INTO has_access;
|
||||
IF has_access THEN
|
||||
RETURN;
|
||||
END IF;
|
||||
SELECT
|
||||
EXISTS (
|
||||
@@ -45,24 +46,25 @@ BEGIN
|
||||
internal.collab c
|
||||
WHERE
|
||||
c.website_id = user_has_website_access.website_id
|
||||
AND c.user_id = (CURRENT_SETTING('request.jwt.claims', TRUE)::JSON ->> 'user_id')::UUID
|
||||
AND c.user_id = _user_id
|
||||
AND c.permission_level >= user_has_website_access.required_permission
|
||||
AND (user_has_website_access.article_user_id IS NULL
|
||||
OR (c.permission_level = 30
|
||||
OR user_has_website_access.article_user_id = _user_id))
|
||||
AND (user_has_website_access.collaborator_permission_level IS NULL
|
||||
OR (user_has_website_access.collaborator_user_id != _user_id
|
||||
AND user_has_website_access.collaborator_permission_level < 30))) INTO _has_access;
|
||||
IF NOT _has_access AND user_has_website_access.raise_error THEN
|
||||
AND user_has_website_access.collaborator_permission_level < 30))) INTO has_access;
|
||||
IF NOT has_access AND user_has_website_access.raise_error THEN
|
||||
RAISE insufficient_privilege
|
||||
USING message = 'You do not have the required permissions for this action.';
|
||||
END IF;
|
||||
RETURN _has_access;
|
||||
END;
|
||||
$$
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER;
|
||||
|
||||
GRANT EXECUTE ON FUNCTION internal.user_has_website_access (UUID, INTEGER, INTEGER, UUID, UUID, BOOLEAN) TO authenticated_user;
|
||||
|
||||
CREATE POLICY view_user ON internal.user
|
||||
FOR SELECT
|
||||
USING (TRUE);
|
||||
@@ -127,6 +129,22 @@ CREATE POLICY insert_article ON internal.article
|
||||
FOR INSERT
|
||||
WITH CHECK (internal.user_has_website_access (website_id, 20));
|
||||
|
||||
CREATE POLICY view_categories ON internal.docs_category
|
||||
FOR SELECT
|
||||
USING (internal.user_has_website_access (website_id, 10));
|
||||
|
||||
CREATE POLICY update_category ON internal.docs_category
|
||||
FOR UPDATE
|
||||
USING (internal.user_has_website_access (website_id, 20));
|
||||
|
||||
CREATE POLICY delete_category ON internal.docs_category
|
||||
FOR DELETE
|
||||
USING (internal.user_has_website_access (website_id, 20, article_user_id => user_id));
|
||||
|
||||
CREATE POLICY insert_category ON internal.docs_category
|
||||
FOR INSERT
|
||||
WITH CHECK (internal.user_has_website_access (website_id, 20));
|
||||
|
||||
CREATE POLICY view_footer ON internal.footer
|
||||
FOR SELECT
|
||||
USING (internal.user_has_website_access (website_id, 10));
|
||||
@@ -135,6 +153,22 @@ CREATE POLICY update_footer ON internal.footer
|
||||
FOR UPDATE
|
||||
USING (internal.user_has_website_access (website_id, 20));
|
||||
|
||||
CREATE POLICY view_legal_information ON internal.legal_information
|
||||
FOR SELECT
|
||||
USING (internal.user_has_website_access (website_id, 10));
|
||||
|
||||
CREATE POLICY update_legal_information ON internal.legal_information
|
||||
FOR UPDATE
|
||||
USING (internal.user_has_website_access (website_id, 30));
|
||||
|
||||
CREATE POLICY delete_legal_information ON internal.legal_information
|
||||
FOR DELETE
|
||||
USING (internal.user_has_website_access (website_id, 30));
|
||||
|
||||
CREATE POLICY insert_legal_information ON internal.legal_information
|
||||
FOR INSERT
|
||||
WITH CHECK (internal.user_has_website_access (website_id, 30));
|
||||
|
||||
CREATE POLICY view_collaborations ON internal.collab
|
||||
FOR SELECT
|
||||
USING (internal.user_has_website_access (website_id, 10));
|
||||
@@ -184,10 +218,26 @@ DROP POLICY delete_article ON internal.article;
|
||||
|
||||
DROP POLICY insert_article ON internal.article;
|
||||
|
||||
DROP POLICY view_categories ON internal.docs_category;
|
||||
|
||||
DROP POLICY update_category ON internal.docs_category;
|
||||
|
||||
DROP POLICY delete_category ON internal.docs_category;
|
||||
|
||||
DROP POLICY insert_category ON internal.docs_category;
|
||||
|
||||
DROP POLICY view_footer ON internal.footer;
|
||||
|
||||
DROP POLICY update_footer ON internal.footer;
|
||||
|
||||
DROP POLICY insert_legal_information ON internal.legal_information;
|
||||
|
||||
DROP POLICY delete_legal_information ON internal.legal_information;
|
||||
|
||||
DROP POLICY update_legal_information ON internal.legal_information;
|
||||
|
||||
DROP POLICY view_legal_information ON internal.legal_information;
|
||||
|
||||
DROP POLICY view_collaborations ON internal.collab;
|
||||
|
||||
DROP POLICY insert_collaborations ON internal.collab;
|
||||
@@ -212,7 +262,11 @@ ALTER TABLE internal.home DISABLE ROW LEVEL SECURITY;
|
||||
|
||||
ALTER TABLE internal.article DISABLE ROW LEVEL SECURITY;
|
||||
|
||||
ALTER TABLE internal.docs_category DISABLE ROW LEVEL SECURITY;
|
||||
|
||||
ALTER TABLE internal.footer DISABLE ROW LEVEL SECURITY;
|
||||
|
||||
ALTER TABLE internal.legal_information DISABLE ROW LEVEL SECURITY;
|
||||
|
||||
ALTER TABLE internal.collab DISABLE ROW LEVEL SECURITY;
|
||||
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
-- migrate:up
|
||||
CREATE VIEW api.website_overview WITH ( security_invoker = ON
|
||||
) AS
|
||||
SELECT
|
||||
w.id,
|
||||
w.user_id,
|
||||
w.content_type,
|
||||
w.title,
|
||||
s.accent_color_light_theme,
|
||||
s.accent_color_dark_theme,
|
||||
s.favicon_image,
|
||||
h.logo_type,
|
||||
h.logo_text,
|
||||
h.logo_image,
|
||||
ho.main_content,
|
||||
f.additional_text,
|
||||
(
|
||||
SELECT
|
||||
JSON_AGG(
|
||||
JSON_BUILD_OBJECT(
|
||||
'title', a.title, 'meta_description', a.meta_description, 'meta_author', a.meta_author, 'cover_image', a.cover_image, 'publication_date', a.publication_date, 'main_content', a.main_content
|
||||
)
|
||||
)
|
||||
FROM
|
||||
internal.article a
|
||||
WHERE
|
||||
a.website_id = w.id
|
||||
) AS articles
|
||||
FROM
|
||||
internal.website w
|
||||
JOIN internal.settings s ON w.id = s.website_id
|
||||
JOIN internal.header h ON w.id = h.website_id
|
||||
JOIN internal.home ho ON w.id = ho.website_id
|
||||
JOIN internal.footer f ON w.id = f.website_id;
|
||||
|
||||
GRANT SELECT ON api.website_overview TO authenticated_user;
|
||||
|
||||
-- migrate:down
|
||||
DROP VIEW api.website_overview;
|
||||
|
||||
@@ -12,25 +12,12 @@ BEGIN
|
||||
last_modified_at = NEW.last_modified_at,
|
||||
last_modified_by = NEW.last_modified_by
|
||||
WHERE
|
||||
id = CASE WHEN TG_TABLE_NAME = 'settings' THEN
|
||||
NEW.website_id
|
||||
WHEN TG_TABLE_NAME = 'header' THEN
|
||||
NEW.website_id
|
||||
WHEN TG_TABLE_NAME = 'home' THEN
|
||||
NEW.website_id
|
||||
WHEN TG_TABLE_NAME = 'article' THEN
|
||||
NEW.website_id
|
||||
WHEN TG_TABLE_NAME = 'footer' THEN
|
||||
NEW.website_id
|
||||
WHEN TG_TABLE_NAME = 'collab' THEN
|
||||
NEW.website_id
|
||||
END;
|
||||
id = NEW.website_id;
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER;
|
||||
LANGUAGE plpgsql;
|
||||
|
||||
CREATE TRIGGER update_website_last_modified
|
||||
BEFORE UPDATE ON internal.website
|
||||
@@ -62,6 +49,11 @@ CREATE TRIGGER update_footer_last_modified
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION internal.update_last_modified ();
|
||||
|
||||
CREATE TRIGGER update_legal_information_last_modified
|
||||
BEFORE INSERT OR UPDATE OR DELETE ON internal.legal_information
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION internal.update_last_modified ();
|
||||
|
||||
CREATE TRIGGER update_collab_last_modified
|
||||
BEFORE UPDATE ON internal.collab
|
||||
FOR EACH ROW
|
||||
@@ -80,6 +72,8 @@ DROP TRIGGER update_article_last_modified ON internal.article;
|
||||
|
||||
DROP TRIGGER update_footer_last_modified ON internal.footer;
|
||||
|
||||
DROP TRIGGER update_legal_information_last_modified ON internal.legal_information;
|
||||
|
||||
DROP TRIGGER update_collab_last_modified ON internal.collab;
|
||||
|
||||
DROP FUNCTION internal.update_last_modified ();
|
||||
|
||||
@@ -7,10 +7,10 @@ BEGIN
|
||||
SELECT
|
||||
1
|
||||
FROM
|
||||
internal.website
|
||||
internal.website AS w
|
||||
WHERE
|
||||
id = NEW.website_id
|
||||
AND user_id = NEW.user_id) THEN
|
||||
w.id = NEW.website_id
|
||||
AND w.user_id = NEW.user_id) THEN
|
||||
RAISE foreign_key_violation
|
||||
USING message = 'User cannot be added as a collaborator to their own website';
|
||||
END IF;
|
||||
|
||||
@@ -10,7 +10,9 @@ DECLARE
|
||||
_original_filename TEXT := _headers ->> 'x-original-filename';
|
||||
_allowed_mimetypes TEXT[] := ARRAY['image/png', 'image/jpeg', 'image/webp'];
|
||||
_max_file_size INT := 5 * 1024 * 1024;
|
||||
_has_access BOOLEAN;
|
||||
BEGIN
|
||||
_has_access = internal.user_has_website_access (_website_id, 20);
|
||||
IF OCTET_LENGTH($1) = 0 THEN
|
||||
RAISE invalid_parameter_value
|
||||
USING message = 'No file data was provided';
|
||||
@@ -19,7 +21,7 @@ BEGIN
|
||||
SELECT
|
||||
UNNEST(_allowed_mimetypes)) THEN
|
||||
RAISE invalid_parameter_value
|
||||
USING message = 'Invalid MIME type. Allowed types are: png, svg, jpg, webp';
|
||||
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
|
||||
@@ -46,7 +48,7 @@ BEGIN
|
||||
'{ "Content-Disposition": "inline; filename=\"%s\"" },'
|
||||
'{ "Cache-Control": "max-age=259200" }]', m.mimetype, m.original_name)
|
||||
FROM
|
||||
internal.media m
|
||||
internal.media AS m
|
||||
WHERE
|
||||
m.id = retrieve_file.id INTO _headers;
|
||||
PERFORM
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
-- migrate:up
|
||||
ALTER TABLE internal.website
|
||||
ADD COLUMN title_search TSVECTOR GENERATED ALWAYS AS (TO_TSVECTOR('english', title)) STORED;
|
||||
|
||||
CREATE OR REPLACE VIEW api.website WITH ( security_invoker = ON
|
||||
) AS
|
||||
SELECT
|
||||
id,
|
||||
user_id,
|
||||
content_type,
|
||||
title,
|
||||
created_at,
|
||||
last_modified_at,
|
||||
last_modified_by,
|
||||
title_search -- New column
|
||||
FROM
|
||||
internal.website;
|
||||
|
||||
GRANT SELECT, UPDATE, DELETE ON api.website TO authenticated_user;
|
||||
|
||||
ALTER TABLE internal.article
|
||||
ADD COLUMN title_description_search TSVECTOR GENERATED ALWAYS AS (TO_TSVECTOR('english', COALESCE(title, '') || ' ' || COALESCE(meta_description, ''))) STORED;
|
||||
|
||||
CREATE OR REPLACE VIEW api.article WITH ( security_invoker = ON
|
||||
) AS
|
||||
SELECT
|
||||
id,
|
||||
website_id,
|
||||
user_id,
|
||||
title,
|
||||
meta_description,
|
||||
meta_author,
|
||||
cover_image,
|
||||
publication_date,
|
||||
main_content,
|
||||
created_at,
|
||||
last_modified_at,
|
||||
last_modified_by,
|
||||
title_description_search -- New column
|
||||
FROM
|
||||
internal.article;
|
||||
|
||||
GRANT SELECT, INSERT, UPDATE, DELETE ON api.article TO authenticated_user;
|
||||
|
||||
-- migrate:down
|
||||
DROP VIEW api.article;
|
||||
|
||||
CREATE VIEW api.article WITH ( security_invoker = ON
|
||||
) AS
|
||||
SELECT
|
||||
id,
|
||||
website_id,
|
||||
user_id,
|
||||
title,
|
||||
meta_description,
|
||||
meta_author,
|
||||
cover_image,
|
||||
publication_date,
|
||||
main_content,
|
||||
created_at,
|
||||
last_modified_at,
|
||||
last_modified_by
|
||||
FROM
|
||||
internal.article;
|
||||
|
||||
ALTER TABLE internal.article
|
||||
DROP COLUMN title_description_search;
|
||||
|
||||
DROP VIEW api.website;
|
||||
|
||||
CREATE VIEW api.website WITH ( security_invoker = ON
|
||||
) AS
|
||||
SELECT
|
||||
id,
|
||||
user_id,
|
||||
content_type,
|
||||
title,
|
||||
created_at,
|
||||
last_modified_at,
|
||||
last_modified_by
|
||||
FROM
|
||||
internal.website;
|
||||
|
||||
ALTER TABLE internal.website
|
||||
DROP COLUMN title_search;
|
||||
|
||||
GRANT SELECT, UPDATE, DELETE ON api.website TO authenticated_user;
|
||||
|
||||
GRANT SELECT, INSERT, UPDATE, DELETE ON api.article TO authenticated_user;
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
-- 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;
|
||||
|
||||
@@ -1,120 +0,0 @@
|
||||
-- migrate:up
|
||||
CREATE TABLE internal.docs_category (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid (),
|
||||
website_id UUID REFERENCES internal.website (id) ON DELETE CASCADE NOT NULL,
|
||||
user_id UUID REFERENCES internal.user (id) ON DELETE SET NULL DEFAULT (CURRENT_SETTING('request.jwt.claims', TRUE)::JSON ->> 'user_id') ::UUID,
|
||||
category_name VARCHAR(50) NOT NULL CHECK (TRIM(category_name) != ''),
|
||||
category_weight INTEGER CHECK (category_weight >= 0) NOT NULL,
|
||||
UNIQUE (website_id, category_name),
|
||||
UNIQUE (website_id, category_weight)
|
||||
);
|
||||
|
||||
ALTER TABLE internal.website
|
||||
ADD COLUMN is_published BOOLEAN NOT NULL DEFAULT FALSE;
|
||||
|
||||
ALTER TABLE internal.article
|
||||
ADD COLUMN category UUID REFERENCES internal.docs_category (id) ON DELETE SET NULL;
|
||||
|
||||
ALTER TABLE internal.article
|
||||
ALTER COLUMN user_id SET DEFAULT (CURRENT_SETTING('request.jwt.claims', TRUE)::JSON ->> 'user_id')::UUID;
|
||||
|
||||
ALTER TABLE internal.docs_category ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
CREATE POLICY view_categories ON internal.docs_category
|
||||
FOR SELECT
|
||||
USING (internal.user_has_website_access (website_id, 10));
|
||||
|
||||
CREATE POLICY update_category ON internal.docs_category
|
||||
FOR UPDATE
|
||||
USING (internal.user_has_website_access (website_id, 20));
|
||||
|
||||
CREATE POLICY delete_category ON internal.docs_category
|
||||
FOR DELETE
|
||||
USING (internal.user_has_website_access (website_id, 20, article_user_id => user_id));
|
||||
|
||||
CREATE POLICY insert_category ON internal.docs_category
|
||||
FOR INSERT
|
||||
WITH CHECK (internal.user_has_website_access (website_id, 20));
|
||||
|
||||
CREATE VIEW api.docs_category WITH ( security_invoker = ON
|
||||
) AS
|
||||
SELECT
|
||||
id,
|
||||
website_id,
|
||||
user_id,
|
||||
category_name,
|
||||
category_weight
|
||||
FROM
|
||||
internal.docs_category;
|
||||
|
||||
CREATE OR REPLACE VIEW api.article WITH ( security_invoker = ON
|
||||
) AS
|
||||
SELECT
|
||||
id,
|
||||
website_id,
|
||||
user_id,
|
||||
title,
|
||||
meta_description,
|
||||
meta_author,
|
||||
cover_image,
|
||||
publication_date,
|
||||
main_content,
|
||||
created_at,
|
||||
last_modified_at,
|
||||
last_modified_by,
|
||||
title_description_search,
|
||||
category -- New column
|
||||
FROM
|
||||
internal.article;
|
||||
|
||||
GRANT SELECT, INSERT, UPDATE, DELETE ON internal.docs_category TO authenticated_user;
|
||||
|
||||
GRANT SELECT, INSERT, UPDATE, DELETE ON api.docs_category TO authenticated_user;
|
||||
|
||||
GRANT SELECT, INSERT, UPDATE, DELETE ON api.article TO authenticated_user;
|
||||
|
||||
-- migrate:down
|
||||
DROP POLICY view_categories ON internal.docs_category;
|
||||
|
||||
DROP POLICY update_category ON internal.docs_category;
|
||||
|
||||
DROP POLICY delete_category ON internal.docs_category;
|
||||
|
||||
DROP POLICY insert_category ON internal.docs_category;
|
||||
|
||||
DROP VIEW api.article;
|
||||
|
||||
CREATE VIEW api.article WITH ( security_invoker = ON
|
||||
) AS
|
||||
SELECT
|
||||
id,
|
||||
website_id,
|
||||
user_id,
|
||||
title,
|
||||
meta_description,
|
||||
meta_author,
|
||||
cover_image,
|
||||
publication_date,
|
||||
main_content,
|
||||
created_at,
|
||||
last_modified_at,
|
||||
last_modified_by,
|
||||
title_description_search
|
||||
FROM
|
||||
internal.article;
|
||||
|
||||
GRANT SELECT, INSERT, UPDATE, DELETE ON api.article TO authenticated_user;
|
||||
|
||||
DROP VIEW api.docs_category;
|
||||
|
||||
ALTER TABLE internal.article
|
||||
DROP COLUMN category;
|
||||
|
||||
DROP TABLE internal.docs_category;
|
||||
|
||||
ALTER TABLE internal.website
|
||||
DROP COLUMN is_published;
|
||||
|
||||
ALTER TABLE internal.article
|
||||
ALTER COLUMN user_id DROP DEFAULT;
|
||||
|
||||
@@ -1,108 +0,0 @@
|
||||
-- migrate:up
|
||||
CREATE OR REPLACE VIEW api.website_overview WITH ( security_invoker = ON
|
||||
) AS
|
||||
SELECT
|
||||
w.id,
|
||||
w.user_id,
|
||||
w.content_type,
|
||||
w.title,
|
||||
s.accent_color_light_theme,
|
||||
s.accent_color_dark_theme,
|
||||
s.favicon_image,
|
||||
h.logo_type,
|
||||
h.logo_text,
|
||||
h.logo_image,
|
||||
ho.main_content,
|
||||
f.additional_text,
|
||||
(
|
||||
SELECT
|
||||
JSON_AGG(
|
||||
JSON_BUILD_OBJECT(
|
||||
'id', a.id, 'title', a.title, 'meta_description', a.meta_description, 'meta_author', a.meta_author, 'cover_image', a.cover_image, 'publication_date', a.publication_date, 'main_content', a.main_content, 'created_at', a.created_at, 'last_modified_at', a.last_modified_at
|
||||
)
|
||||
)
|
||||
FROM
|
||||
internal.article a
|
||||
WHERE
|
||||
a.website_id = w.id
|
||||
) AS articles,
|
||||
CASE WHEN w.content_type = 'Docs' THEN
|
||||
(
|
||||
SELECT
|
||||
JSON_OBJECT_AGG(
|
||||
COALESCE(
|
||||
category_name, 'Uncategorized'
|
||||
), articles
|
||||
)
|
||||
FROM (
|
||||
SELECT
|
||||
dc.category_name,
|
||||
dc.category_weight AS category_weight,
|
||||
JSON_AGG(
|
||||
JSON_BUILD_OBJECT(
|
||||
'id', a.id, 'title', a.title, 'meta_description', a.meta_description, 'meta_author', a.meta_author, 'cover_image', a.cover_image, 'publication_date', a.publication_date, 'main_content', a.main_content, 'created_at', a.created_at, 'last_modified_at', a.last_modified_at
|
||||
)
|
||||
) AS articles
|
||||
FROM
|
||||
internal.article a
|
||||
LEFT JOIN internal.docs_category dc ON a.category = dc.id
|
||||
WHERE
|
||||
a.website_id = w.id
|
||||
GROUP BY
|
||||
dc.id,
|
||||
dc.category_name,
|
||||
dc.category_weight
|
||||
ORDER BY
|
||||
category_weight DESC
|
||||
) AS categorized_articles)
|
||||
ELSE
|
||||
NULL
|
||||
END AS categorized_articles
|
||||
FROM
|
||||
internal.website w
|
||||
JOIN internal.settings s ON w.id = s.website_id
|
||||
JOIN internal.header h ON w.id = h.website_id
|
||||
JOIN internal.home ho ON w.id = ho.website_id
|
||||
JOIN internal.footer f ON w.id = f.website_id;
|
||||
|
||||
GRANT SELECT ON api.website_overview TO authenticated_user;
|
||||
|
||||
-- migrate:down
|
||||
DROP VIEW api.website_overview;
|
||||
|
||||
CREATE VIEW api.website_overview WITH ( security_invoker = ON
|
||||
) AS
|
||||
SELECT
|
||||
w.id,
|
||||
w.user_id,
|
||||
w.content_type,
|
||||
w.title,
|
||||
s.accent_color_light_theme,
|
||||
s.accent_color_dark_theme,
|
||||
s.favicon_image,
|
||||
h.logo_type,
|
||||
h.logo_text,
|
||||
h.logo_image,
|
||||
ho.main_content,
|
||||
f.additional_text,
|
||||
(
|
||||
SELECT
|
||||
JSON_AGG(
|
||||
JSON_BUILD_OBJECT(
|
||||
'title', a.title, 'meta_description', a.meta_description, 'meta_author', a.meta_author, 'cover_image', a.cover_image, 'publication_date', a.publication_date, 'main_content', a.main_content
|
||||
)
|
||||
)
|
||||
FROM
|
||||
internal.article a
|
||||
WHERE
|
||||
a.website_id = w.id
|
||||
) AS articles
|
||||
FROM
|
||||
internal.website w
|
||||
JOIN internal.settings s ON w.id = s.website_id
|
||||
JOIN internal.header h ON w.id = h.website_id
|
||||
JOIN internal.home ho ON w.id = ho.website_id
|
||||
JOIN internal.footer f ON w.id = f.website_id;
|
||||
|
||||
GRANT SELECT ON api.website_overview TO authenticated_user;
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
-- migrate:up
|
||||
ALTER TABLE internal.article
|
||||
ADD COLUMN article_weight INTEGER CHECK (article_weight IS NULL
|
||||
OR article_weight >= 0);
|
||||
|
||||
CREATE OR REPLACE VIEW api.article WITH ( security_invoker = ON
|
||||
) AS
|
||||
SELECT
|
||||
id,
|
||||
website_id,
|
||||
user_id,
|
||||
title,
|
||||
meta_description,
|
||||
meta_author,
|
||||
cover_image,
|
||||
publication_date,
|
||||
main_content,
|
||||
created_at,
|
||||
last_modified_at,
|
||||
last_modified_by,
|
||||
title_description_search,
|
||||
category,
|
||||
article_weight -- New column
|
||||
FROM
|
||||
internal.article;
|
||||
|
||||
GRANT SELECT, INSERT, UPDATE, DELETE ON api.article TO authenticated_user;
|
||||
|
||||
-- migrate:down
|
||||
DROP VIEW api.article;
|
||||
|
||||
CREATE VIEW api.article WITH ( security_invoker = ON
|
||||
) AS
|
||||
SELECT
|
||||
id,
|
||||
website_id,
|
||||
user_id,
|
||||
title,
|
||||
meta_description,
|
||||
meta_author,
|
||||
cover_image,
|
||||
publication_date,
|
||||
main_content,
|
||||
created_at,
|
||||
last_modified_at,
|
||||
last_modified_by,
|
||||
title_description_search,
|
||||
category
|
||||
FROM
|
||||
internal.article;
|
||||
|
||||
ALTER TABLE internal.article
|
||||
DROP COLUMN article_weight;
|
||||
|
||||
GRANT SELECT, INSERT, UPDATE, DELETE ON api.article TO authenticated_user;
|
||||
|
||||
@@ -1,140 +0,0 @@
|
||||
-- migrate:up
|
||||
CREATE OR REPLACE VIEW api.website_overview WITH ( security_invoker = ON
|
||||
) AS
|
||||
SELECT
|
||||
w.id,
|
||||
w.user_id,
|
||||
w.content_type,
|
||||
w.title,
|
||||
s.accent_color_light_theme,
|
||||
s.accent_color_dark_theme,
|
||||
s.favicon_image,
|
||||
h.logo_type,
|
||||
h.logo_text,
|
||||
h.logo_image,
|
||||
ho.main_content,
|
||||
f.additional_text,
|
||||
(
|
||||
SELECT
|
||||
JSON_AGG(
|
||||
JSON_BUILD_OBJECT(
|
||||
'id', a.id, 'title', a.title, 'meta_description', a.meta_description, 'meta_author', a.meta_author, 'cover_image', a.cover_image, 'publication_date', a.publication_date, 'main_content', a.main_content, 'created_at', a.created_at, 'last_modified_at', a.last_modified_at
|
||||
)
|
||||
)
|
||||
FROM
|
||||
internal.article a
|
||||
WHERE
|
||||
a.website_id = w.id
|
||||
) AS articles,
|
||||
CASE WHEN w.content_type = 'Docs' THEN
|
||||
(
|
||||
SELECT
|
||||
JSON_OBJECT_AGG(
|
||||
COALESCE(
|
||||
category_name, 'Uncategorized'
|
||||
), articles
|
||||
)
|
||||
FROM (
|
||||
SELECT
|
||||
dc.category_name,
|
||||
dc.category_weight AS category_weight,
|
||||
JSON_AGG(
|
||||
JSON_BUILD_OBJECT(
|
||||
'id', a.id, 'title', a.title, 'meta_description', a.meta_description, 'meta_author', a.meta_author, 'cover_image', a.cover_image, 'publication_date', a.publication_date, 'main_content', a.main_content, 'created_at', a.created_at, 'last_modified_at', a.last_modified_at
|
||||
)
|
||||
) AS articles
|
||||
FROM
|
||||
internal.article a
|
||||
LEFT JOIN internal.docs_category dc ON a.category = dc.id
|
||||
WHERE
|
||||
a.website_id = w.id
|
||||
GROUP BY
|
||||
dc.id,
|
||||
dc.category_name,
|
||||
dc.category_weight
|
||||
ORDER BY
|
||||
category_weight DESC NULLS LAST
|
||||
) AS categorized_articles)
|
||||
ELSE
|
||||
NULL
|
||||
END AS categorized_articles
|
||||
FROM
|
||||
internal.website w
|
||||
JOIN internal.settings s ON w.id = s.website_id
|
||||
JOIN internal.header h ON w.id = h.website_id
|
||||
JOIN internal.home ho ON w.id = ho.website_id
|
||||
JOIN internal.footer f ON w.id = f.website_id;
|
||||
|
||||
GRANT SELECT ON api.website_overview TO authenticated_user;
|
||||
|
||||
-- migrate:down
|
||||
DROP VIEW api.website_overview;
|
||||
|
||||
CREATE VIEW api.website_overview WITH ( security_invoker = ON
|
||||
) AS
|
||||
SELECT
|
||||
w.id,
|
||||
w.user_id,
|
||||
w.content_type,
|
||||
w.title,
|
||||
s.accent_color_light_theme,
|
||||
s.accent_color_dark_theme,
|
||||
s.favicon_image,
|
||||
h.logo_type,
|
||||
h.logo_text,
|
||||
h.logo_image,
|
||||
ho.main_content,
|
||||
f.additional_text,
|
||||
(
|
||||
SELECT
|
||||
JSON_AGG(
|
||||
JSON_BUILD_OBJECT(
|
||||
'id', a.id, 'title', a.title, 'meta_description', a.meta_description, 'meta_author', a.meta_author, 'cover_image', a.cover_image, 'publication_date', a.publication_date, 'main_content', a.main_content, 'created_at', a.created_at, 'last_modified_at', a.last_modified_at
|
||||
)
|
||||
)
|
||||
FROM
|
||||
internal.article a
|
||||
WHERE
|
||||
a.website_id = w.id
|
||||
) AS articles,
|
||||
CASE WHEN w.content_type = 'Docs' THEN
|
||||
(
|
||||
SELECT
|
||||
JSON_OBJECT_AGG(
|
||||
COALESCE(
|
||||
category_name, 'Uncategorized'
|
||||
), articles
|
||||
)
|
||||
FROM (
|
||||
SELECT
|
||||
dc.category_name,
|
||||
dc.category_weight AS category_weight,
|
||||
JSON_AGG(
|
||||
JSON_BUILD_OBJECT(
|
||||
'id', a.id, 'title', a.title, 'meta_description', a.meta_description, 'meta_author', a.meta_author, 'cover_image', a.cover_image, 'publication_date', a.publication_date, 'main_content', a.main_content, 'created_at', a.created_at, 'last_modified_at', a.last_modified_at
|
||||
)
|
||||
) AS articles
|
||||
FROM
|
||||
internal.article a
|
||||
LEFT JOIN internal.docs_category dc ON a.category = dc.id
|
||||
WHERE
|
||||
a.website_id = w.id
|
||||
GROUP BY
|
||||
dc.id,
|
||||
dc.category_name,
|
||||
dc.category_weight
|
||||
ORDER BY
|
||||
category_weight DESC
|
||||
) AS categorized_articles)
|
||||
ELSE
|
||||
NULL
|
||||
END AS categorized_articles
|
||||
FROM
|
||||
internal.website w
|
||||
JOIN internal.settings s ON w.id = s.website_id
|
||||
JOIN internal.header h ON w.id = h.website_id
|
||||
JOIN internal.home ho ON w.id = ho.website_id
|
||||
JOIN internal.footer f ON w.id = f.website_id;
|
||||
|
||||
GRANT SELECT ON api.website_overview TO authenticated_user;
|
||||
|
||||
@@ -1,140 +0,0 @@
|
||||
-- migrate:up
|
||||
CREATE OR REPLACE VIEW api.website_overview WITH ( security_invoker = ON
|
||||
) AS
|
||||
SELECT
|
||||
w.id,
|
||||
w.user_id,
|
||||
w.content_type,
|
||||
w.title,
|
||||
s.accent_color_light_theme,
|
||||
s.accent_color_dark_theme,
|
||||
s.favicon_image,
|
||||
h.logo_type,
|
||||
h.logo_text,
|
||||
h.logo_image,
|
||||
ho.main_content,
|
||||
f.additional_text,
|
||||
(
|
||||
SELECT
|
||||
JSON_AGG(
|
||||
JSON_BUILD_OBJECT(
|
||||
'id', a.id, 'title', a.title, 'meta_description', a.meta_description, 'meta_author', a.meta_author, 'cover_image', a.cover_image, 'publication_date', a.publication_date, 'main_content', a.main_content, 'created_at', a.created_at, 'last_modified_at', a.last_modified_at
|
||||
)
|
||||
)
|
||||
FROM
|
||||
internal.article a
|
||||
WHERE
|
||||
a.website_id = w.id
|
||||
) AS articles,
|
||||
CASE WHEN w.content_type = 'Docs' THEN
|
||||
(
|
||||
SELECT
|
||||
JSON_OBJECT_AGG(
|
||||
COALESCE(
|
||||
category_name, 'Uncategorized'
|
||||
), articles
|
||||
)
|
||||
FROM (
|
||||
SELECT
|
||||
dc.category_name,
|
||||
dc.category_weight AS category_weight,
|
||||
JSON_AGG(
|
||||
JSON_BUILD_OBJECT(
|
||||
'id', a.id, 'title', a.title, 'meta_description', a.meta_description, 'meta_author', a.meta_author, 'cover_image', a.cover_image, 'publication_date', a.publication_date, 'main_content', a.main_content, 'created_at', a.created_at, 'last_modified_at', a.last_modified_at
|
||||
) ORDER BY a.article_weight DESC NULLS LAST
|
||||
) AS articles
|
||||
FROM
|
||||
internal.article a
|
||||
LEFT JOIN internal.docs_category dc ON a.category = dc.id
|
||||
WHERE
|
||||
a.website_id = w.id
|
||||
GROUP BY
|
||||
dc.id,
|
||||
dc.category_name,
|
||||
dc.category_weight
|
||||
ORDER BY
|
||||
category_weight DESC NULLS LAST
|
||||
) AS categorized_articles)
|
||||
ELSE
|
||||
NULL
|
||||
END AS categorized_articles
|
||||
FROM
|
||||
internal.website w
|
||||
JOIN internal.settings s ON w.id = s.website_id
|
||||
JOIN internal.header h ON w.id = h.website_id
|
||||
JOIN internal.home ho ON w.id = ho.website_id
|
||||
JOIN internal.footer f ON w.id = f.website_id;
|
||||
|
||||
GRANT SELECT ON api.website_overview TO authenticated_user;
|
||||
|
||||
-- migrate:down
|
||||
DROP VIEW api.website_overview;
|
||||
|
||||
CREATE VIEW api.website_overview WITH ( security_invoker = ON
|
||||
) AS
|
||||
SELECT
|
||||
w.id,
|
||||
w.user_id,
|
||||
w.content_type,
|
||||
w.title,
|
||||
s.accent_color_light_theme,
|
||||
s.accent_color_dark_theme,
|
||||
s.favicon_image,
|
||||
h.logo_type,
|
||||
h.logo_text,
|
||||
h.logo_image,
|
||||
ho.main_content,
|
||||
f.additional_text,
|
||||
(
|
||||
SELECT
|
||||
JSON_AGG(
|
||||
JSON_BUILD_OBJECT(
|
||||
'id', a.id, 'title', a.title, 'meta_description', a.meta_description, 'meta_author', a.meta_author, 'cover_image', a.cover_image, 'publication_date', a.publication_date, 'main_content', a.main_content, 'created_at', a.created_at, 'last_modified_at', a.last_modified_at
|
||||
)
|
||||
)
|
||||
FROM
|
||||
internal.article a
|
||||
WHERE
|
||||
a.website_id = w.id
|
||||
) AS articles,
|
||||
CASE WHEN w.content_type = 'Docs' THEN
|
||||
(
|
||||
SELECT
|
||||
JSON_OBJECT_AGG(
|
||||
COALESCE(
|
||||
category_name, 'Uncategorized'
|
||||
), articles
|
||||
)
|
||||
FROM (
|
||||
SELECT
|
||||
dc.category_name,
|
||||
dc.category_weight AS category_weight,
|
||||
JSON_AGG(
|
||||
JSON_BUILD_OBJECT(
|
||||
'id', a.id, 'title', a.title, 'meta_description', a.meta_description, 'meta_author', a.meta_author, 'cover_image', a.cover_image, 'publication_date', a.publication_date, 'main_content', a.main_content, 'created_at', a.created_at, 'last_modified_at', a.last_modified_at
|
||||
)
|
||||
) AS articles
|
||||
FROM
|
||||
internal.article a
|
||||
LEFT JOIN internal.docs_category dc ON a.category = dc.id
|
||||
WHERE
|
||||
a.website_id = w.id
|
||||
GROUP BY
|
||||
dc.id,
|
||||
dc.category_name,
|
||||
dc.category_weight
|
||||
ORDER BY
|
||||
category_weight DESC NULLS LAST
|
||||
) AS categorized_articles)
|
||||
ELSE
|
||||
NULL
|
||||
END AS categorized_articles
|
||||
FROM
|
||||
internal.website w
|
||||
JOIN internal.settings s ON w.id = s.website_id
|
||||
JOIN internal.header h ON w.id = h.website_id
|
||||
JOIN internal.home ho ON w.id = ho.website_id
|
||||
JOIN internal.footer f ON w.id = f.website_id;
|
||||
|
||||
GRANT SELECT ON api.website_overview TO authenticated_user;
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
-- migrate:up
|
||||
ALTER TABLE internal.article
|
||||
ALTER COLUMN publication_date DROP NOT NULL;
|
||||
|
||||
-- migrate:down
|
||||
UPDATE
|
||||
internal.article
|
||||
SET
|
||||
publication_date = CURRENT_DATE
|
||||
WHERE
|
||||
publication_date IS NULL;
|
||||
|
||||
ALTER TABLE internal.article
|
||||
ALTER COLUMN publication_date SET NOT NULL;
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
-- migrate:up
|
||||
CREATE OR REPLACE VIEW api.website WITH ( security_invoker = ON
|
||||
) AS
|
||||
SELECT
|
||||
id,
|
||||
user_id,
|
||||
content_type,
|
||||
title,
|
||||
created_at,
|
||||
last_modified_at,
|
||||
last_modified_by,
|
||||
title_search,
|
||||
is_published -- New column
|
||||
FROM
|
||||
internal.website;
|
||||
|
||||
GRANT SELECT, UPDATE, DELETE ON api.website TO authenticated_user;
|
||||
|
||||
-- migrate:down
|
||||
DROP VIEW api.website;
|
||||
|
||||
CREATE OR REPLACE VIEW api.website WITH ( security_invoker = ON
|
||||
) AS
|
||||
SELECT
|
||||
id,
|
||||
user_id,
|
||||
content_type,
|
||||
title,
|
||||
created_at,
|
||||
last_modified_at,
|
||||
last_modified_by,
|
||||
title_search
|
||||
FROM
|
||||
internal.website;
|
||||
|
||||
GRANT SELECT, UPDATE, DELETE ON api.website TO authenticated_user;
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
-- 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;
|
||||
_has_access BOOLEAN;
|
||||
BEGIN
|
||||
_has_access = internal.user_has_website_access (_website_id, 20);
|
||||
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, 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;
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
-- migrate:up
|
||||
CREATE TABLE internal.legal_information (
|
||||
website_id UUID PRIMARY KEY REFERENCES internal.website (id) ON DELETE CASCADE,
|
||||
main_content TEXT NOT NULL CHECK (TRIM(main_content) != ''),
|
||||
last_modified_at TIMESTAMPTZ NOT NULL DEFAULT CLOCK_TIMESTAMP(),
|
||||
last_modified_by UUID REFERENCES internal.user (id) ON DELETE SET NULL
|
||||
);
|
||||
|
||||
CREATE VIEW api.legal_information WITH ( security_invoker = ON
|
||||
) AS
|
||||
SELECT
|
||||
website_id,
|
||||
main_content,
|
||||
last_modified_at,
|
||||
last_modified_by
|
||||
FROM
|
||||
internal.legal_information;
|
||||
|
||||
GRANT SELECT, INSERT, UPDATE, DELETE ON internal.legal_information TO authenticated_user;
|
||||
|
||||
GRANT SELECT, INSERT, UPDATE, DELETE ON api.legal_information TO authenticated_user;
|
||||
|
||||
ALTER TABLE internal.legal_information ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
CREATE POLICY view_legal_information ON internal.legal_information
|
||||
FOR SELECT
|
||||
USING (internal.user_has_website_access (website_id, 10));
|
||||
|
||||
CREATE POLICY update_legal_information ON internal.legal_information
|
||||
FOR UPDATE
|
||||
USING (internal.user_has_website_access (website_id, 30));
|
||||
|
||||
CREATE POLICY delete_legal_information ON internal.legal_information
|
||||
FOR DELETE
|
||||
USING (internal.user_has_website_access (website_id, 30));
|
||||
|
||||
CREATE POLICY insert_legal_information ON internal.legal_information
|
||||
FOR INSERT
|
||||
WITH CHECK (internal.user_has_website_access (website_id, 30));
|
||||
|
||||
-- migrate:down
|
||||
DROP POLICY insert_legal_information ON internal.legal_information;
|
||||
|
||||
DROP POLICY delete_legal_information ON internal.legal_information;
|
||||
|
||||
DROP POLICY update_legal_information ON internal.legal_information;
|
||||
|
||||
DROP POLICY view_legal_information ON internal.legal_information;
|
||||
|
||||
ALTER TABLE internal.legal_information DISABLE ROW LEVEL SECURITY;
|
||||
|
||||
DROP VIEW api.legal_information;
|
||||
|
||||
DROP TABLE internal.legal_information;
|
||||
|
||||
@@ -1,142 +0,0 @@
|
||||
-- migrate:up
|
||||
CREATE OR REPLACE VIEW api.website_overview WITH ( security_invoker = ON
|
||||
) AS
|
||||
SELECT
|
||||
w.id,
|
||||
w.user_id,
|
||||
w.content_type,
|
||||
w.title,
|
||||
s.accent_color_light_theme,
|
||||
s.accent_color_dark_theme,
|
||||
s.favicon_image,
|
||||
h.logo_type,
|
||||
h.logo_text,
|
||||
h.logo_image,
|
||||
ho.main_content,
|
||||
f.additional_text,
|
||||
(
|
||||
SELECT
|
||||
JSON_AGG(
|
||||
JSON_BUILD_OBJECT(
|
||||
'id', a.id, 'title', a.title, 'meta_description', a.meta_description, 'meta_author', a.meta_author, 'cover_image', a.cover_image, 'publication_date', a.publication_date, 'main_content', a.main_content, 'created_at', a.created_at, 'last_modified_at', a.last_modified_at
|
||||
)
|
||||
)
|
||||
FROM
|
||||
internal.article a
|
||||
WHERE
|
||||
a.website_id = w.id
|
||||
) AS articles,
|
||||
CASE WHEN w.content_type = 'Docs' THEN
|
||||
(
|
||||
SELECT
|
||||
JSON_OBJECT_AGG(
|
||||
COALESCE(
|
||||
category_name, 'Uncategorized'
|
||||
), articles
|
||||
)
|
||||
FROM (
|
||||
SELECT
|
||||
dc.category_name,
|
||||
dc.category_weight AS category_weight,
|
||||
JSON_AGG(
|
||||
JSON_BUILD_OBJECT(
|
||||
'id', a.id, 'title', a.title, 'meta_description', a.meta_description, 'meta_author', a.meta_author, 'cover_image', a.cover_image, 'publication_date', a.publication_date, 'main_content', a.main_content, 'created_at', a.created_at, 'last_modified_at', a.last_modified_at
|
||||
) ORDER BY a.article_weight DESC NULLS LAST
|
||||
) AS articles
|
||||
FROM
|
||||
internal.article a
|
||||
LEFT JOIN internal.docs_category dc ON a.category = dc.id
|
||||
WHERE
|
||||
a.website_id = w.id
|
||||
GROUP BY
|
||||
dc.id,
|
||||
dc.category_name,
|
||||
dc.category_weight
|
||||
ORDER BY
|
||||
category_weight DESC NULLS LAST
|
||||
) AS categorized_articles)
|
||||
ELSE
|
||||
NULL
|
||||
END AS categorized_articles,
|
||||
li.main_content legal_information_main_content
|
||||
FROM
|
||||
internal.website w
|
||||
JOIN internal.settings s ON w.id = s.website_id
|
||||
JOIN internal.header h ON w.id = h.website_id
|
||||
JOIN internal.home ho ON w.id = ho.website_id
|
||||
JOIN internal.footer f ON w.id = f.website_id
|
||||
LEFT JOIN internal.legal_information li ON w.id = li.website_id;
|
||||
|
||||
GRANT SELECT ON api.website_overview TO authenticated_user;
|
||||
|
||||
-- migrate:down
|
||||
DROP VIEW api.website_overview;
|
||||
|
||||
CREATE VIEW api.website_overview WITH ( security_invoker = ON
|
||||
) AS
|
||||
SELECT
|
||||
w.id,
|
||||
w.user_id,
|
||||
w.content_type,
|
||||
w.title,
|
||||
s.accent_color_light_theme,
|
||||
s.accent_color_dark_theme,
|
||||
s.favicon_image,
|
||||
h.logo_type,
|
||||
h.logo_text,
|
||||
h.logo_image,
|
||||
ho.main_content,
|
||||
f.additional_text,
|
||||
(
|
||||
SELECT
|
||||
JSON_AGG(
|
||||
JSON_BUILD_OBJECT(
|
||||
'id', a.id, 'title', a.title, 'meta_description', a.meta_description, 'meta_author', a.meta_author, 'cover_image', a.cover_image, 'publication_date', a.publication_date, 'main_content', a.main_content, 'created_at', a.created_at, 'last_modified_at', a.last_modified_at
|
||||
)
|
||||
)
|
||||
FROM
|
||||
internal.article a
|
||||
WHERE
|
||||
a.website_id = w.id
|
||||
) AS articles,
|
||||
CASE WHEN w.content_type = 'Docs' THEN
|
||||
(
|
||||
SELECT
|
||||
JSON_OBJECT_AGG(
|
||||
COALESCE(
|
||||
category_name, 'Uncategorized'
|
||||
), articles
|
||||
)
|
||||
FROM (
|
||||
SELECT
|
||||
dc.category_name,
|
||||
dc.category_weight AS category_weight,
|
||||
JSON_AGG(
|
||||
JSON_BUILD_OBJECT(
|
||||
'id', a.id, 'title', a.title, 'meta_description', a.meta_description, 'meta_author', a.meta_author, 'cover_image', a.cover_image, 'publication_date', a.publication_date, 'main_content', a.main_content, 'created_at', a.created_at, 'last_modified_at', a.last_modified_at
|
||||
) ORDER BY a.article_weight DESC NULLS LAST
|
||||
) AS articles
|
||||
FROM
|
||||
internal.article a
|
||||
LEFT JOIN internal.docs_category dc ON a.category = dc.id
|
||||
WHERE
|
||||
a.website_id = w.id
|
||||
GROUP BY
|
||||
dc.id,
|
||||
dc.category_name,
|
||||
dc.category_weight
|
||||
ORDER BY
|
||||
category_weight DESC NULLS LAST
|
||||
) AS categorized_articles)
|
||||
ELSE
|
||||
NULL
|
||||
END AS categorized_articles
|
||||
FROM
|
||||
internal.website w
|
||||
JOIN internal.settings s ON w.id = s.website_id
|
||||
JOIN internal.header h ON w.id = h.website_id
|
||||
JOIN internal.home ho ON w.id = ho.website_id
|
||||
JOIN internal.footer f ON w.id = f.website_id;
|
||||
|
||||
GRANT SELECT ON api.website_overview TO authenticated_user;
|
||||
|
||||
544
web-app/package-lock.json
generated
544
web-app/package-lock.json
generated
@@ -28,6 +28,7 @@
|
||||
"eslint-config-prettier": "9.1.0",
|
||||
"eslint-plugin-svelte": "2.43.0",
|
||||
"globals": "15.9.0",
|
||||
"pg-to-ts": "4.1.1",
|
||||
"prettier": "3.3.3",
|
||||
"prettier-plugin-svelte": "3.2.6",
|
||||
"svelte": "5.0.0-next.220",
|
||||
@@ -1631,6 +1632,16 @@
|
||||
"dequal": "^2.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/assert-options": {
|
||||
"version": "0.8.0",
|
||||
"resolved": "https://registry.npmjs.org/assert-options/-/assert-options-0.8.0.tgz",
|
||||
"integrity": "sha512-qSELrEaEz4sGwTs4Qh+swQkjiHAysC4rot21+jzXU86dJzNG+FDqBzyS3ohSoTRf4ZLA3FSwxQdiuNl5NXUtvA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
@@ -1700,6 +1711,16 @@
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/buffer-writer": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/buffer-writer/-/buffer-writer-2.0.0.tgz",
|
||||
"integrity": "sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/builtin-modules": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz",
|
||||
@@ -1781,6 +1802,100 @@
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/cliui": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
|
||||
"integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"string-width": "^4.2.0",
|
||||
"strip-ansi": "^6.0.1",
|
||||
"wrap-ansi": "^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/cliui/node_modules/ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/cliui/node_modules/ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-convert": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/cliui/node_modules/emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cliui/node_modules/string-width": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
"is-fullwidth-code-point": "^3.0.0",
|
||||
"strip-ansi": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/cliui/node_modules/strip-ansi": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/cliui/node_modules/wrap-ansi": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
||||
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.0.0",
|
||||
"string-width": "^4.1.0",
|
||||
"strip-ansi": "^6.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
@@ -1813,6 +1928,20 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/commander": {
|
||||
"version": "2.20.3",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
|
||||
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/commandpost": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/commandpost/-/commandpost-1.4.0.tgz",
|
||||
"integrity": "sha512-aE2Y4MTFJ870NuB/+2z1cXBhSBBzRydVVjzhFC4gtenEhpnj15yu0qptWGJsO9YGrcPZ3ezX8AWb1VA391MKpQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/commondir": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz",
|
||||
@@ -1985,6 +2114,43 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/editorconfig": {
|
||||
"version": "0.15.3",
|
||||
"resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-0.15.3.tgz",
|
||||
"integrity": "sha512-M9wIMFx96vq0R4F+gRpY3o2exzb8hEj/n9S8unZtHSvYjibBp/iMufSzvmOcV/laG0ZtuTVGtiJggPOSW2r93g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"commander": "^2.19.0",
|
||||
"lru-cache": "^4.1.5",
|
||||
"semver": "^5.6.0",
|
||||
"sigmund": "^1.0.1"
|
||||
},
|
||||
"bin": {
|
||||
"editorconfig": "bin/editorconfig"
|
||||
}
|
||||
},
|
||||
"node_modules/editorconfig/node_modules/lru-cache": {
|
||||
"version": "4.1.5",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz",
|
||||
"integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"pseudomap": "^1.0.2",
|
||||
"yallist": "^2.1.2"
|
||||
}
|
||||
},
|
||||
"node_modules/editorconfig/node_modules/semver": {
|
||||
"version": "5.7.2",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
|
||||
"integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver"
|
||||
}
|
||||
},
|
||||
"node_modules/emoji-regex": {
|
||||
"version": "9.2.2",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
|
||||
@@ -2050,6 +2216,16 @@
|
||||
"@esbuild/win32-x64": "0.21.5"
|
||||
}
|
||||
},
|
||||
"node_modules/escalade": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
|
||||
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/escape-string-regexp": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
|
||||
@@ -2541,6 +2717,16 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/get-caller-file": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
|
||||
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": "6.* || 8.* || >= 10.*"
|
||||
}
|
||||
},
|
||||
"node_modules/github-slugger": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-2.0.0.tgz",
|
||||
@@ -3077,6 +3263,13 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/lodash": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.merge": {
|
||||
"version": "4.6.2",
|
||||
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
|
||||
@@ -3361,6 +3554,13 @@
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0"
|
||||
},
|
||||
"node_modules/packet-reader": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/packet-reader/-/packet-reader-1.0.0.tgz",
|
||||
"integrity": "sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/parent-module": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
|
||||
@@ -3440,6 +3640,142 @@
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/pg": {
|
||||
"version": "8.8.0",
|
||||
"resolved": "https://registry.npmjs.org/pg/-/pg-8.8.0.tgz",
|
||||
"integrity": "sha512-UXYN0ziKj+AeNNP7VDMwrehpACThH7LUl/p8TDFpEUuSejCUIwGSfxpHsPvtM6/WXFy6SU4E5RG4IJV/TZAGjw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"buffer-writer": "2.0.0",
|
||||
"packet-reader": "1.0.0",
|
||||
"pg-connection-string": "^2.5.0",
|
||||
"pg-pool": "^3.5.2",
|
||||
"pg-protocol": "^1.5.0",
|
||||
"pg-types": "^2.1.0",
|
||||
"pgpass": "1.x"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"pg-native": ">=3.0.1"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"pg-native": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/pg-connection-string": {
|
||||
"version": "2.6.4",
|
||||
"resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.6.4.tgz",
|
||||
"integrity": "sha512-v+Z7W/0EO707aNMaAEfiGnGL9sxxumwLl2fJvCQtMn9Fxsg+lPpPkdcyBSv/KFgpGdYkMfn+EI1Or2EHjpgLCA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/pg-int8": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz",
|
||||
"integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/pg-minify": {
|
||||
"version": "1.6.2",
|
||||
"resolved": "https://registry.npmjs.org/pg-minify/-/pg-minify-1.6.2.tgz",
|
||||
"integrity": "sha512-1KdmFGGTP6jplJoI8MfvRlfvMiyBivMRP7/ffh4a11RUFJ7kC2J0ZHlipoKiH/1hz+DVgceon9U2qbaHpPeyPg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/pg-pool": {
|
||||
"version": "3.6.2",
|
||||
"resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.6.2.tgz",
|
||||
"integrity": "sha512-Htjbg8BlwXqSBQ9V8Vjtc+vzf/6fVUuak/3/XXKA9oxZprwW3IMDQTGHP+KDmVL7rtd+R1QjbnCFPuTHm3G4hg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"pg": ">=8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/pg-promise": {
|
||||
"version": "10.15.4",
|
||||
"resolved": "https://registry.npmjs.org/pg-promise/-/pg-promise-10.15.4.tgz",
|
||||
"integrity": "sha512-BKlHCMCdNUmF6gagVbehRWSEiVcZzPVltEx14OJExR9Iz9/1R6KETDWLLGv2l6yRqYFnEZZy1VDjRhArzeIGrw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"assert-options": "0.8.0",
|
||||
"pg": "8.8.0",
|
||||
"pg-minify": "1.6.2",
|
||||
"spex": "3.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/pg-protocol": {
|
||||
"version": "1.6.1",
|
||||
"resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.6.1.tgz",
|
||||
"integrity": "sha512-jPIlvgoD63hrEuihvIg+tJhoGjUsLPn6poJY9N5CnlPd91c2T18T/9zBtLxZSb1EhYxBRoZJtzScCaWlYLtktg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/pg-to-ts": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/pg-to-ts/-/pg-to-ts-4.1.1.tgz",
|
||||
"integrity": "sha512-wc/ZXMMQrxu42mnl6eEdMgT31S9rvA/Oh9I9PchovUwoJLzEg0osGQjxiQOLjAdz3Ti45o749XREJ2s+xncZ6w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"lodash": "^4.17.21",
|
||||
"pg-promise": "^10.11.1",
|
||||
"typescript-formatter": "^7.0.1",
|
||||
"yargs": "^17.3.1"
|
||||
},
|
||||
"bin": {
|
||||
"pg-to-ts": "dist/cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.15.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": ">=4.1"
|
||||
}
|
||||
},
|
||||
"node_modules/pg-types": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz",
|
||||
"integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"pg-int8": "1.0.1",
|
||||
"postgres-array": "~2.0.0",
|
||||
"postgres-bytea": "~1.0.0",
|
||||
"postgres-date": "~1.0.4",
|
||||
"postgres-interval": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/pgpass": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz",
|
||||
"integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"split2": "^4.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz",
|
||||
@@ -3624,6 +3960,49 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/postgres-array": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
|
||||
"integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/postgres-bytea": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz",
|
||||
"integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/postgres-date": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz",
|
||||
"integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/postgres-interval": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz",
|
||||
"integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"xtend": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prelude-ls": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
|
||||
@@ -3661,6 +4040,13 @@
|
||||
"svelte": "^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0"
|
||||
}
|
||||
},
|
||||
"node_modules/pseudomap": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz",
|
||||
"integrity": "sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/psl": {
|
||||
"version": "1.9.0",
|
||||
"resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz",
|
||||
@@ -3716,6 +4102,16 @@
|
||||
"node": ">=8.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/require-directory": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
||||
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/requires-port": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
|
||||
@@ -3974,6 +4370,13 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/sigmund": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz",
|
||||
"integrity": "sha512-fCvEXfh6NWpm+YSuY2bpXb/VIihqWA6hLsgboC+0nl71Q7N7o2eaCW8mJa/NLvQhs6jpd3VZV4UiUQlV6+lc8g==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/signal-exit": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
|
||||
@@ -4028,6 +4431,26 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/spex": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/spex/-/spex-3.2.0.tgz",
|
||||
"integrity": "sha512-9srjJM7NaymrpwMHvSmpDeIK5GoRMX/Tq0E8aOlDPS54dDnDUIp30DrP9SphMPEETDLzEM9+4qo+KipmbtPecg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4.5"
|
||||
}
|
||||
},
|
||||
"node_modules/split2": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
|
||||
"integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">= 10.x"
|
||||
}
|
||||
},
|
||||
"node_modules/string-width": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
|
||||
@@ -4517,6 +4940,26 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/typescript-formatter": {
|
||||
"version": "7.2.2",
|
||||
"resolved": "https://registry.npmjs.org/typescript-formatter/-/typescript-formatter-7.2.2.tgz",
|
||||
"integrity": "sha512-V7vfI9XArVhriOTYHPzMU2WUnm5IMdu9X/CPxs8mIMGxmTBFpDABlbkBka64PZJ9/xgQeRpK8KzzAG4MPzxBDQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"commandpost": "^1.0.0",
|
||||
"editorconfig": "^0.15.0"
|
||||
},
|
||||
"bin": {
|
||||
"tsfmt": "bin/tsfmt"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 4.2.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^2.1.6 || >=2.7.0-dev || >=2.8.0-dev || >=2.9.0-dev || >=3.0.0-dev"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "6.13.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.13.0.tgz",
|
||||
@@ -4857,6 +5300,33 @@
|
||||
"integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/xtend": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
||||
"integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/y18n": {
|
||||
"version": "5.0.8",
|
||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
|
||||
"integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/yallist": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz",
|
||||
"integrity": "sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/yaml": {
|
||||
"version": "1.10.2",
|
||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
|
||||
@@ -4867,6 +5337,80 @@
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/yargs": {
|
||||
"version": "17.7.2",
|
||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
|
||||
"integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cliui": "^8.0.1",
|
||||
"escalade": "^3.1.1",
|
||||
"get-caller-file": "^2.0.5",
|
||||
"require-directory": "^2.1.1",
|
||||
"string-width": "^4.2.3",
|
||||
"y18n": "^5.0.5",
|
||||
"yargs-parser": "^21.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/yargs-parser": {
|
||||
"version": "21.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
|
||||
"integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/yargs/node_modules/ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/yargs/node_modules/emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/yargs/node_modules/string-width": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
"is-fullwidth-code-point": "^3.0.0",
|
||||
"strip-ansi": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/yargs/node_modules/strip-ansi": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/yocto-queue": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
|
||||
|
||||
@@ -10,7 +10,8 @@
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||
"lint": "prettier --check . && eslint .",
|
||||
"format": "prettier --write ."
|
||||
"format": "prettier --write .",
|
||||
"gents": "pg-to-ts generate -c postgres://postgres@localhost:15432/archtika -o src/lib/db-schema.ts -s internal"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "1.40.0",
|
||||
@@ -26,6 +27,7 @@
|
||||
"eslint-config-prettier": "9.1.0",
|
||||
"eslint-plugin-svelte": "2.43.0",
|
||||
"globals": "15.9.0",
|
||||
"pg-to-ts": "4.1.1",
|
||||
"prettier": "3.3.3",
|
||||
"prettier-plugin-svelte": "3.2.6",
|
||||
"svelte": "5.0.0-next.220",
|
||||
|
||||
5
web-app/src/app.d.ts
vendored
5
web-app/src/app.d.ts
vendored
@@ -1,9 +1,6 @@
|
||||
// See https://kit.svelte.dev/docs/types#app
|
||||
// for information about these interfaces
|
||||
interface User {
|
||||
id: string;
|
||||
username: string;
|
||||
}
|
||||
import type { User } from "$lib/db-schema";
|
||||
|
||||
declare global {
|
||||
namespace App {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
const { date }: { date: string } = $props();
|
||||
const { date }: { date: Date } = $props();
|
||||
|
||||
const options: Intl.DateTimeFormatOptions = {
|
||||
year: "numeric",
|
||||
@@ -11,6 +11,6 @@
|
||||
};
|
||||
</script>
|
||||
|
||||
<time datetime={new Date(date).toLocaleString("sv").replace(" ", "T")}>
|
||||
{new Date(date).toLocaleString("en-us", { ...options })}
|
||||
<time datetime={date.toLocaleString("sv").replace(" ", "T")}>
|
||||
{date.toLocaleString("en-us", { ...options })}
|
||||
</time>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<script lang="ts">
|
||||
const { success, message }: { success: boolean | undefined; message: string | undefined } =
|
||||
$props();
|
||||
const { success, message }: { success?: boolean; message?: string } = $props();
|
||||
</script>
|
||||
|
||||
{#if success}
|
||||
|
||||
496
web-app/src/lib/db-schema.ts
Normal file
496
web-app/src/lib/db-schema.ts
Normal file
@@ -0,0 +1,496 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
|
||||
/**
|
||||
* AUTO-GENERATED FILE - DO NOT EDIT!
|
||||
*
|
||||
* This file was automatically generated by pg-to-ts v.4.1.1
|
||||
* $ pg-to-ts generate -c postgres://username:password@localhost:15432/archtika -t article -t change_log -t collab -t docs_category -t footer -t header -t home -t legal_information -t media -t settings -t user -t website -s internal
|
||||
*
|
||||
*/
|
||||
|
||||
export type Json = unknown;
|
||||
|
||||
// Table article
|
||||
export interface Article {
|
||||
id: string;
|
||||
website_id: string;
|
||||
user_id: string | null;
|
||||
title: string;
|
||||
meta_description: string | null;
|
||||
meta_author: string | null;
|
||||
cover_image: string | null;
|
||||
publication_date: Date | null;
|
||||
main_content: string | null;
|
||||
created_at: Date;
|
||||
last_modified_at: Date;
|
||||
last_modified_by: string | null;
|
||||
title_description_search: any | null;
|
||||
category: string | null;
|
||||
article_weight: number | null;
|
||||
}
|
||||
export interface ArticleInput {
|
||||
id?: string;
|
||||
website_id: string;
|
||||
user_id?: string | null;
|
||||
title: string;
|
||||
meta_description?: string | null;
|
||||
meta_author?: string | null;
|
||||
cover_image?: string | null;
|
||||
publication_date?: Date | null;
|
||||
main_content?: string | null;
|
||||
created_at?: Date;
|
||||
last_modified_at?: Date;
|
||||
last_modified_by?: string | null;
|
||||
title_description_search?: any | null;
|
||||
category?: string | null;
|
||||
article_weight?: number | null;
|
||||
}
|
||||
const article = {
|
||||
tableName: "article",
|
||||
columns: [
|
||||
"id",
|
||||
"website_id",
|
||||
"user_id",
|
||||
"title",
|
||||
"meta_description",
|
||||
"meta_author",
|
||||
"cover_image",
|
||||
"publication_date",
|
||||
"main_content",
|
||||
"created_at",
|
||||
"last_modified_at",
|
||||
"last_modified_by",
|
||||
"title_description_search",
|
||||
"category",
|
||||
"article_weight"
|
||||
],
|
||||
requiredForInsert: ["website_id", "title"],
|
||||
primaryKey: "id",
|
||||
foreignKeys: {
|
||||
website_id: { table: "website", column: "id", $type: null as unknown as Website },
|
||||
user_id: { table: "user", column: "id", $type: null as unknown as User },
|
||||
cover_image: { table: "media", column: "id", $type: null as unknown as Media },
|
||||
last_modified_by: { table: "user", column: "id", $type: null as unknown as User },
|
||||
category: { table: "docs_category", column: "id", $type: null as unknown as DocsCategory }
|
||||
},
|
||||
$type: null as unknown as Article,
|
||||
$input: null as unknown as ArticleInput
|
||||
} as const;
|
||||
|
||||
// Table change_log
|
||||
export interface ChangeLog {
|
||||
website_id: string;
|
||||
user_id: string;
|
||||
change_summary: string;
|
||||
previous_value: Json | null;
|
||||
new_value: Json | null;
|
||||
timestamp: Date;
|
||||
}
|
||||
export interface ChangeLogInput {
|
||||
website_id: string;
|
||||
user_id?: string;
|
||||
change_summary: string;
|
||||
previous_value?: Json | null;
|
||||
new_value?: Json | null;
|
||||
timestamp?: Date;
|
||||
}
|
||||
const change_log = {
|
||||
tableName: "change_log",
|
||||
columns: ["website_id", "user_id", "change_summary", "previous_value", "new_value", "timestamp"],
|
||||
requiredForInsert: ["website_id", "change_summary"],
|
||||
primaryKey: "website_id",
|
||||
foreignKeys: {
|
||||
website_id: { table: "website", column: "id", $type: null as unknown as Website },
|
||||
user_id: { table: "user", column: "id", $type: null as unknown as User }
|
||||
},
|
||||
$type: null as unknown as ChangeLog,
|
||||
$input: null as unknown as ChangeLogInput
|
||||
} as const;
|
||||
|
||||
// Table collab
|
||||
export interface Collab {
|
||||
website_id: string;
|
||||
user_id: string;
|
||||
permission_level: number;
|
||||
added_at: Date;
|
||||
last_modified_at: Date;
|
||||
last_modified_by: string | null;
|
||||
}
|
||||
export interface CollabInput {
|
||||
website_id: string;
|
||||
user_id: string;
|
||||
permission_level?: number;
|
||||
added_at?: Date;
|
||||
last_modified_at?: Date;
|
||||
last_modified_by?: string | null;
|
||||
}
|
||||
const collab = {
|
||||
tableName: "collab",
|
||||
columns: [
|
||||
"website_id",
|
||||
"user_id",
|
||||
"permission_level",
|
||||
"added_at",
|
||||
"last_modified_at",
|
||||
"last_modified_by"
|
||||
],
|
||||
requiredForInsert: ["website_id", "user_id"],
|
||||
primaryKey: "website_id",
|
||||
foreignKeys: {
|
||||
website_id: { table: "website", column: "id", $type: null as unknown as Website },
|
||||
user_id: { table: "user", column: "id", $type: null as unknown as User },
|
||||
last_modified_by: { table: "user", column: "id", $type: null as unknown as User }
|
||||
},
|
||||
$type: null as unknown as Collab,
|
||||
$input: null as unknown as CollabInput
|
||||
} as const;
|
||||
|
||||
// Table docs_category
|
||||
export interface DocsCategory {
|
||||
id: string;
|
||||
website_id: string;
|
||||
user_id: string | null;
|
||||
category_name: string;
|
||||
category_weight: number;
|
||||
}
|
||||
export interface DocsCategoryInput {
|
||||
id?: string;
|
||||
website_id: string;
|
||||
user_id?: string | null;
|
||||
category_name: string;
|
||||
category_weight: number;
|
||||
}
|
||||
const docs_category = {
|
||||
tableName: "docs_category",
|
||||
columns: ["id", "website_id", "user_id", "category_name", "category_weight"],
|
||||
requiredForInsert: ["website_id", "category_name", "category_weight"],
|
||||
primaryKey: "id",
|
||||
foreignKeys: {
|
||||
website_id: { table: "website", column: "id", $type: null as unknown as Website },
|
||||
user_id: { table: "user", column: "id", $type: null as unknown as User }
|
||||
},
|
||||
$type: null as unknown as DocsCategory,
|
||||
$input: null as unknown as DocsCategoryInput
|
||||
} as const;
|
||||
|
||||
// Table footer
|
||||
export interface Footer {
|
||||
website_id: string;
|
||||
additional_text: string;
|
||||
last_modified_at: Date;
|
||||
last_modified_by: string | null;
|
||||
}
|
||||
export interface FooterInput {
|
||||
website_id: string;
|
||||
additional_text: string;
|
||||
last_modified_at?: Date;
|
||||
last_modified_by?: string | null;
|
||||
}
|
||||
const footer = {
|
||||
tableName: "footer",
|
||||
columns: ["website_id", "additional_text", "last_modified_at", "last_modified_by"],
|
||||
requiredForInsert: ["website_id", "additional_text"],
|
||||
primaryKey: "website_id",
|
||||
foreignKeys: {
|
||||
website_id: { table: "website", column: "id", $type: null as unknown as Website },
|
||||
last_modified_by: { table: "user", column: "id", $type: null as unknown as User }
|
||||
},
|
||||
$type: null as unknown as Footer,
|
||||
$input: null as unknown as FooterInput
|
||||
} as const;
|
||||
|
||||
// Table header
|
||||
export interface Header {
|
||||
website_id: string;
|
||||
logo_type: string;
|
||||
logo_text: string | null;
|
||||
logo_image: string | null;
|
||||
last_modified_at: Date;
|
||||
last_modified_by: string | null;
|
||||
}
|
||||
export interface HeaderInput {
|
||||
website_id: string;
|
||||
logo_type?: string;
|
||||
logo_text?: string | null;
|
||||
logo_image?: string | null;
|
||||
last_modified_at?: Date;
|
||||
last_modified_by?: string | null;
|
||||
}
|
||||
const header = {
|
||||
tableName: "header",
|
||||
columns: [
|
||||
"website_id",
|
||||
"logo_type",
|
||||
"logo_text",
|
||||
"logo_image",
|
||||
"last_modified_at",
|
||||
"last_modified_by"
|
||||
],
|
||||
requiredForInsert: ["website_id"],
|
||||
primaryKey: "website_id",
|
||||
foreignKeys: {
|
||||
website_id: { table: "website", column: "id", $type: null as unknown as Website },
|
||||
logo_image: { table: "media", column: "id", $type: null as unknown as Media },
|
||||
last_modified_by: { table: "user", column: "id", $type: null as unknown as User }
|
||||
},
|
||||
$type: null as unknown as Header,
|
||||
$input: null as unknown as HeaderInput
|
||||
} as const;
|
||||
|
||||
// Table home
|
||||
export interface Home {
|
||||
website_id: string;
|
||||
main_content: string;
|
||||
last_modified_at: Date;
|
||||
last_modified_by: string | null;
|
||||
}
|
||||
export interface HomeInput {
|
||||
website_id: string;
|
||||
main_content: string;
|
||||
last_modified_at?: Date;
|
||||
last_modified_by?: string | null;
|
||||
}
|
||||
const home = {
|
||||
tableName: "home",
|
||||
columns: ["website_id", "main_content", "last_modified_at", "last_modified_by"],
|
||||
requiredForInsert: ["website_id", "main_content"],
|
||||
primaryKey: "website_id",
|
||||
foreignKeys: {
|
||||
website_id: { table: "website", column: "id", $type: null as unknown as Website },
|
||||
last_modified_by: { table: "user", column: "id", $type: null as unknown as User }
|
||||
},
|
||||
$type: null as unknown as Home,
|
||||
$input: null as unknown as HomeInput
|
||||
} as const;
|
||||
|
||||
// Table legal_information
|
||||
export interface LegalInformation {
|
||||
website_id: string;
|
||||
main_content: string;
|
||||
last_modified_at: Date;
|
||||
last_modified_by: string | null;
|
||||
}
|
||||
export interface LegalInformationInput {
|
||||
website_id: string;
|
||||
main_content: string;
|
||||
last_modified_at?: Date;
|
||||
last_modified_by?: string | null;
|
||||
}
|
||||
const legal_information = {
|
||||
tableName: "legal_information",
|
||||
columns: ["website_id", "main_content", "last_modified_at", "last_modified_by"],
|
||||
requiredForInsert: ["website_id", "main_content"],
|
||||
primaryKey: "website_id",
|
||||
foreignKeys: {
|
||||
website_id: { table: "website", column: "id", $type: null as unknown as Website },
|
||||
last_modified_by: { table: "user", column: "id", $type: null as unknown as User }
|
||||
},
|
||||
$type: null as unknown as LegalInformation,
|
||||
$input: null as unknown as LegalInformationInput
|
||||
} as const;
|
||||
|
||||
// Table media
|
||||
export interface Media {
|
||||
id: string;
|
||||
website_id: string;
|
||||
user_id: string;
|
||||
blob: string;
|
||||
mimetype: string;
|
||||
original_name: string;
|
||||
created_at: Date;
|
||||
}
|
||||
export interface MediaInput {
|
||||
id?: string;
|
||||
website_id: string;
|
||||
user_id?: string;
|
||||
blob: string;
|
||||
mimetype: string;
|
||||
original_name: string;
|
||||
created_at?: Date;
|
||||
}
|
||||
const media = {
|
||||
tableName: "media",
|
||||
columns: ["id", "website_id", "user_id", "blob", "mimetype", "original_name", "created_at"],
|
||||
requiredForInsert: ["website_id", "blob", "mimetype", "original_name"],
|
||||
primaryKey: "id",
|
||||
foreignKeys: {
|
||||
website_id: { table: "website", column: "id", $type: null as unknown as Website },
|
||||
user_id: { table: "user", column: "id", $type: null as unknown as User }
|
||||
},
|
||||
$type: null as unknown as Media,
|
||||
$input: null as unknown as MediaInput
|
||||
} as const;
|
||||
|
||||
// Table settings
|
||||
export interface Settings {
|
||||
website_id: string;
|
||||
accent_color_light_theme: string;
|
||||
accent_color_dark_theme: string;
|
||||
favicon_image: string | null;
|
||||
last_modified_at: Date;
|
||||
last_modified_by: string | null;
|
||||
}
|
||||
export interface SettingsInput {
|
||||
website_id: string;
|
||||
accent_color_light_theme?: string;
|
||||
accent_color_dark_theme?: string;
|
||||
favicon_image?: string | null;
|
||||
last_modified_at?: Date;
|
||||
last_modified_by?: string | null;
|
||||
}
|
||||
const settings = {
|
||||
tableName: "settings",
|
||||
columns: [
|
||||
"website_id",
|
||||
"accent_color_light_theme",
|
||||
"accent_color_dark_theme",
|
||||
"favicon_image",
|
||||
"last_modified_at",
|
||||
"last_modified_by"
|
||||
],
|
||||
requiredForInsert: ["website_id"],
|
||||
primaryKey: "website_id",
|
||||
foreignKeys: {
|
||||
website_id: { table: "website", column: "id", $type: null as unknown as Website },
|
||||
favicon_image: { table: "media", column: "id", $type: null as unknown as Media },
|
||||
last_modified_by: { table: "user", column: "id", $type: null as unknown as User }
|
||||
},
|
||||
$type: null as unknown as Settings,
|
||||
$input: null as unknown as SettingsInput
|
||||
} as const;
|
||||
|
||||
// Table user
|
||||
export interface User {
|
||||
id: string;
|
||||
username: string;
|
||||
password_hash: string;
|
||||
role: string;
|
||||
}
|
||||
export interface UserInput {
|
||||
id?: string;
|
||||
username: string;
|
||||
password_hash: string;
|
||||
role?: string;
|
||||
}
|
||||
const user = {
|
||||
tableName: "user",
|
||||
columns: ["id", "username", "password_hash", "role"],
|
||||
requiredForInsert: ["username", "password_hash"],
|
||||
primaryKey: "id",
|
||||
foreignKeys: {},
|
||||
$type: null as unknown as User,
|
||||
$input: null as unknown as UserInput
|
||||
} as const;
|
||||
|
||||
// Table website
|
||||
export interface Website {
|
||||
id: string;
|
||||
user_id: string;
|
||||
content_type: string;
|
||||
title: string;
|
||||
created_at: Date;
|
||||
last_modified_at: Date;
|
||||
last_modified_by: string | null;
|
||||
title_search: any | null;
|
||||
is_published: boolean;
|
||||
}
|
||||
export interface WebsiteInput {
|
||||
id?: string;
|
||||
user_id?: string;
|
||||
content_type: string;
|
||||
title: string;
|
||||
created_at?: Date;
|
||||
last_modified_at?: Date;
|
||||
last_modified_by?: string | null;
|
||||
title_search?: any | null;
|
||||
is_published?: boolean;
|
||||
}
|
||||
const website = {
|
||||
tableName: "website",
|
||||
columns: [
|
||||
"id",
|
||||
"user_id",
|
||||
"content_type",
|
||||
"title",
|
||||
"created_at",
|
||||
"last_modified_at",
|
||||
"last_modified_by",
|
||||
"title_search",
|
||||
"is_published"
|
||||
],
|
||||
requiredForInsert: ["content_type", "title"],
|
||||
primaryKey: "id",
|
||||
foreignKeys: {
|
||||
user_id: { table: "user", column: "id", $type: null as unknown as User },
|
||||
last_modified_by: { table: "user", column: "id", $type: null as unknown as User }
|
||||
},
|
||||
$type: null as unknown as Website,
|
||||
$input: null as unknown as WebsiteInput
|
||||
} as const;
|
||||
|
||||
export interface TableTypes {
|
||||
article: {
|
||||
select: Article;
|
||||
input: ArticleInput;
|
||||
};
|
||||
change_log: {
|
||||
select: ChangeLog;
|
||||
input: ChangeLogInput;
|
||||
};
|
||||
collab: {
|
||||
select: Collab;
|
||||
input: CollabInput;
|
||||
};
|
||||
docs_category: {
|
||||
select: DocsCategory;
|
||||
input: DocsCategoryInput;
|
||||
};
|
||||
footer: {
|
||||
select: Footer;
|
||||
input: FooterInput;
|
||||
};
|
||||
header: {
|
||||
select: Header;
|
||||
input: HeaderInput;
|
||||
};
|
||||
home: {
|
||||
select: Home;
|
||||
input: HomeInput;
|
||||
};
|
||||
legal_information: {
|
||||
select: LegalInformation;
|
||||
input: LegalInformationInput;
|
||||
};
|
||||
media: {
|
||||
select: Media;
|
||||
input: MediaInput;
|
||||
};
|
||||
settings: {
|
||||
select: Settings;
|
||||
input: SettingsInput;
|
||||
};
|
||||
user: {
|
||||
select: User;
|
||||
input: UserInput;
|
||||
};
|
||||
website: {
|
||||
select: Website;
|
||||
input: WebsiteInput;
|
||||
};
|
||||
}
|
||||
|
||||
export const tables = {
|
||||
article,
|
||||
change_log,
|
||||
collab,
|
||||
docs_category,
|
||||
footer,
|
||||
header,
|
||||
home,
|
||||
legal_information,
|
||||
media,
|
||||
settings,
|
||||
user,
|
||||
website
|
||||
};
|
||||
@@ -2,52 +2,44 @@
|
||||
import Head from "../common/Head.svelte";
|
||||
import Nav from "../common/Nav.svelte";
|
||||
import Footer from "../common/Footer.svelte";
|
||||
import { type WebsiteOverview, md } from "../../utils";
|
||||
import type { Article } from "../../db-schema";
|
||||
|
||||
const {
|
||||
favicon,
|
||||
title,
|
||||
logoType,
|
||||
logo,
|
||||
mainContent,
|
||||
coverImage,
|
||||
publicationDate,
|
||||
footerAdditionalText,
|
||||
metaDescription
|
||||
}: {
|
||||
favicon: string;
|
||||
title: string;
|
||||
logoType: "text" | "image";
|
||||
logo: string;
|
||||
mainContent: string;
|
||||
coverImage: string;
|
||||
publicationDate: string;
|
||||
footerAdditionalText: string;
|
||||
metaDescription: string;
|
||||
} = $props();
|
||||
websiteOverview,
|
||||
article,
|
||||
apiUrl
|
||||
}: { websiteOverview: WebsiteOverview; article: Article; apiUrl: string } = $props();
|
||||
</script>
|
||||
|
||||
<Head {title} {favicon} nestingLevel={1} {metaDescription} />
|
||||
<Head
|
||||
{websiteOverview}
|
||||
nestingLevel={1}
|
||||
{apiUrl}
|
||||
title={article.title}
|
||||
metaDescription={article.meta_description}
|
||||
/>
|
||||
|
||||
<Nav {logoType} {logo} isIndexPage={false} />
|
||||
<Nav {websiteOverview} isDocsTemplate={false} isIndexPage={false} {apiUrl} />
|
||||
|
||||
<header>
|
||||
<div class="container">
|
||||
<hgroup>
|
||||
<p>{publicationDate}</p>
|
||||
<h1>{title}</h1>
|
||||
<p>{article.publication_date}</p>
|
||||
<h1>{article.title}</h1>
|
||||
</hgroup>
|
||||
{#if coverImage}
|
||||
<img src={coverImage} alt="" />
|
||||
{#if article.cover_image}
|
||||
<img src="{apiUrl}/rpc/retrieve_file?id={article.cover_image}" alt="" />
|
||||
{/if}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{#if mainContent}
|
||||
{#if article.main_content}
|
||||
<main>
|
||||
<div class="container">
|
||||
{@html mainContent}
|
||||
{@html md(article.main_content)}
|
||||
</div>
|
||||
</main>
|
||||
{/if}
|
||||
|
||||
<Footer text={footerAdditionalText} isIndexPage={false} />
|
||||
<Footer {websiteOverview} isIndexPage={false} />
|
||||
|
||||
@@ -2,47 +2,46 @@
|
||||
import Head from "../common/Head.svelte";
|
||||
import Nav from "../common/Nav.svelte";
|
||||
import Footer from "../common/Footer.svelte";
|
||||
import { md, type WebsiteOverview } from "../../utils";
|
||||
|
||||
const {
|
||||
favicon,
|
||||
title,
|
||||
logoType,
|
||||
logo,
|
||||
mainContent,
|
||||
articles,
|
||||
footerAdditionalText
|
||||
}: {
|
||||
favicon: string;
|
||||
title: string;
|
||||
logoType: "text" | "image";
|
||||
logo: string;
|
||||
mainContent: string;
|
||||
articles: { title: string; publication_date: string; meta_description: string }[];
|
||||
footerAdditionalText: string;
|
||||
} = $props();
|
||||
websiteOverview,
|
||||
apiUrl,
|
||||
isLegalPage
|
||||
}: { websiteOverview: WebsiteOverview; apiUrl: string; isLegalPage: boolean } = $props();
|
||||
</script>
|
||||
|
||||
<Head {title} {favicon} />
|
||||
<Head
|
||||
{websiteOverview}
|
||||
nestingLevel={0}
|
||||
{apiUrl}
|
||||
title={isLegalPage ? "Legal information" : websiteOverview.title}
|
||||
/>
|
||||
|
||||
<Nav {logoType} {logo} />
|
||||
<Nav {websiteOverview} isDocsTemplate={false} isIndexPage={true} {apiUrl} />
|
||||
|
||||
<header>
|
||||
<div class="container">
|
||||
<h1>{title}</h1>
|
||||
<h1>{isLegalPage ? "Legal information" : websiteOverview.title}</h1>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<div class="container">
|
||||
{@html mainContent}
|
||||
{#if articles.length > 0}
|
||||
{@html md(
|
||||
isLegalPage
|
||||
? (websiteOverview.legal_information?.main_content ?? "")
|
||||
: websiteOverview.home.main_content,
|
||||
false
|
||||
)}
|
||||
{#if websiteOverview.article.length > 0 && !isLegalPage}
|
||||
<section class="articles" id="articles">
|
||||
<h2>
|
||||
<a href="#articles">Articles</a>
|
||||
</h2>
|
||||
|
||||
<ul class="unpadded">
|
||||
{#each articles as article}
|
||||
{#each websiteOverview.article as article}
|
||||
{@const articleFileName = article.title.toLowerCase().split(" ").join("-")}
|
||||
<li>
|
||||
<p>{article.publication_date}</p>
|
||||
@@ -62,4 +61,4 @@
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<Footer text={footerAdditionalText} />
|
||||
<Footer {websiteOverview} isIndexPage={true} />
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
<script lang="ts">
|
||||
const { text, isIndexPage = true }: { text: string; isIndexPage?: boolean } = $props();
|
||||
import type { WebsiteOverview } from "../../utils";
|
||||
|
||||
const {
|
||||
websiteOverview,
|
||||
isIndexPage
|
||||
}: { websiteOverview: WebsiteOverview; isIndexPage: boolean } = $props();
|
||||
</script>
|
||||
|
||||
<footer>
|
||||
<div class="container">
|
||||
<small>
|
||||
{@html text.replace(
|
||||
{@html websiteOverview.footer.additional_text.replace(
|
||||
"!!legal",
|
||||
`<a href="${isIndexPage ? "./legal-information" : "../legal-information"}">Legal information</a>`
|
||||
)}
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
<script lang="ts">
|
||||
import type { WebsiteOverview } from "../../utils";
|
||||
|
||||
const {
|
||||
websiteOverview,
|
||||
nestingLevel,
|
||||
apiUrl,
|
||||
title,
|
||||
favicon,
|
||||
nestingLevel = 0,
|
||||
metaDescription = null
|
||||
metaDescription
|
||||
}: {
|
||||
websiteOverview: WebsiteOverview;
|
||||
nestingLevel: number;
|
||||
apiUrl: string;
|
||||
title: string;
|
||||
favicon: string;
|
||||
nestingLevel?: number;
|
||||
metaDescription?: string | null;
|
||||
} = $props();
|
||||
</script>
|
||||
@@ -19,8 +23,11 @@
|
||||
<title>{title}</title>
|
||||
<meta name="description" content={metaDescription ?? title} />
|
||||
<link rel="stylesheet" href={`${"../".repeat(nestingLevel)}styles.css`} />
|
||||
{#if favicon}
|
||||
<link rel="icon" href={favicon} />
|
||||
{#if websiteOverview.settings.favicon_image}
|
||||
<link
|
||||
rel="icon"
|
||||
href="{apiUrl}/rpc/retrieve_file?id={websiteOverview.settings.favicon_image}"
|
||||
/>
|
||||
{/if}
|
||||
</head>
|
||||
</svelte:head>
|
||||
|
||||
@@ -1,17 +1,36 @@
|
||||
<script lang="ts">
|
||||
import type { WebsiteOverview } from "../../utils";
|
||||
import type { Article } from "../../db-schema";
|
||||
|
||||
const {
|
||||
logoType,
|
||||
logo,
|
||||
isDocsTemplate = false,
|
||||
categorizedArticles = {},
|
||||
isIndexPage = true
|
||||
websiteOverview,
|
||||
isDocsTemplate,
|
||||
isIndexPage,
|
||||
apiUrl
|
||||
}: {
|
||||
logoType: "text" | "image";
|
||||
logo: string;
|
||||
isDocsTemplate?: boolean;
|
||||
categorizedArticles?: { [key: string]: { title: string }[] };
|
||||
isIndexPage?: boolean;
|
||||
websiteOverview: WebsiteOverview;
|
||||
isDocsTemplate: boolean;
|
||||
isIndexPage: boolean;
|
||||
apiUrl: string;
|
||||
} = $props();
|
||||
|
||||
const categorizedArticles = Object.fromEntries(
|
||||
Object.entries(
|
||||
Object.groupBy(
|
||||
websiteOverview.article.sort((a, b) => (b.article_weight ?? 0) - (a.article_weight ?? 0)),
|
||||
(article) => article.docs_category?.category_name ?? "Uncategorized"
|
||||
)
|
||||
).sort(([a], [b]) =>
|
||||
a === "Uncategorized"
|
||||
? 1
|
||||
: b === "Uncategorized"
|
||||
? -1
|
||||
: (websiteOverview.article.find((art) => art.docs_category?.category_name === b)
|
||||
?.docs_category?.category_weight ?? 0) -
|
||||
(websiteOverview.article.find((art) => art.docs_category?.category_name === a)
|
||||
?.docs_category?.category_weight ?? 0)
|
||||
)
|
||||
) as { [key: string]: Article[] };
|
||||
</script>
|
||||
|
||||
<nav>
|
||||
@@ -53,10 +72,15 @@
|
||||
</section>
|
||||
{/if}
|
||||
<a href={isIndexPage ? "." : ".."}>
|
||||
{#if logoType === "text"}
|
||||
<strong>{logo}</strong>
|
||||
{#if websiteOverview.header.logo_type === "text"}
|
||||
<strong>{websiteOverview.header.logo_text}</strong>
|
||||
{:else}
|
||||
<img src={logo} width="24" height="24" alt="" />
|
||||
<img
|
||||
src="{apiUrl}/rpc/retrieve_file?id={websiteOverview.header.logo_image}"
|
||||
width="24"
|
||||
height="24"
|
||||
alt=""
|
||||
/>
|
||||
{/if}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -2,44 +2,38 @@
|
||||
import Head from "../common/Head.svelte";
|
||||
import Nav from "../common/Nav.svelte";
|
||||
import Footer from "../common/Footer.svelte";
|
||||
import { md, type WebsiteOverview } from "../../utils";
|
||||
import type { Article } from "../../db-schema";
|
||||
|
||||
const {
|
||||
favicon,
|
||||
title,
|
||||
logoType,
|
||||
logo,
|
||||
mainContent,
|
||||
categorizedArticles,
|
||||
footerAdditionalText,
|
||||
metaDescription
|
||||
}: {
|
||||
favicon: string;
|
||||
title: string;
|
||||
logoType: "text" | "image";
|
||||
logo: string;
|
||||
mainContent: string;
|
||||
categorizedArticles: { [key: string]: { title: string }[] };
|
||||
footerAdditionalText: string;
|
||||
metaDescription: string;
|
||||
} = $props();
|
||||
websiteOverview,
|
||||
article,
|
||||
apiUrl
|
||||
}: { websiteOverview: WebsiteOverview; article: Article; apiUrl: string } = $props();
|
||||
</script>
|
||||
|
||||
<Head {title} {favicon} nestingLevel={1} {metaDescription} />
|
||||
<Head
|
||||
{websiteOverview}
|
||||
nestingLevel={1}
|
||||
{apiUrl}
|
||||
title={article.title}
|
||||
metaDescription={article.meta_description}
|
||||
/>
|
||||
|
||||
<Nav {logoType} {logo} isDocsTemplate={true} {categorizedArticles} isIndexPage={false} />
|
||||
<Nav {websiteOverview} isDocsTemplate={true} isIndexPage={false} {apiUrl} />
|
||||
|
||||
<header>
|
||||
<div class="container">
|
||||
<h1>{title}</h1>
|
||||
<h1>{article.title}</h1>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{#if mainContent}
|
||||
{#if article.main_content}
|
||||
<main>
|
||||
<div class="container">
|
||||
{@html mainContent}
|
||||
{@html md(article.main_content)}
|
||||
</div>
|
||||
</main>
|
||||
{/if}
|
||||
|
||||
<Footer text={footerAdditionalText} isIndexPage={false} />
|
||||
<Footer {websiteOverview} isIndexPage={false} />
|
||||
|
||||
@@ -2,40 +2,39 @@
|
||||
import Head from "../common/Head.svelte";
|
||||
import Nav from "../common/Nav.svelte";
|
||||
import Footer from "../common/Footer.svelte";
|
||||
import { md, type WebsiteOverview } from "../../utils";
|
||||
|
||||
const {
|
||||
favicon,
|
||||
title,
|
||||
logoType,
|
||||
logo,
|
||||
mainContent,
|
||||
categorizedArticles,
|
||||
footerAdditionalText
|
||||
}: {
|
||||
favicon: string;
|
||||
title: string;
|
||||
logoType: "text" | "image";
|
||||
logo: string;
|
||||
mainContent: string;
|
||||
categorizedArticles: { [key: string]: { title: string }[] };
|
||||
footerAdditionalText: string;
|
||||
} = $props();
|
||||
websiteOverview,
|
||||
apiUrl,
|
||||
isLegalPage
|
||||
}: { websiteOverview: WebsiteOverview; apiUrl: string; isLegalPage: boolean } = $props();
|
||||
</script>
|
||||
|
||||
<Head {title} {favicon} />
|
||||
<Head
|
||||
{websiteOverview}
|
||||
nestingLevel={0}
|
||||
{apiUrl}
|
||||
title={isLegalPage ? "Legal information" : websiteOverview.title}
|
||||
/>
|
||||
|
||||
<Nav {logoType} {logo} isDocsTemplate={true} {categorizedArticles} />
|
||||
<Nav {websiteOverview} isDocsTemplate={true} isIndexPage={true} {apiUrl} />
|
||||
|
||||
<header>
|
||||
<div class="container">
|
||||
<h1>{title}</h1>
|
||||
<h1>{isLegalPage ? "Legal information" : websiteOverview.title}</h1>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<div class="container">
|
||||
{@html mainContent}
|
||||
{@html md(
|
||||
isLegalPage
|
||||
? (websiteOverview.legal_information?.main_content ?? "")
|
||||
: websiteOverview.home.main_content,
|
||||
false
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<Footer text={footerAdditionalText} />
|
||||
<Footer {websiteOverview} isIndexPage={true} />
|
||||
|
||||
@@ -5,6 +5,16 @@ import hljs from "highlight.js";
|
||||
import GithubSlugger from "github-slugger";
|
||||
import DOMPurify from "isomorphic-dompurify";
|
||||
import { applyAction, deserialize } from "$app/forms";
|
||||
import type {
|
||||
Website,
|
||||
Settings,
|
||||
Header,
|
||||
Home,
|
||||
Footer,
|
||||
Article,
|
||||
DocsCategory,
|
||||
LegalInformation
|
||||
} from "$lib/db-schema";
|
||||
|
||||
export const ALLOWED_MIME_TYPES = ["image/jpeg", "image/png", "image/svg+xml", "image/webp"];
|
||||
|
||||
@@ -189,3 +199,12 @@ export const handleImagePaste = async (event: ClipboardEvent, API_BASE_PREFIX: s
|
||||
return "";
|
||||
}
|
||||
};
|
||||
|
||||
export interface WebsiteOverview extends Website {
|
||||
settings: Settings;
|
||||
header: Header;
|
||||
home: Home;
|
||||
footer: Footer;
|
||||
article: (Article & { docs_category: DocsCategory | null })[];
|
||||
legal_information?: LegalInformation;
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ export const actions: Actions = {
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
username: data.get("username"),
|
||||
password: data.get("password")
|
||||
pass: data.get("password")
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ export const actions: Actions = {
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
username: data.get("username"),
|
||||
password: data.get("password")
|
||||
pass: data.get("password")
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { Actions, PageServerLoad } from "./$types";
|
||||
import { API_BASE_PREFIX } from "$lib/server/utils";
|
||||
import { rm } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import type { Website, WebsiteInput } from "$lib/db-schema";
|
||||
|
||||
export const load: PageServerLoad = async ({ fetch, cookies, url, locals }) => {
|
||||
const searchQuery = url.searchParams.get("website_search_query");
|
||||
@@ -47,7 +48,7 @@ export const load: PageServerLoad = async ({ fetch, cookies, url, locals }) => {
|
||||
}
|
||||
});
|
||||
|
||||
const websites = await websiteData.json();
|
||||
const websites: Website[] = await websiteData.json();
|
||||
|
||||
return {
|
||||
totalWebsiteCount,
|
||||
@@ -66,9 +67,9 @@ export const actions: Actions = {
|
||||
Authorization: `Bearer ${cookies.get("session_token")}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
content_type: data.get("content-type"),
|
||||
title: data.get("title")
|
||||
})
|
||||
content_type: data.get("content-type") as string,
|
||||
title: data.get("title") as string
|
||||
} satisfies WebsiteInput)
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
|
||||
@@ -23,7 +23,7 @@ export const actions: Actions = {
|
||||
Authorization: `Bearer ${cookies.get("session_token")}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
password: data.get("password")
|
||||
pass: data.get("password")
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { LayoutServerLoad } from "./$types";
|
||||
import { API_BASE_PREFIX } from "$lib/server/utils";
|
||||
import { error } from "@sveltejs/kit";
|
||||
import type { Website, Home } from "$lib/db-schema";
|
||||
|
||||
export const load: LayoutServerLoad = async ({ params, fetch, cookies }) => {
|
||||
const websiteData = await fetch(`${API_BASE_PREFIX}/website?id=eq.${params.websiteId}`, {
|
||||
@@ -25,8 +26,8 @@ export const load: LayoutServerLoad = async ({ params, fetch, cookies }) => {
|
||||
}
|
||||
});
|
||||
|
||||
const website = await websiteData.json();
|
||||
const home = await homeData.json();
|
||||
const website: Website = await websiteData.json();
|
||||
const home: Home = await homeData.json();
|
||||
|
||||
return {
|
||||
website,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { Actions, PageServerLoad } from "./$types";
|
||||
import { API_BASE_PREFIX } from "$lib/server/utils";
|
||||
import type { Settings, Header, Footer } from "$lib/db-schema";
|
||||
|
||||
export const load: PageServerLoad = async ({ params, fetch, cookies }) => {
|
||||
const globalSettingsData = await fetch(
|
||||
@@ -32,9 +33,9 @@ export const load: PageServerLoad = async ({ params, fetch, cookies }) => {
|
||||
}
|
||||
});
|
||||
|
||||
const globalSettings = await globalSettingsData.json();
|
||||
const header = await headerData.json();
|
||||
const footer = await footerData.json();
|
||||
const globalSettings: Settings = await globalSettingsData.json();
|
||||
const header: Header = await headerData.json();
|
||||
const footer: Footer = await footerData.json();
|
||||
|
||||
return {
|
||||
globalSettings,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { Actions, PageServerLoad } from "./$types";
|
||||
import { API_BASE_PREFIX } from "$lib/server/utils";
|
||||
import type { Article, ArticleInput, DocsCategory } from "$lib/db-schema";
|
||||
|
||||
export const load: PageServerLoad = async ({ params, fetch, cookies, url, parent, locals }) => {
|
||||
const searchQuery = url.searchParams.get("article_search_query");
|
||||
@@ -54,7 +55,7 @@ export const load: PageServerLoad = async ({ params, fetch, cookies, url, parent
|
||||
}
|
||||
});
|
||||
|
||||
const articles = await articlesData.json();
|
||||
const articles: (Article & { docs_category: DocsCategory | null })[] = await articlesData.json();
|
||||
|
||||
return {
|
||||
totalArticleCount,
|
||||
@@ -76,8 +77,8 @@ export const actions: Actions = {
|
||||
},
|
||||
body: JSON.stringify({
|
||||
website_id: params.websiteId,
|
||||
title: data.get("title")
|
||||
})
|
||||
title: data.get("title") as string
|
||||
} satisfies ArticleInput)
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { Actions, PageServerLoad } from "./$types";
|
||||
import { API_BASE_PREFIX } from "$lib/server/utils";
|
||||
import type { Article, DocsCategory } from "$lib/db-schema";
|
||||
|
||||
export const load: PageServerLoad = async ({ parent, params, cookies, fetch }) => {
|
||||
const articleData = await fetch(`${API_BASE_PREFIX}/article?id=eq.${params.articleId}`, {
|
||||
@@ -22,8 +23,8 @@ export const load: PageServerLoad = async ({ parent, params, cookies, fetch }) =
|
||||
}
|
||||
);
|
||||
|
||||
const article = await articleData.json();
|
||||
const categories = await categoryData.json();
|
||||
const article: Article = await articleData.json();
|
||||
const categories: DocsCategory[] = await categoryData.json();
|
||||
const { website } = await parent();
|
||||
|
||||
return { website, article, categories, API_BASE_PREFIX };
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { Actions, PageServerLoad } from "./$types";
|
||||
import { API_BASE_PREFIX } from "$lib/server/utils";
|
||||
import type { DocsCategory, DocsCategoryInput } from "$lib/db-schema";
|
||||
|
||||
export const load: PageServerLoad = async ({ parent, params, cookies, fetch }) => {
|
||||
const categoryData = await fetch(
|
||||
@@ -13,7 +14,7 @@ export const load: PageServerLoad = async ({ parent, params, cookies, fetch }) =
|
||||
}
|
||||
);
|
||||
|
||||
const categories = await categoryData.json();
|
||||
const categories: DocsCategory[] = await categoryData.json();
|
||||
const { website, home } = await parent();
|
||||
|
||||
return {
|
||||
@@ -35,9 +36,9 @@ export const actions: Actions = {
|
||||
},
|
||||
body: JSON.stringify({
|
||||
website_id: params.websiteId,
|
||||
category_name: data.get("category-name"),
|
||||
category_weight: data.get("category-weight")
|
||||
})
|
||||
category_name: data.get("category-name") as string,
|
||||
category_weight: data.get("category-weight") as unknown as number
|
||||
} satisfies DocsCategoryInput)
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { Actions, PageServerLoad } from "./$types";
|
||||
import { API_BASE_PREFIX } from "$lib/server/utils";
|
||||
import type { Collab, CollabInput, User } from "$lib/db-schema";
|
||||
|
||||
export const load: PageServerLoad = async ({ parent, params, fetch, cookies }) => {
|
||||
const { website, home } = await parent();
|
||||
@@ -15,7 +16,7 @@ export const load: PageServerLoad = async ({ parent, params, fetch, cookies }) =
|
||||
}
|
||||
);
|
||||
|
||||
const collaborators = await collabData.json();
|
||||
const collaborators: (Collab & { user: User })[] = await collabData.json();
|
||||
|
||||
return {
|
||||
website,
|
||||
@@ -37,6 +38,8 @@ export const actions: Actions = {
|
||||
}
|
||||
});
|
||||
|
||||
const user: User = await userData.json();
|
||||
|
||||
const res = await fetch(`${API_BASE_PREFIX}/collab`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
@@ -45,9 +48,9 @@ export const actions: Actions = {
|
||||
},
|
||||
body: JSON.stringify({
|
||||
website_id: params.websiteId,
|
||||
user_id: (await userData.json()).id,
|
||||
permission_level: data.get("permission-level")
|
||||
})
|
||||
user_id: user.id,
|
||||
permission_level: data.get("permission-level") as unknown as number
|
||||
} satisfies CollabInput)
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { Actions, PageServerLoad } from "./$types";
|
||||
import { API_BASE_PREFIX } from "$lib/server/utils";
|
||||
import { rm } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import type { LegalInformation, LegalInformationInput } from "$lib/db-schema";
|
||||
|
||||
export const load: PageServerLoad = async ({ parent, fetch, params, cookies }) => {
|
||||
const legalInformationData = await fetch(
|
||||
@@ -16,7 +17,7 @@ export const load: PageServerLoad = async ({ parent, fetch, params, cookies }) =
|
||||
}
|
||||
);
|
||||
|
||||
const legalInformation = legalInformationData.ok ? await legalInformationData.json() : null;
|
||||
const legalInformation: LegalInformation = await legalInformationData.json();
|
||||
const { website } = await parent();
|
||||
|
||||
return {
|
||||
@@ -39,8 +40,8 @@ export const actions: Actions = {
|
||||
},
|
||||
body: JSON.stringify({
|
||||
website_id: params.websiteId,
|
||||
main_content: data.get("main-content")
|
||||
})
|
||||
main_content: data.get("main-content") as string
|
||||
} satisfies LegalInformationInput)
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
const { data, form }: { data: PageServerData; form: ActionData } = $props();
|
||||
|
||||
let previewContent = $state(data.legalInformation?.main_content);
|
||||
let previewContent = $state(data.legalInformation.main_content);
|
||||
let mainContentTextarea: HTMLTextAreaElement;
|
||||
let textareaScrollTop = $state(0);
|
||||
|
||||
@@ -80,14 +80,14 @@
|
||||
bind:value={previewContent}
|
||||
bind:this={mainContentTextarea}
|
||||
onscroll={updateScrollPercentage}
|
||||
required>{data.legalInformation?.main_content ?? ""}</textarea
|
||||
required>{data.legalInformation.main_content ?? ""}</textarea
|
||||
>
|
||||
</label>
|
||||
|
||||
<button type="submit">Submit</button>
|
||||
</form>
|
||||
|
||||
{#if data.legalInformation?.main_content}
|
||||
{#if data.legalInformation.main_content}
|
||||
<Modal id="delete-legal-information" text="Delete">
|
||||
<form
|
||||
action="?/deleteLegalInformation"
|
||||
@@ -98,7 +98,6 @@
|
||||
await update();
|
||||
window.location.hash = "!";
|
||||
sending = false;
|
||||
previewContent = null;
|
||||
};
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { readFile, mkdir, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { md } from "$lib/utils";
|
||||
import { type WebsiteOverview } from "$lib/utils";
|
||||
import type { Actions, PageServerLoad } from "./$types";
|
||||
import { API_BASE_PREFIX } from "$lib/server/utils";
|
||||
import { render } from "svelte/server";
|
||||
@@ -10,34 +10,9 @@ import DocsIndex from "$lib/templates/docs/DocsIndex.svelte";
|
||||
import DocsArticle from "$lib/templates/docs/DocsArticle.svelte";
|
||||
import { dev } from "$app/environment";
|
||||
|
||||
interface WebsiteData {
|
||||
id: string;
|
||||
content_type: "Blog" | "Docs";
|
||||
favicon_image: string | null;
|
||||
title: string;
|
||||
logo_type: "text" | "image";
|
||||
logo_text: string | null;
|
||||
logo_image: string | null;
|
||||
main_content: string;
|
||||
additional_text: string;
|
||||
accent_color_light_theme: string;
|
||||
accent_color_dark_theme: string;
|
||||
articles: {
|
||||
cover_image: string | null;
|
||||
title: string;
|
||||
publication_date: string;
|
||||
meta_description: string;
|
||||
main_content: string;
|
||||
}[];
|
||||
categorized_articles: {
|
||||
[key: string]: { title: string; publication_date: string; meta_description: string }[];
|
||||
};
|
||||
legal_information_main_content: string | null;
|
||||
}
|
||||
|
||||
export const load: PageServerLoad = async ({ params, fetch, cookies, parent }) => {
|
||||
export const load: PageServerLoad = async ({ params, fetch, cookies }) => {
|
||||
const websiteOverviewData = await fetch(
|
||||
`${API_BASE_PREFIX}/website_overview?id=eq.${params.websiteId}`,
|
||||
`${API_BASE_PREFIX}/website?id=eq.${params.websiteId}&select=*,settings(*),header(*),home(*),footer(*),article(*,docs_category(*)),legal_information(*)`,
|
||||
{
|
||||
method: "GET",
|
||||
headers: {
|
||||
@@ -48,8 +23,7 @@ export const load: PageServerLoad = async ({ params, fetch, cookies, parent }) =
|
||||
}
|
||||
);
|
||||
|
||||
const websiteOverview = await websiteOverviewData.json();
|
||||
const { website } = await parent();
|
||||
const websiteOverview: WebsiteOverview = await websiteOverviewData.json();
|
||||
|
||||
generateStaticFiles(websiteOverview);
|
||||
|
||||
@@ -70,15 +44,14 @@ export const load: PageServerLoad = async ({ params, fetch, cookies, parent }) =
|
||||
return {
|
||||
websiteOverview,
|
||||
websitePreviewUrl,
|
||||
websiteProdUrl,
|
||||
website
|
||||
websiteProdUrl
|
||||
};
|
||||
};
|
||||
|
||||
export const actions: Actions = {
|
||||
publishWebsite: async ({ fetch, params, cookies }) => {
|
||||
const websiteOverviewData = await fetch(
|
||||
`${API_BASE_PREFIX}/website_overview?id=eq.${params.websiteId}`,
|
||||
`${API_BASE_PREFIX}/website?id=eq.${params.websiteId}&select=*,settings(*),header(*),home(*),footer(*),article(*,docs_category(*)),legal_information(*)`,
|
||||
{
|
||||
method: "GET",
|
||||
headers: {
|
||||
@@ -112,54 +85,9 @@ export const actions: Actions = {
|
||||
}
|
||||
};
|
||||
|
||||
const generateStaticFiles = async (websiteData: WebsiteData, isPreview: boolean = true) => {
|
||||
let head = "";
|
||||
let body = "";
|
||||
|
||||
switch (websiteData.content_type) {
|
||||
case "Blog":
|
||||
{
|
||||
({ 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_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 ?? "")
|
||||
}
|
||||
}));
|
||||
}
|
||||
break;
|
||||
case "Docs":
|
||||
{
|
||||
({ head, body } = render(DocsIndex, {
|
||||
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_type === "text"
|
||||
? (websiteData.logo_text ?? "")
|
||||
: `${API_BASE_PREFIX}/rpc/retrieve_file?id=${websiteData.logo_image}`,
|
||||
mainContent: md(websiteData.main_content ?? "", false),
|
||||
categorizedArticles: websiteData.categorized_articles ?? [],
|
||||
footerAdditionalText: md(websiteData.additional_text ?? "")
|
||||
}
|
||||
}));
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
const indexFileContents = `
|
||||
const generateStaticFiles = async (websiteData: WebsiteOverview, isPreview: boolean = true) => {
|
||||
const fileContents = (head: string, body: string) => {
|
||||
return `
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
@@ -169,6 +97,15 @@ const generateStaticFiles = async (websiteData: WebsiteData, isPreview: boolean
|
||||
${body}
|
||||
</body>
|
||||
</html>`;
|
||||
};
|
||||
|
||||
const { head, body } = render(websiteData.content_type === "Blog" ? BlogIndex : DocsIndex, {
|
||||
props: {
|
||||
websiteOverview: websiteData,
|
||||
apiUrl: API_BASE_PREFIX,
|
||||
isLegalPage: false
|
||||
}
|
||||
});
|
||||
|
||||
let uploadDir = "";
|
||||
|
||||
@@ -179,138 +116,38 @@ const generateStaticFiles = async (websiteData: WebsiteData, isPreview: boolean
|
||||
}
|
||||
|
||||
await mkdir(uploadDir, { recursive: true });
|
||||
await writeFile(join(uploadDir, "index.html"), indexFileContents);
|
||||
await writeFile(join(uploadDir, "index.html"), fileContents(head, body));
|
||||
await mkdir(join(uploadDir, "articles"), {
|
||||
recursive: true
|
||||
});
|
||||
|
||||
for (const article of websiteData.articles ?? []) {
|
||||
for (const article of websiteData.article ?? []) {
|
||||
const articleFileName = article.title.toLowerCase().split(" ").join("-");
|
||||
|
||||
let head = "";
|
||||
let body = "";
|
||||
|
||||
switch (websiteData.content_type) {
|
||||
case "Blog":
|
||||
{
|
||||
({ head, body } = render(BlogArticle, {
|
||||
const { head, body } = render(websiteData.content_type === "Blog" ? BlogArticle : DocsArticle, {
|
||||
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_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}`
|
||||
: "",
|
||||
publicationDate: article.publication_date,
|
||||
mainContent: md(article.main_content ?? ""),
|
||||
footerAdditionalText: md(websiteData.additional_text ?? ""),
|
||||
metaDescription: article.meta_description
|
||||
websiteOverview: websiteData,
|
||||
article,
|
||||
apiUrl: API_BASE_PREFIX
|
||||
}
|
||||
}));
|
||||
});
|
||||
|
||||
await writeFile(
|
||||
join(uploadDir, "articles", `${articleFileName}.html`),
|
||||
fileContents(head, body)
|
||||
);
|
||||
}
|
||||
break;
|
||||
case "Docs":
|
||||
{
|
||||
({ head, body } = render(DocsArticle, {
|
||||
|
||||
if (websiteData.legal_information) {
|
||||
const { head, body } = render(websiteData.content_type === "Blog" ? BlogIndex : DocsIndex, {
|
||||
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_type === "text"
|
||||
? (websiteData.logo_text ?? "")
|
||||
: `${API_BASE_PREFIX}/rpc/retrieve_file?id=${websiteData.logo_image}`,
|
||||
mainContent: md(article.main_content ?? ""),
|
||||
categorizedArticles: websiteData.categorized_articles ?? [],
|
||||
footerAdditionalText: md(websiteData.additional_text ?? ""),
|
||||
metaDescription: article.meta_description
|
||||
}
|
||||
}));
|
||||
}
|
||||
break;
|
||||
websiteOverview: websiteData,
|
||||
apiUrl: API_BASE_PREFIX,
|
||||
isLegalPage: true
|
||||
}
|
||||
});
|
||||
|
||||
const articleFileContents = `
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
${head}
|
||||
</head>
|
||||
<body>
|
||||
${body}
|
||||
</body>
|
||||
</html>`;
|
||||
|
||||
await writeFile(join(uploadDir, "articles", `${articleFileName}.html`), articleFileContents);
|
||||
}
|
||||
|
||||
if (websiteData.legal_information_main_content) {
|
||||
let head = "";
|
||||
let body = "";
|
||||
|
||||
switch (websiteData.content_type) {
|
||||
case "Blog":
|
||||
{
|
||||
({ head, body } = render(BlogIndex, {
|
||||
props: {
|
||||
favicon: websiteData.favicon_image
|
||||
? `${API_BASE_PREFIX}/rpc/retrieve_file?id=${websiteData.favicon_image}`
|
||||
: "",
|
||||
title: "Legal information",
|
||||
logoType: websiteData.logo_type,
|
||||
logo:
|
||||
websiteData.logo_type === "text"
|
||||
? (websiteData.logo_text ?? "")
|
||||
: `${API_BASE_PREFIX}/rpc/retrieve_file?id=${websiteData.logo_image}`,
|
||||
mainContent: md(websiteData.legal_information_main_content ?? "", false),
|
||||
articles: [],
|
||||
footerAdditionalText: md(websiteData.additional_text ?? "")
|
||||
}
|
||||
}));
|
||||
}
|
||||
break;
|
||||
case "Docs":
|
||||
{
|
||||
({ head, body } = render(DocsIndex, {
|
||||
props: {
|
||||
favicon: websiteData.favicon_image
|
||||
? `${API_BASE_PREFIX}/rpc/retrieve_file?id=${websiteData.favicon_image}`
|
||||
: "",
|
||||
title: "Legal information",
|
||||
logoType: websiteData.logo_type,
|
||||
logo:
|
||||
websiteData.logo_type === "text"
|
||||
? (websiteData.logo_text ?? "")
|
||||
: `${API_BASE_PREFIX}/rpc/retrieve_file?id=${websiteData.logo_image}`,
|
||||
mainContent: md(websiteData.legal_information_main_content ?? "", false),
|
||||
categorizedArticles: {},
|
||||
footerAdditionalText: md(websiteData.additional_text ?? "")
|
||||
}
|
||||
}));
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
const legalInformationFileContents = `
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
${head}
|
||||
</head>
|
||||
<body>
|
||||
${body}
|
||||
</body>
|
||||
</html>`;
|
||||
|
||||
await writeFile(join(uploadDir, "legal-information.html"), legalInformationFileContents);
|
||||
await writeFile(join(uploadDir, "legal-information.html"), fileContents(head, body));
|
||||
}
|
||||
|
||||
const commonStyles = await readFile(`${process.cwd()}/template-styles/common-styles.css`, {
|
||||
@@ -328,14 +165,14 @@ const generateStaticFiles = async (websiteData: WebsiteData, isPreview: boolean
|
||||
.concat(specificStyles)
|
||||
.replace(
|
||||
/--color-accent:\s*(.*?);/,
|
||||
`--color-accent: ${websiteData.accent_color_dark_theme};`
|
||||
`--color-accent: ${websiteData.settings.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};`
|
||||
`--color-accent: ${websiteData.settings.accent_color_light_theme};`
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
@@ -17,9 +17,9 @@
|
||||
{/if}
|
||||
|
||||
<WebsiteEditor
|
||||
id={data.website.id}
|
||||
contentType={data.website.content_type}
|
||||
title={data.website.title}
|
||||
id={data.websiteOverview.id}
|
||||
contentType={data.websiteOverview.content_type}
|
||||
title={data.websiteOverview.title}
|
||||
previewContent={data.websitePreviewUrl}
|
||||
fullPreview={true}
|
||||
>
|
||||
@@ -46,7 +46,7 @@
|
||||
<button type="submit">Publish</button>
|
||||
</form>
|
||||
|
||||
{#if data.website.is_published}
|
||||
{#if data.websiteOverview.is_published}
|
||||
<section id="publication-status">
|
||||
<h3>
|
||||
<a href="#publication-status">Publication status</a>
|
||||
|
||||
@@ -25,11 +25,12 @@
|
||||
|
||||
--color-text: black;
|
||||
--color-text-invert: white;
|
||||
--color-border: hsl(0 0% 50%);
|
||||
--color-accent: hsl(210, 100%, 30%);
|
||||
--color-success: hsl(105, 100%, 30%);
|
||||
--color-error: hsl(0, 100%, 30%);
|
||||
|
||||
--border-primary: 0.0625rem solid var(--bg-tertiary);
|
||||
--border-primary: 0.0625rem solid var(--color-border);
|
||||
--border-radius: 0.125rem;
|
||||
|
||||
/* Step -1: 14.9953px → 14.2222px */
|
||||
@@ -222,6 +223,10 @@ a {
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
a:has(img) {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
:is(h1, h2, h3, h4, h5, h6) > a {
|
||||
color: var(--color-text);
|
||||
text-decoration: none;
|
||||
@@ -242,6 +247,7 @@ svg,
|
||||
video {
|
||||
max-inline-size: 100%;
|
||||
block-size: auto;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
p,
|
||||
|
||||
Reference in New Issue
Block a user