diff --git a/rest-api/db/migrations/20240719071602_main_tables.sql b/rest-api/db/migrations/20240719071602_main_tables.sql index d70a788..987297c 100644 --- a/rest-api/db/migrations/20240719071602_main_tables.sql +++ b/rest-api/db/migrations/20240719071602_main_tables.sql @@ -20,57 +20,57 @@ CREATE TABLE internal.user ( role NAME NOT NULL DEFAULT 'authenticated_user' ); -CREATE TABLE internal.cms_content ( +CREATE TABLE internal.website ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), owner_id UUID REFERENCES internal.user(id) ON DELETE CASCADE NOT NULL DEFAULT (current_setting('request.jwt.claims', true)::JSON->>'user_id')::UUID, content_type VARCHAR(10) CHECK (content_type IN ('Blog', 'Docs')) NOT NULL, - project_name VARCHAR(50) NOT NULL, + title VARCHAR(50) NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT CLOCK_TIMESTAMP(), last_modified_at TIMESTAMPTZ, last_modified_by UUID REFERENCES internal.user(id) ON DELETE SET NULL ); -CREATE TABLE internal.cms_media ( +CREATE TABLE internal.media ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - content_id UUID REFERENCES internal.cms_content(id) ON DELETE CASCADE NOT NULL, + website_id UUID REFERENCES internal.website(id) ON DELETE CASCADE NOT NULL, user_id UUID REFERENCES internal.user(id) ON DELETE CASCADE NOT NULL DEFAULT (current_setting('request.jwt.claims', true)::JSON->>'user_id')::UUID, original_name TEXT NOT NULL, file_system_path TEXT NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT CLOCK_TIMESTAMP() ); -CREATE TABLE internal.cms_settings ( - content_id UUID PRIMARY KEY REFERENCES internal.cms_content(id) ON DELETE CASCADE, +CREATE TABLE internal.settings ( + website_id UUID PRIMARY KEY REFERENCES internal.website(id) ON DELETE CASCADE, accent_color_light_theme CHAR(7) CHECK (accent_color_light_theme ~ '^#[a-fA-F0-9]{6}$') NOT NULL DEFAULT '#a5d8ff', accent_color_dark_theme CHAR(7) CHECK (accent_color_dark_theme ~ '^#[a-fA-F0-9]{6}$') NOT NULL DEFAULT '#114678', - favicon_image UUID REFERENCES internal.cms_media(id) ON DELETE SET NULL, + favicon_image UUID REFERENCES internal.media(id) ON DELETE SET NULL, last_modified_at TIMESTAMPTZ, last_modified_by UUID REFERENCES internal.user(id) ON DELETE SET NULL ); -CREATE TABLE internal.cms_header ( - content_id UUID PRIMARY KEY REFERENCES internal.cms_content(id) ON DELETE CASCADE, +CREATE TABLE internal.header ( + website_id UUID PRIMARY KEY REFERENCES internal.website(id) ON DELETE CASCADE, logo_type TEXT CHECK (logo_type IN ('text', 'image')) NOT NULL DEFAULT 'text', logo_text VARCHAR(255), - logo_image UUID REFERENCES internal.cms_media(id) ON DELETE SET NULL, + logo_image UUID REFERENCES internal.media(id) ON DELETE SET NULL, last_modified_at TIMESTAMPTZ, last_modified_by UUID REFERENCES internal.user(id) ON DELETE SET NULL ); -CREATE TABLE internal.cms_home ( - content_id UUID PRIMARY KEY REFERENCES internal.cms_content(id) ON DELETE CASCADE, +CREATE TABLE internal.home ( + website_id UUID PRIMARY KEY REFERENCES internal.website(id) ON DELETE CASCADE, main_content TEXT, last_modified_at TIMESTAMPTZ, last_modified_by UUID REFERENCES internal.user(id) ON DELETE SET NULL ); -CREATE TABLE internal.cms_article ( +CREATE TABLE internal.article ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - content_id UUID REFERENCES internal.cms_content(id) ON DELETE CASCADE NOT NULL, + website_id UUID REFERENCES internal.website(id) ON DELETE CASCADE NOT NULL, title VARCHAR(255) NOT NULL, meta_description VARCHAR(500), meta_author VARCHAR(255), - cover_image UUID REFERENCES internal.cms_media(id) ON DELETE SET NULL, + cover_image UUID REFERENCES internal.media(id) ON DELETE SET NULL, publication_date DATE NOT NULL DEFAULT CURRENT_DATE, main_content TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT CLOCK_TIMESTAMP(), @@ -78,43 +78,43 @@ CREATE TABLE internal.cms_article ( last_modified_by UUID REFERENCES internal.user(id) ON DELETE SET NULL ); -CREATE TABLE internal.cms_footer ( - content_id UUID PRIMARY KEY REFERENCES internal.cms_content(id) ON DELETE CASCADE, +CREATE TABLE internal.footer ( + website_id UUID PRIMARY KEY REFERENCES internal.website(id) ON DELETE CASCADE, additional_text VARCHAR(255), last_modified_at TIMESTAMPTZ, last_modified_by UUID REFERENCES internal.user(id) ON DELETE SET NULL ); -CREATE TABLE internal.cms_collab ( - content_id UUID REFERENCES internal.cms_content(id) ON DELETE CASCADE, +CREATE TABLE internal.collab ( + website_id UUID REFERENCES internal.website(id) ON DELETE CASCADE, user_id UUID REFERENCES internal.user(id) ON DELETE CASCADE, permission_level INTEGER CHECK (permission_level IN (10, 20, 30)) NOT NULL DEFAULT 10, added_at TIMESTAMPTZ NOT NULL DEFAULT CLOCK_TIMESTAMP(), last_modified_at TIMESTAMPTZ, last_modified_by UUID REFERENCES internal.user(id) ON DELETE SET NULL, - PRIMARY KEY (content_id, user_id) + PRIMARY KEY (website_id, user_id) ); -CREATE TABLE internal.cms_change_log ( - content_id UUID REFERENCES internal.cms_content(id) ON DELETE CASCADE, +CREATE TABLE internal.change_log ( + website_id UUID REFERENCES internal.website(id) ON DELETE CASCADE, user_id UUID REFERENCES internal.user(id) ON DELETE CASCADE DEFAULT (current_setting('request.jwt.claims', true)::JSON->>'user_id')::UUID, change_summary VARCHAR(255) NOT NULL, previous_value JSONB, new_value JSONB, timestamp TIMESTAMPTZ NOT NULL DEFAULT CLOCK_TIMESTAMP(), - PRIMARY KEY (content_id, user_id, timestamp) + PRIMARY KEY (website_id, user_id, timestamp) ); -- migrate:down -DROP TABLE internal.cms_change_log; -DROP TABLE internal.cms_collab; -DROP TABLE internal.cms_footer; -DROP TABLE internal.cms_article; -DROP TABLE internal.cms_home; -DROP TABLE internal.cms_header; -DROP TABLE internal.cms_settings; -DROP TABLE internal.cms_media; -DROP TABLE internal.cms_content; +DROP TABLE internal.change_log; +DROP TABLE internal.collab; +DROP TABLE internal.footer; +DROP TABLE internal.article; +DROP TABLE internal.home; +DROP TABLE internal.header; +DROP TABLE internal.settings; +DROP TABLE internal.media; +DROP TABLE internal.website; DROP SCHEMA api; DROP TABLE internal.user; diff --git a/rest-api/db/migrations/20240720132802_exposed_views_functions.sql b/rest-api/db/migrations/20240720132802_exposed_views_functions.sql index a40d5b9..0ea536d 100644 --- a/rest-api/db/migrations/20240720132802_exposed_views_functions.sql +++ b/rest-api/db/migrations/20240720132802_exposed_views_functions.sql @@ -5,136 +5,136 @@ AS SELECT id, username FROM internal.user; -CREATE VIEW api.cms_content +CREATE VIEW api.website WITH (security_invoker = on) AS SELECT * -FROM internal.cms_content; +FROM internal.website; -CREATE VIEW api.cms_media +CREATE VIEW api.media WITH (security_invoker = on) AS SELECT * -FROM internal.cms_media; +FROM internal.media; -CREATE VIEW api.cms_settings +CREATE VIEW api.settings WITH (security_invoker = on) AS SELECT * -FROM internal.cms_settings; +FROM internal.settings; -CREATE VIEW api.cms_header +CREATE VIEW api.header WITH (security_invoker = on) AS SELECT * -FROM internal.cms_header; +FROM internal.header; -CREATE view api.cms_home +CREATE view api.home WITH (security_invoker = on) AS SELECT * -FROM internal.cms_home; +FROM internal.home; -CREATE VIEW api.cms_article +CREATE VIEW api.article WITH (security_invoker = on) AS SELECT * -FROM internal.cms_article; +FROM internal.article; -CREATE VIEW api.cms_footer +CREATE VIEW api.footer WITH (security_invoker = on) AS SELECT * -FROM internal.cms_footer; +FROM internal.footer; -CREATE VIEW api.cms_collab +CREATE VIEW api.collab WITH (security_invoker = on) AS SELECT * -FROM internal.cms_collab; +FROM internal.collab; -CREATE VIEW api.cms_change_log +CREATE VIEW api.change_log WITH (security_invoker = on) AS SELECT * -FROM internal.cms_change_log; +FROM internal.change_log; CREATE FUNCTION -api.create_project(content_type VARCHAR(10), project_name VARCHAR(50), OUT content_id UUID) AS $$ +api.create_website(content_type VARCHAR(10), title VARCHAR(50), OUT website_id UUID) AS $$ DECLARE - _content_id UUID; + _website_id UUID; BEGIN - INSERT INTO internal.cms_content (content_type, project_name) - VALUES (create_project.content_type, create_project.project_name) - RETURNING id INTO _content_id; + INSERT INTO internal.website (content_type, title) + VALUES (create_website.content_type, create_website.title) + RETURNING id INTO _website_id; - INSERT INTO internal.cms_settings (content_id) - VALUES (_content_id); + INSERT INTO internal.settings (website_id) + VALUES (_website_id); - INSERT INTO internal.cms_header (content_id, logo_text) - VALUES (_content_id, 'archtika ' || create_project.content_type); + INSERT INTO internal.header (website_id, logo_text) + VALUES (_website_id, 'archtika ' || create_website.content_type); - INSERT INTO internal.cms_home (content_id, main_content) + INSERT INTO internal.home (website_id, main_content) VALUES - (_content_id, '## Main content comes in here'); + (_website_id, '## Main content comes in here'); - INSERT INTO internal.cms_article (content_id, title, meta_description, meta_author, main_content) + INSERT INTO internal.article (website_id, title, meta_description, meta_author, main_content) VALUES - (_content_id, 'First article', 'This is the first sample article', 'Author Name', '## First article'), - (_content_id, 'Second article', 'This is the second sample article', 'Author Name', '## Second article'); + (_website_id, 'First article', 'This is the first sample article', 'Author Name', '## First article'), + (_website_id, 'Second article', 'This is the second sample article', 'Author Name', '## Second article'); - INSERT INTO internal.cms_footer (content_id, additional_text) - VALUES (_content_id, 'This website was created with archtika'); + INSERT INTO internal.footer (website_id, additional_text) + VALUES (_website_id, 'This website was created with archtika'); - content_id := _content_id; + website_id := _website_id; END; $$ LANGUAGE plpgsql SECURITY DEFINER; -GRANT EXECUTE ON FUNCTION api.create_project(VARCHAR(10), VARCHAR(50)) TO authenticated_user; +GRANT EXECUTE ON FUNCTION api.create_website(VARCHAR(10), VARCHAR(50)) TO authenticated_user; -- Security invoker only works on views if the user has access to the underlying table GRANT SELECT ON internal.user TO authenticated_user; GRANT SELECT ON api.user TO authenticated_user; -GRANT SELECT, UPDATE, DELETE ON internal.cms_content TO authenticated_user; -GRANT SELECT, UPDATE, DELETE ON api.cms_content TO authenticated_user; -GRANT SELECT, INSERT ON internal.cms_media TO authenticated_user; -GRANT SELECT, INSERT ON api.cms_media TO authenticated_user; -GRANT SELECT, UPDATE ON internal.cms_settings TO authenticated_user; -GRANT SELECT, UPDATE ON api.cms_settings TO authenticated_user; -GRANT SELECT, UPDATE ON internal.cms_header TO authenticated_user; -GRANT SELECT, UPDATE ON api.cms_header TO authenticated_user; -GRANT SELECT, UPDATE ON internal.cms_home TO authenticated_user; -GRANT SELECT, UPDATE ON api.cms_home TO authenticated_user; -GRANT SELECT, INSERT, UPDATE, DELETE ON internal.cms_article TO authenticated_user; -GRANT SELECT, INSERT, UPDATE, DELETE ON api.cms_article TO authenticated_user; -GRANT SELECT, UPDATE ON internal.cms_footer TO authenticated_user; -GRANT SELECT, UPDATE ON api.cms_footer TO authenticated_user; -GRANT SELECT, INSERT, UPDATE, DELETE ON internal.cms_collab TO authenticated_user; -GRANT SELECT, INSERT, UPDATE, DELETE ON api.cms_collab TO authenticated_user; -GRANT SELECT ON internal.cms_change_log TO authenticated_user; -GRANT SELECT ON api.cms_change_log TO authenticated_user; +GRANT SELECT, UPDATE, DELETE ON internal.website TO authenticated_user; +GRANT SELECT, UPDATE, DELETE ON api.website TO authenticated_user; +GRANT SELECT, INSERT ON internal.media TO authenticated_user; +GRANT SELECT, INSERT ON api.media TO authenticated_user; +GRANT SELECT, UPDATE ON internal.settings TO authenticated_user; +GRANT SELECT, UPDATE ON api.settings TO authenticated_user; +GRANT SELECT, UPDATE ON internal.header TO authenticated_user; +GRANT SELECT, UPDATE ON api.header TO authenticated_user; +GRANT SELECT, UPDATE ON internal.home TO authenticated_user; +GRANT SELECT, UPDATE ON api.home TO authenticated_user; +GRANT SELECT, INSERT, UPDATE, DELETE ON internal.article TO authenticated_user; +GRANT SELECT, INSERT, UPDATE, DELETE ON api.article 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.collab TO authenticated_user; +GRANT SELECT, INSERT, UPDATE, DELETE ON api.collab TO authenticated_user; +GRANT SELECT ON internal.change_log TO authenticated_user; +GRANT SELECT ON api.change_log TO authenticated_user; -- migrate:down REVOKE SELECT ON internal.user FROM authenticated_user; -REVOKE SELECT, UPDATE, DELETE ON internal.cms_content FROM authenticated_user; -REVOKE SELECT, INSERT ON internal.cms_media FROM authenticated_user; -REVOKE SELECT, UPDATE ON internal.cms_settings FROM authenticated_user; -REVOKE SELECT, UPDATE ON internal.cms_header FROM authenticated_user; -REVOKE SELECT, INSERT, UPDATE, DELETE ON internal.cms_article FROM authenticated_user; -REVOKE SELECT, UPDATE ON internal.cms_footer FROM authenticated_user; -REVOKE SELECT, INSERT, UPDATE, DELETE ON internal.cms_collab FROM authenticated_user; -REVOKE SELECT ON internal.cms_change_log FROM authenticated_user; +REVOKE SELECT, UPDATE, DELETE ON internal.website FROM authenticated_user; +REVOKE SELECT, INSERT ON internal.media FROM authenticated_user; +REVOKE SELECT, UPDATE ON internal.settings FROM authenticated_user; +REVOKE SELECT, UPDATE ON internal.header FROM authenticated_user; +REVOKE SELECT, INSERT, UPDATE, DELETE ON internal.article FROM authenticated_user; +REVOKE SELECT, UPDATE ON internal.footer FROM authenticated_user; +REVOKE SELECT, INSERT, UPDATE, DELETE ON internal.collab FROM authenticated_user; +REVOKE SELECT ON internal.change_log FROM authenticated_user; -DROP FUNCTION api.create_project(VARCHAR(10), VARCHAR(50)); +DROP FUNCTION api.create_website(VARCHAR(10), VARCHAR(50)); -DROP VIEW api.cms_change_log; -DROP VIEW api.cms_collab; -DROP VIEW api.cms_footer; -DROP VIEW api.cms_home; -DROP VIEW api.cms_article; -DROP VIEW api.cms_header; -DROP VIEW api.cms_settings; -DROP VIEW api.cms_media; -DROP VIEW api.cms_content; +DROP VIEW api.change_log; +DROP VIEW api.collab; +DROP VIEW api.footer; +DROP VIEW api.home; +DROP VIEW api.article; +DROP VIEW api.header; +DROP VIEW api.settings; +DROP VIEW api.media; +DROP VIEW api.website; DROP VIEW api.user; \ No newline at end of file diff --git a/rest-api/db/migrations/20240724191017_row_level_security.sql b/rest-api/db/migrations/20240724191017_row_level_security.sql index c47af36..bc588ce 100644 --- a/rest-api/db/migrations/20240724191017_row_level_security.sql +++ b/rest-api/db/migrations/20240724191017_row_level_security.sql @@ -1,208 +1,208 @@ -- migrate:up ALTER TABLE internal.user ENABLE ROW LEVEL SECURITY; -ALTER TABLE internal.cms_content ENABLE ROW LEVEL SECURITY; -ALTER TABLE internal.cms_media ENABLE ROW LEVEL SECURITY; -ALTER TABLE internal.cms_settings ENABLE ROW LEVEL SECURITY; -ALTER TABLE internal.cms_header ENABLE ROW LEVEL SECURITY; -ALTER TABLE internal.cms_home ENABLE ROW LEVEL SECURITY; -ALTER TABLE internal.cms_article ENABLE ROW LEVEL SECURITY; -ALTER TABLE internal.cms_footer ENABLE ROW LEVEL SECURITY; +ALTER TABLE internal.website ENABLE ROW LEVEL SECURITY; +ALTER TABLE internal.media ENABLE ROW LEVEL SECURITY; +ALTER TABLE internal.settings ENABLE ROW LEVEL SECURITY; +ALTER TABLE internal.header ENABLE ROW LEVEL SECURITY; +ALTER TABLE internal.home ENABLE ROW LEVEL SECURITY; +ALTER TABLE internal.article ENABLE ROW LEVEL SECURITY; +ALTER TABLE internal.footer ENABLE ROW LEVEL SECURITY; CREATE POLICY view_own_user ON internal.user FOR SELECT USING (id = (current_setting('request.jwt.claims', true)::json->>'user_id')::UUID); -CREATE POLICY view_own_projects ON internal.cms_content +CREATE POLICY view_own_websites ON internal.website FOR SELECT USING (owner_id = (current_setting('request.jwt.claims', true)::json->>'user_id')::UUID); -CREATE POLICY update_own_project ON internal.cms_content +CREATE POLICY update_own_website ON internal.website FOR UPDATE USING (owner_id = (current_setting('request.jwt.claims', true)::json->>'user_id')::UUID); -CREATE POLICY delete_own_project ON internal.cms_content +CREATE POLICY delete_own_website ON internal.website FOR DELETE USING (owner_id = (current_setting('request.jwt.claims', true)::json->>'user_id')::UUID); -CREATE POLICY view_own_media ON internal.cms_media +CREATE POLICY view_own_media ON internal.media FOR SELECT USING (user_id = (current_setting('request.jwt.claims', true)::json->>'user_id')::UUID); -CREATE POLICY insert_own_media ON internal.cms_media +CREATE POLICY insert_own_media ON internal.media FOR INSERT WITH CHECK ( EXISTS ( SELECT 1 - FROM internal.cms_content - WHERE internal.cms_content.id = internal.cms_media.content_id - AND internal.cms_content.owner_id = (current_setting('request.jwt.claims', true)::json->>'user_id')::UUID + FROM internal.website + WHERE internal.website.id = internal.media.website_id + AND internal.website.owner_id = (current_setting('request.jwt.claims', true)::json->>'user_id')::UUID ) ); -CREATE POLICY view_own_settings ON internal.cms_settings +CREATE POLICY view_own_settings ON internal.settings FOR SELECT USING ( EXISTS ( SELECT 1 - FROM internal.cms_content - WHERE internal.cms_content.id = internal.cms_settings.content_id - AND internal.cms_content.owner_id = (current_setting('request.jwt.claims', true)::json->>'user_id')::UUID + FROM internal.website + WHERE internal.website.id = internal.settings.website_id + AND internal.website.owner_id = (current_setting('request.jwt.claims', true)::json->>'user_id')::UUID ) ); -CREATE POLICY update_own_settings ON internal.cms_settings +CREATE POLICY update_own_settings ON internal.settings FOR UPDATE USING ( EXISTS ( SELECT 1 - FROM internal.cms_content - WHERE internal.cms_content.id = internal.cms_settings.content_id - AND internal.cms_content.owner_id = (current_setting('request.jwt.claims', true)::json->>'user_id')::UUID + FROM internal.website + WHERE internal.website.id = internal.settings.website_id + AND internal.website.owner_id = (current_setting('request.jwt.claims', true)::json->>'user_id')::UUID ) ); -CREATE POLICY view_own_header ON internal.cms_header +CREATE POLICY view_own_header ON internal.header FOR SELECT USING ( EXISTS ( SELECT 1 - FROM internal.cms_content - WHERE internal.cms_content.id = internal.cms_header.content_id - AND internal.cms_content.owner_id = (current_setting('request.jwt.claims', true)::json->>'user_id')::UUID + FROM internal.website + WHERE internal.website.id = internal.header.website_id + AND internal.website.owner_id = (current_setting('request.jwt.claims', true)::json->>'user_id')::UUID ) ); -CREATE POLICY update_own_header ON internal.cms_header +CREATE POLICY update_own_header ON internal.header FOR UPDATE USING ( EXISTS ( SELECT 1 - FROM internal.cms_content - WHERE internal.cms_content.id = internal.cms_header.content_id - AND internal.cms_content.owner_id = (current_setting('request.jwt.claims', true)::json->>'user_id')::UUID + FROM internal.website + WHERE internal.website.id = internal.header.website_id + AND internal.website.owner_id = (current_setting('request.jwt.claims', true)::json->>'user_id')::UUID ) ); -CREATE POLICY view_own_home ON internal.cms_home +CREATE POLICY view_own_home ON internal.home FOR SELECT USING ( EXISTS ( SELECT 1 - FROM internal.cms_content - WHERE internal.cms_content.id = internal.cms_home.content_id - AND internal.cms_content.owner_id = (current_setting('request.jwt.claims', true)::json->>'user_id')::UUID + FROM internal.website + WHERE internal.website.id = internal.home.website_id + AND internal.website.owner_id = (current_setting('request.jwt.claims', true)::json->>'user_id')::UUID ) ); -CREATE POLICY update_own_home ON internal.cms_home +CREATE POLICY update_own_home ON internal.home FOR UPDATE USING ( EXISTS ( SELECT 1 - FROM internal.cms_content - WHERE internal.cms_content.id = internal.cms_home.content_id - AND internal.cms_content.owner_id = (current_setting('request.jwt.claims', true)::json->>'user_id')::UUID + FROM internal.website + WHERE internal.website.id = internal.home.website_id + AND internal.website.owner_id = (current_setting('request.jwt.claims', true)::json->>'user_id')::UUID ) ); -CREATE POLICY view_own_articles ON internal.cms_article +CREATE POLICY view_own_articles ON internal.article FOR SELECT USING ( EXISTS ( SELECT 1 - FROM internal.cms_content - WHERE internal.cms_content.id = internal.cms_article.content_id - AND internal.cms_content.owner_id = (current_setting('request.jwt.claims', true)::json->>'user_id')::UUID + FROM internal.website + WHERE internal.website.id = internal.article.website_id + AND internal.website.owner_id = (current_setting('request.jwt.claims', true)::json->>'user_id')::UUID ) ); -CREATE POLICY update_own_article ON internal.cms_article +CREATE POLICY update_own_article ON internal.article FOR UPDATE USING ( EXISTS ( SELECT 1 - FROM internal.cms_content - WHERE internal.cms_content.id = internal.cms_article.content_id - AND internal.cms_content.owner_id = (current_setting('request.jwt.claims', true)::json->>'user_id')::UUID + FROM internal.website + WHERE internal.website.id = internal.article.website_id + AND internal.website.owner_id = (current_setting('request.jwt.claims', true)::json->>'user_id')::UUID ) ); -CREATE POLICY delete_own_article ON internal.cms_article +CREATE POLICY delete_own_article ON internal.article FOR DELETE USING ( EXISTS ( SELECT 1 - FROM internal.cms_content - WHERE internal.cms_content.id = internal.cms_article.content_id - AND internal.cms_content.owner_id = (current_setting('request.jwt.claims', true)::json->>'user_id')::UUID + FROM internal.website + WHERE internal.website.id = internal.article.website_id + AND internal.website.owner_id = (current_setting('request.jwt.claims', true)::json->>'user_id')::UUID ) ); -CREATE POLICY insert_own_article ON internal.cms_article +CREATE POLICY insert_own_article ON internal.article FOR INSERT WITH CHECK ( EXISTS ( SELECT 1 - FROM internal.cms_content - WHERE internal.cms_content.id = internal.cms_article.content_id - AND internal.cms_content.owner_id = (current_setting('request.jwt.claims', true)::json->>'user_id')::UUID + FROM internal.website + WHERE internal.website.id = internal.article.website_id + AND internal.website.owner_id = (current_setting('request.jwt.claims', true)::json->>'user_id')::UUID ) ); -CREATE POLICY view_own_footer ON internal.cms_footer +CREATE POLICY view_own_footer ON internal.footer FOR SELECT USING ( EXISTS ( SELECT 1 - FROM internal.cms_content - WHERE internal.cms_content.id = internal.cms_footer.content_id - AND internal.cms_content.owner_id = (current_setting('request.jwt.claims', true)::json->>'user_id')::UUID + FROM internal.website + WHERE internal.website.id = internal.footer.website_id + AND internal.website.owner_id = (current_setting('request.jwt.claims', true)::json->>'user_id')::UUID ) ); -CREATE POLICY update_own_footer ON internal.cms_footer +CREATE POLICY update_own_footer ON internal.footer FOR UPDATE USING ( EXISTS ( SELECT 1 - FROM internal.cms_content - WHERE internal.cms_content.id = internal.cms_footer.content_id - AND internal.cms_content.owner_id = (current_setting('request.jwt.claims', true)::json->>'user_id')::UUID + FROM internal.website + WHERE internal.website.id = internal.footer.website_id + AND internal.website.owner_id = (current_setting('request.jwt.claims', true)::json->>'user_id')::UUID ) ); -- migrate:down DROP POLICY view_own_user ON internal.user; -DROP POLICY view_own_projects ON internal.cms_content; -DROP POLICY delete_own_project ON internal.cms_content; -DROP POLICY update_own_project ON internal.cms_content; -DROP POLICY view_own_media ON internal.cms_media; -DROP POLICY insert_own_media ON internal.cms_media; -DROP POLICY view_own_settings ON internal.cms_settings; -DROP POLICY update_own_settings ON internal.cms_settings; -DROP POLICY view_own_header ON internal.cms_header; -DROP POLICY update_own_header ON internal.cms_header; -DROP POLICY view_own_home ON internal.cms_home; -DROP POLICY update_own_home ON internal.cms_home; -DROP POLICY view_own_articles ON internal.cms_article; -DROP POLICY update_own_article ON internal.cms_article; -DROP POLICY delete_own_article ON internal.cms_article; -DROP POLICY insert_own_article ON internal.cms_article; -DROP POLICY view_own_footer ON internal.cms_footer; -DROP POLICY update_own_footer ON internal.cms_footer; +DROP POLICY view_own_websites ON internal.website; +DROP POLICY delete_own_website ON internal.website; +DROP POLICY update_own_website ON internal.website; +DROP POLICY view_own_media ON internal.media; +DROP POLICY insert_own_media ON internal.media; +DROP POLICY view_own_settings ON internal.settings; +DROP POLICY update_own_settings ON internal.settings; +DROP POLICY view_own_header ON internal.header; +DROP POLICY update_own_header ON internal.header; +DROP POLICY view_own_home ON internal.home; +DROP POLICY update_own_home ON internal.home; +DROP POLICY view_own_articles ON internal.article; +DROP POLICY update_own_article ON internal.article; +DROP POLICY delete_own_article ON internal.article; +DROP POLICY insert_own_article ON internal.article; +DROP POLICY view_own_footer ON internal.footer; +DROP POLICY update_own_footer ON internal.footer; ALTER TABLE internal.user DISABLE ROW LEVEL SECURITY; -ALTER TABLE internal.cms_content DISABLE ROW LEVEL SECURITY; -ALTER TABLE internal.cms_media DISABLE ROW LEVEL SECURITY; -ALTER TABLE internal.cms_settings DISABLE ROW LEVEL SECURITY; -ALTER TABLE internal.cms_header DISABLE ROW LEVEL SECURITY; -ALTER TABLE internal.cms_home DISABLE ROW LEVEL SECURITY; -ALTER TABLE internal.cms_article DISABLE ROW LEVEL SECURITY; -ALTER TABLE internal.cms_footer DISABLE ROW LEVEL SECURITY; \ No newline at end of file +ALTER TABLE internal.website DISABLE ROW LEVEL SECURITY; +ALTER TABLE internal.media DISABLE ROW LEVEL SECURITY; +ALTER TABLE internal.settings DISABLE ROW LEVEL SECURITY; +ALTER TABLE internal.header DISABLE ROW LEVEL SECURITY; +ALTER TABLE internal.home DISABLE ROW LEVEL SECURITY; +ALTER TABLE internal.article DISABLE ROW LEVEL SECURITY; +ALTER TABLE internal.footer DISABLE ROW LEVEL SECURITY; \ No newline at end of file diff --git a/web-app/src/routes/(anonymous)/login/+page.svelte b/web-app/src/routes/(anonymous)/login/+page.svelte index 489c2e9..f086a8b 100644 --- a/web-app/src/routes/(anonymous)/login/+page.svelte +++ b/web-app/src/routes/(anonymous)/login/+page.svelte @@ -1 +1,26 @@ -
+ + +
+ {#if form?.success} +

Successfully logged in

+ {/if} + + {#if form?.success === false} +

{form.message}

+ {/if} + + + + + +
diff --git a/web-app/src/routes/(anonymous)/register/+page.svelte b/web-app/src/routes/(anonymous)/register/+page.svelte index 489c2e9..b365c94 100644 --- a/web-app/src/routes/(anonymous)/register/+page.svelte +++ b/web-app/src/routes/(anonymous)/register/+page.svelte @@ -1 +1,26 @@ -
+ + +
+ {#if form?.success} +

Successfully registered

+ {/if} + + {#if form?.success === false} +

{form.message}

+ {/if} + + + + + +
diff --git a/web-app/src/routes/(authenticated)/+page.server.ts b/web-app/src/routes/(authenticated)/+page.server.ts new file mode 100644 index 0000000..0e9043d --- /dev/null +++ b/web-app/src/routes/(authenticated)/+page.server.ts @@ -0,0 +1,107 @@ +export const load = async ({ fetch, cookies, url }) => { + const searchQuery = url.searchParams.get("website_search_query"); + const sortBy = url.searchParams.get("website_sort"); + + const params = new URLSearchParams(); + + const baseFetchUrl = "http://localhost:3000/website"; + + if (searchQuery) { + params.append("website_title", `ilike.*${searchQuery}*`); + } + + switch (sortBy) { + case "creation-time": + params.append("order", "created_at.desc"); + break; + case "last-modified": + params.append("order", "last_modified_at.desc"); + break; + case "title-a-to-z": + params.append("order", "title.asc"); + break; + case "title-z-to-a": + params.append("order", "title.desc"); + break; + } + + const constructedFetchUrl = `${baseFetchUrl}?${params.toString()}`; + + const websiteData = await fetch(constructedFetchUrl, { + method: "GET", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${cookies.get("session_token")}` + } + }); + + const websites = await websiteData.json(); + + return { + websites + }; +}; + +export const actions = { + createWebsite: async ({ request, fetch, cookies }) => { + const data = await request.formData(); + + const res = await fetch("http://localhost:3000/rpc/create_website", { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${cookies.get("session_token")}` + }, + body: JSON.stringify({ + content_type: data.get("content-type"), + title: data.get("title") + }) + }); + + if (!res.ok) { + const response = await res.json(); + return { createWebsite: { success: false, message: response.message } }; + } + + return { createWebsite: { success: true, operation: "created" } }; + }, + updateWebsite: async ({ request, cookies, fetch }) => { + const data = await request.formData(); + + const res = await fetch(`http://localhost:3000/website?id=eq.${data.get("id")}`, { + method: "PATCH", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${cookies.get("session_token")}` + }, + body: JSON.stringify({ + title: data.get("title") + }) + }); + + if (!res.ok) { + const response = await res.json(); + return { updateWebsite: { success: false, message: response.message } }; + } + + return { updateWebsite: { success: true, operation: "updated" } }; + }, + deleteWebsite: async ({ request, cookies, fetch }) => { + const data = await request.formData(); + + const res = await fetch(`http://localhost:3000/website?id=eq.${data.get("id")}`, { + method: "DELETE", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${cookies.get("session_token")}` + } + }); + + if (!res.ok) { + const response = await res.json(); + return { deleteWebsite: { success: false, message: response.message } }; + } + + return { deleteWebsite: { success: true, operation: "deleted" } }; + } +}; diff --git a/web-app/src/routes/(authenticated)/+page.svelte b/web-app/src/routes/(authenticated)/+page.svelte new file mode 100644 index 0000000..2d00648 --- /dev/null +++ b/web-app/src/routes/(authenticated)/+page.svelte @@ -0,0 +1,103 @@ + + +
+

Create website

+ +
+ {#if form?.createWebsite?.success} +

Successfully created website

+ {/if} + + {#if form?.createWebsite?.success === false} +

{form.createWebsite.message}

+ {/if} + + + + + +
+
+ +
+

Your websites

+ + {#if form?.deleteWebsite?.success} +

Successfully deleted website

+ {/if} + + {#if form?.deleteWebsite?.success === false} +

{form.deleteWebsite.message}

+ {/if} + + {#each data.websites as { id, content_type, title, created_at }} +
+

+ {title} +

+

+ Type: + {content_type} +

+

+ Created at: + +

+
+ Update +
{ + return async ({ update }) => { + await update({ reset: false }); + }; + }} + > + {#if form?.updateWebsite?.success} +

Successfully updated website

+ {/if} + + {#if form?.updateWebsite?.success === false} +

{form.updateWebsite.message}

+ {/if} + + + + + +
+
+
+ Delete + +
+ + + +
+
+
+ {/each} +
+ +
+

Shared with you

+
diff --git a/web-app/src/routes/(authenticated)/account/+page.server.ts b/web-app/src/routes/(authenticated)/account/+page.server.ts new file mode 100644 index 0000000..75b6153 --- /dev/null +++ b/web-app/src/routes/(authenticated)/account/+page.server.ts @@ -0,0 +1,36 @@ +export const load = async ({ locals }) => { + return { + user: locals.user + }; +}; + +export const actions = { + logout: async ({ cookies }) => { + cookies.delete("session_token", { path: "/" }); + + return { logout: { success: true } }; + }, + deleteAccount: async ({ request, fetch, cookies }) => { + const data = await request.formData(); + + const res = await fetch("http://localhost:3000/rpc/delete_account", { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${cookies.get("session_token")}` + }, + body: JSON.stringify({ + password: data.get("password") + }) + }); + + const response = await res.json(); + + if (!res.ok) { + return { deleteAccount: { success: false, message: response.message } }; + } + + cookies.delete("session_token", { path: "/" }); + return { deleteAccount: { success: true } }; + } +}; diff --git a/web-app/src/routes/(authenticated)/account/+page.svelte b/web-app/src/routes/(authenticated)/account/+page.svelte new file mode 100644 index 0000000..8efddd9 --- /dev/null +++ b/web-app/src/routes/(authenticated)/account/+page.svelte @@ -0,0 +1,51 @@ + + +
+

Overview

+ +

+ Username: + {data.user.username} +

+

+ ID: + {data.user.id} +

+
+ +
+

Logout

+ + {#if form?.logout?.success} +

Successfully logged out

+ {/if} + +
+ +
+
+ +
+

Delete account

+ + {#if form?.deleteAccount?.success} +

Account was deleted

+ {/if} + + {#if form?.deleteAccount?.success === false} +

{form.deleteAccount.message}

+ {/if} + +
+ + + +
+
diff --git a/web-app/src/routes/(authenticated)/website/[websiteId]/+layout.server.ts b/web-app/src/routes/(authenticated)/website/[websiteId]/+layout.server.ts index 9eee387..339100d 100644 --- a/web-app/src/routes/(authenticated)/website/[websiteId]/+layout.server.ts +++ b/web-app/src/routes/(authenticated)/website/[websiteId]/+layout.server.ts @@ -1,5 +1,5 @@ export const load = async ({ params, fetch, cookies }) => { - const websiteData = await fetch(`http://localhost:3000/cms_content?id=eq.${params.websiteId}`, { + const websiteData = await fetch(`http://localhost:3000/content?id=eq.${params.websiteId}`, { method: "GET", headers: { "Content-Type": "application/json", diff --git a/web-app/src/routes/(authenticated)/website/[websiteId]/+page.server.ts b/web-app/src/routes/(authenticated)/website/[websiteId]/+page.server.ts index b080e58..641eb71 100644 --- a/web-app/src/routes/(authenticated)/website/[websiteId]/+page.server.ts +++ b/web-app/src/routes/(authenticated)/website/[websiteId]/+page.server.ts @@ -5,7 +5,7 @@ import { ALLOWED_MIME_TYPES } from "$lib/utils.js"; export const load = async ({ params, fetch, cookies, url }) => { const globalSettingsData = await fetch( - `http://localhost:3000/cms_settings?content_id=eq.${params.websiteId}&select=*,cms_media(*)`, + `http://localhost:3000/settings?website_id=eq.${params.websiteId}&select=*,media(*)`, { method: "GET", headers: { @@ -17,7 +17,7 @@ export const load = async ({ params, fetch, cookies, url }) => { ); const headerData = await fetch( - `http://localhost:3000/cms_header?content_id=eq.${params.websiteId}&select=*,cms_media(*)`, + `http://localhost:3000/header?website_id=eq.${params.websiteId}&select=*,media(*)`, { method: "GET", headers: { @@ -28,7 +28,7 @@ export const load = async ({ params, fetch, cookies, url }) => { } ); - const homeData = await fetch(`http://localhost:3000/cms_home?content_id=eq.${params.websiteId}`, { + const homeData = await fetch(`http://localhost:3000/home?website_id=eq.${params.websiteId}`, { method: "GET", headers: { "Content-Type": "application/json", @@ -37,24 +37,21 @@ export const load = async ({ params, fetch, cookies, url }) => { } }); - const footerData = await fetch( - `http://localhost:3000/cms_footer?content_id=eq.${params.websiteId}`, - { - method: "GET", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${cookies.get("session_token")}`, - Accept: "application/vnd.pgrst.object+json" - } + const footerData = await fetch(`http://localhost:3000/footer?website_id=eq.${params.websiteId}`, { + method: "GET", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${cookies.get("session_token")}`, + Accept: "application/vnd.pgrst.object+json" } - ); + }); const searchQuery = url.searchParams.get("article_search_query"); const sortBy = url.searchParams.get("article_sort"); const parameters = new URLSearchParams(); - const baseFetchUrl = `http://localhost:3000/cms_article?content_id=eq.${params.websiteId}&select=*,cms_media(*)`; + const baseFetchUrl = `http://localhost:3000/article?website_id=eq.${params.websiteId}&select=*,media(*)`; if (searchQuery) { parameters.append("title", `ilike.*${searchQuery}*`); @@ -117,21 +114,18 @@ export const actions = { return favicon; } - const res = await fetch( - `http://localhost:3000/cms_settings?content_id=eq.${params.websiteId}`, - { - method: "PATCH", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${cookies.get("session_token")}` - }, - body: JSON.stringify({ - accent_color_light_theme: data.get("accent-color-light"), - accent_color_dark_theme: data.get("accent-color-dark"), - favicon_image: favicon?.content - }) - } - ); + const res = await fetch(`http://localhost:3000/settings?website_id=eq.${params.websiteId}`, { + method: "PATCH", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${cookies.get("session_token")}` + }, + body: JSON.stringify({ + accent_color_light_theme: data.get("accent-color-light"), + accent_color_dark_theme: data.get("accent-color-dark"), + favicon_image: favicon?.content + }) + }); if (!res.ok) { const response = await res.json(); @@ -160,7 +154,7 @@ export const actions = { return logo; } - const res = await fetch(`http://localhost:3000/cms_header?content_id=eq.${params.websiteId}`, { + const res = await fetch(`http://localhost:3000/header?website_id=eq.${params.websiteId}`, { method: "PATCH", headers: { "Content-Type": "application/json", @@ -187,7 +181,7 @@ export const actions = { updateHome: async ({ request, fetch, cookies, params }) => { const data = await request.formData(); - const res = await fetch(`http://localhost:3000/cms_home?content_id=eq.${params.websiteId}`, { + const res = await fetch(`http://localhost:3000/home?website_id=eq.${params.websiteId}`, { method: "PATCH", headers: { "Content-Type": "application/json", @@ -208,7 +202,7 @@ export const actions = { updateFooter: async ({ request, fetch, cookies, params }) => { const data = await request.formData(); - const res = await fetch(`http://localhost:3000/cms_footer?content_id=eq.${params.websiteId}`, { + const res = await fetch(`http://localhost:3000/footer?website_id=eq.${params.websiteId}`, { method: "PATCH", headers: { "Content-Type": "application/json", @@ -233,14 +227,14 @@ export const actions = { createArticle: async ({ request, fetch, cookies, params }) => { const data = await request.formData(); - const res = await fetch("http://localhost:3000/cms_article", { + const res = await fetch("http://localhost:3000/article", { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${cookies.get("session_token")}` }, body: JSON.stringify({ - content_id: params.websiteId, + website_id: params.websiteId, title: data.get("title") }) }); @@ -268,7 +262,7 @@ export const actions = { return cover; } - const res = await fetch(`http://localhost:3000/cms_article?id=eq.${data.get("article-id")}`, { + const res = await fetch(`http://localhost:3000/article?id=eq.${data.get("article-id")}`, { method: "PATCH", headers: { "Content-Type": "application/json", @@ -294,7 +288,7 @@ export const actions = { deleteArticle: async ({ request, fetch, cookies }) => { const data = await request.formData(); - const res = await fetch(`http://localhost:3000/cms_article?id=eq.${data.get("article-id")}`, { + const res = await fetch(`http://localhost:3000/article?id=eq.${data.get("article-id")}`, { method: "DELETE", headers: { "Content-Type": "application/json", @@ -348,7 +342,7 @@ const handleFileUpload = async ( const relativePath = relative(join(process.cwd(), "static"), filepath); - const res = await customFetch("http://localhost:3000/cms_media", { + const res = await customFetch("http://localhost:3000/media", { method: "POST", headers: { "Content-Type": "application/json", @@ -357,7 +351,7 @@ const handleFileUpload = async ( Accept: "application/vnd.pgrst.object+json" }, body: JSON.stringify({ - content_id: contentId, + website_id: contentId, user_id: userId, original_name: file.name, file_system_path: relativePath diff --git a/web-app/src/routes/+layout.svelte b/web-app/src/routes/+layout.svelte new file mode 100644 index 0000000..de0cab5 --- /dev/null +++ b/web-app/src/routes/+layout.svelte @@ -0,0 +1,28 @@ + + + + +
+

{$page.url.pathname}

+
+ +
+ {@render children()} +
+ + diff --git a/web-app/src/routes/+page.svelte b/web-app/src/routes/+page.svelte deleted file mode 100644 index 160f060..0000000 --- a/web-app/src/routes/+page.svelte +++ /dev/null @@ -1,11 +0,0 @@ -
-

Create website

-
- -
-

Your websites

-
- -
-

Shared with you

-