mirror of
https://github.com/thiloho/archtika.git
synced 2025-11-22 10:51:36 +01:00
Update README, row level policies for collaborators and articles and fix minor issues
This commit is contained in:
15
README.md
15
README.md
@@ -1,5 +1,20 @@
|
||||
# archtika
|
||||
|
||||
## About
|
||||
|
||||
archtika is a FLOSS, modern, performant and lightweight CMS (Content Mangement System) in the form of a web application. It allows you to easily create, manage and publish minimal, responsive and SEO friendly blogging and documentation websites with official, professionally designed templates.
|
||||
|
||||
It is also possible to add contributors to your sites, which is very useful for larger projects where, for example, several people are constantly working on the documentation.
|
||||
|
||||
## How it works
|
||||
|
||||
For the backend, PostgreSQL is used in combination with PostgREST to create a RESTful API. JSON web tokens along with row-level security control authentication and authorisation flows.
|
||||
|
||||
The web application uses SvelteKit with SSR (Server Side Rendering) and Svelte version 5, currently in beta.
|
||||
|
||||
NGINX is used to deploy the websites, serving the static site files from the `/var/www/archtika-websites` directory. The static files can be found in this directory via the path `<user_id>/<website_id>`, which is dynamically created by the web application.
|
||||
|
||||
|
||||
## Virtual machine for local development
|
||||
|
||||
The website directory used by the virtual machine needs to be created and the NodeJS process, which typically runs as the default system user, needs permission to write to this directory.
|
||||
|
||||
@@ -143,15 +143,24 @@ BEGIN
|
||||
|
||||
INSERT INTO internal.home (website_id, main_content)
|
||||
VALUES
|
||||
(_website_id, '## Main content comes in here');
|
||||
(_website_id, '
|
||||
## About
|
||||
|
||||
INSERT INTO internal.article (website_id, user_id, title, meta_description, meta_author, main_content)
|
||||
VALUES
|
||||
(_website_id, _user_id, 'First article', 'This is the first sample article', 'Author Name', '## First article'),
|
||||
(_website_id, _user_id, 'Second article', 'This is the second sample article', 'Author Name', '## Second article');
|
||||
archtika is a FLOSS, modern, performant and lightweight CMS (Content Mangement System) in the form of a web application. It allows you to easily create, manage and publish minimal, responsive and SEO friendly blogging and documentation websites with official, professionally designed templates.
|
||||
|
||||
It is also possible to add contributors to your sites, which is very useful for larger projects where, for example, several people are constantly working on the documentation.
|
||||
|
||||
## How it works
|
||||
|
||||
For the backend, PostgreSQL is used in combination with PostgREST to create a RESTful API. JSON web tokens along with row-level security control authentication and authorisation flows.
|
||||
|
||||
The web application uses SvelteKit with SSR (Server Side Rendering) and Svelte version 5, currently in beta.
|
||||
|
||||
NGINX is used to deploy the websites, serving the static site files from the `/var/www/archtika-websites` directory. The static files can be found in this directory via the path `<user_id>/<website_id>`, which is dynamically created by the web application.
|
||||
');
|
||||
|
||||
INSERT INTO internal.footer (website_id, additional_text)
|
||||
VALUES (_website_id, 'This website was created with archtika');
|
||||
VALUES (_website_id, 'archtika is a free, open, modern, performant and lightweight CMS');
|
||||
|
||||
website_id := _website_id;
|
||||
END;
|
||||
|
||||
@@ -9,7 +9,14 @@ ALTER TABLE internal.article ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE internal.footer 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 DEFAULT 10)
|
||||
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 $$
|
||||
DECLARE
|
||||
_user_id UUID;
|
||||
@@ -33,8 +40,30 @@ BEGIN
|
||||
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.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
|
||||
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;
|
||||
@@ -46,7 +75,7 @@ USING (true);
|
||||
|
||||
CREATE POLICY view_websites ON internal.website
|
||||
FOR SELECT
|
||||
USING (internal.user_has_website_access(id, 10));
|
||||
USING (internal.user_has_website_access(id, 10, raise_error => false));
|
||||
|
||||
CREATE POLICY update_website ON internal.website
|
||||
FOR UPDATE
|
||||
@@ -103,15 +132,7 @@ USING (internal.user_has_website_access(website_id, 20));
|
||||
|
||||
CREATE POLICY delete_article ON internal.article
|
||||
FOR DELETE
|
||||
USING (
|
||||
internal.user_has_website_access(website_id, 30)
|
||||
OR
|
||||
(
|
||||
internal.user_has_website_access(website_id, 20)
|
||||
AND
|
||||
user_id = (current_setting('request.jwt.claims', true)::json->>'user_id')::UUID
|
||||
)
|
||||
);
|
||||
USING (internal.user_has_website_access(website_id, 20, article_user_id => user_id));
|
||||
|
||||
CREATE POLICY insert_article ON internal.article
|
||||
FOR INSERT
|
||||
@@ -133,48 +154,15 @@ USING (internal.user_has_website_access(website_id, 10));
|
||||
|
||||
CREATE POLICY insert_collaborations ON internal.collab
|
||||
FOR INSERT
|
||||
WITH CHECK (
|
||||
CASE
|
||||
WHEN internal.user_has_website_access(website_id, 40) THEN
|
||||
true
|
||||
WHEN internal.user_has_website_access(website_id, 30) THEN
|
||||
(user_id != (current_setting('request.jwt.claims', true)::json->>'user_id')::UUID)
|
||||
AND
|
||||
(permission_level < 30)
|
||||
ELSE
|
||||
false
|
||||
END
|
||||
);
|
||||
WITH CHECK (internal.user_has_website_access(website_id, 30, collaborator_permission_level => permission_level, collaborator_user_id => user_id));
|
||||
|
||||
CREATE POLICY update_collaborations ON internal.collab
|
||||
FOR UPDATE
|
||||
USING (
|
||||
CASE
|
||||
WHEN internal.user_has_website_access(website_id, 40) THEN
|
||||
true
|
||||
WHEN internal.user_has_website_access(website_id, 30) THEN
|
||||
(user_id != (current_setting('request.jwt.claims', true)::json->>'user_id')::UUID)
|
||||
AND
|
||||
(permission_level < 30)
|
||||
ELSE
|
||||
false
|
||||
END
|
||||
);
|
||||
USING (internal.user_has_website_access(website_id, 30, collaborator_permission_level => permission_level, collaborator_user_id => user_id));
|
||||
|
||||
CREATE POLICY delete_collaborations ON internal.collab
|
||||
FOR DELETE
|
||||
USING (
|
||||
CASE
|
||||
WHEN internal.user_has_website_access(website_id, 40) THEN
|
||||
TRUE
|
||||
WHEN internal.user_has_website_access(website_id, 30) THEN
|
||||
(user_id != (current_setting('request.jwt.claims', true)::json->>'user_id')::UUID)
|
||||
AND
|
||||
(permission_level < 30)
|
||||
ELSE
|
||||
FALSE
|
||||
END
|
||||
);
|
||||
USING (internal.user_has_website_access(website_id, 30, collaborator_permission_level => permission_level, collaborator_user_id => user_id));
|
||||
|
||||
|
||||
-- migrate:down
|
||||
@@ -200,7 +188,7 @@ DROP POLICY view_collaborations ON internal.collab;
|
||||
DROP POLICY insert_collaborations ON internal.collab;
|
||||
DROP POLICY update_collaborations ON internal.collab;
|
||||
DROP POLICY delete_collaborations ON internal.collab;
|
||||
DROP FUNCTION internal.user_has_website_access(UUID, INTEGER);
|
||||
DROP FUNCTION internal.user_has_website_access(UUID, INTEGER, INTEGER, UUID, UUID, BOOLEAN);
|
||||
|
||||
ALTER TABLE internal.user DISABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE internal.website DISABLE ROW LEVEL SECURITY;
|
||||
|
||||
@@ -55,28 +55,35 @@ const generateStaticFiles = async (websiteData: any, isPreview: boolean = true)
|
||||
: `<img src="https://picsum.photos/32/32" />`
|
||||
)
|
||||
.replace("{{title}}", `<h1>${websiteData.title}</h1>`)
|
||||
.replace("{{main_content}}", md.render(websiteData.main_content || ""))
|
||||
.replace("{{main_content}}", md.render(websiteData.main_content ?? ""))
|
||||
.replace(
|
||||
"{{articles}}",
|
||||
websiteData.articles
|
||||
.map((article: { title: string; publication_date: string; meta_description: string }) => {
|
||||
const articleFileName = article.title.toLowerCase().split(" ").join("-");
|
||||
Array.isArray(websiteData.articles) && websiteData.articles.length > 0
|
||||
? `
|
||||
<h2>Articles</h2>
|
||||
${websiteData.articles
|
||||
.map(
|
||||
(article: { title: string; publication_date: string; meta_description: string }) => {
|
||||
const articleFileName = article.title.toLowerCase().split(" ").join("-");
|
||||
|
||||
return `
|
||||
<article>
|
||||
<p>${article.publication_date}</p>
|
||||
<h3>
|
||||
<a href="./articles/${articleFileName}.html">
|
||||
${article.title}
|
||||
</a>
|
||||
</h3>
|
||||
<p>${article.meta_description}</p>
|
||||
</article>
|
||||
`;
|
||||
})
|
||||
.join("")
|
||||
return `
|
||||
<article>
|
||||
<p>${article.publication_date}</p>
|
||||
<h3>
|
||||
<a href="./articles/${articleFileName}.html">
|
||||
${article.title}
|
||||
</a>
|
||||
</h3>
|
||||
<p>${article.meta_description ?? "No description provided"}</p>
|
||||
</article>
|
||||
`;
|
||||
}
|
||||
)
|
||||
.join("")}
|
||||
`
|
||||
: "<h2>Articles</h2><p>No articles available at this time.</p>"
|
||||
)
|
||||
.replace("{{additional_text}}", md.render(websiteData.additional_text || ""));
|
||||
.replace("{{additional_text}}", md.render(websiteData.additional_text ?? ""));
|
||||
|
||||
let uploadDir = "";
|
||||
|
||||
@@ -92,7 +99,7 @@ const generateStaticFiles = async (websiteData: any, isPreview: boolean = true)
|
||||
|
||||
await mkdir(join(uploadDir, "articles"), { recursive: true });
|
||||
|
||||
for (const article of websiteData.articles) {
|
||||
for (const article of websiteData.articles ?? []) {
|
||||
const articleFileName = article.title.toLowerCase().split(" ").join("-");
|
||||
|
||||
const articleFileContents = articleFile
|
||||
@@ -105,8 +112,8 @@ const generateStaticFiles = async (websiteData: any, isPreview: boolean = true)
|
||||
.replace("{{cover_image}}", `<img src="https://picsum.photos/600/200" />`)
|
||||
.replace("{{title}}", `<h1>${article.title}</h1>`)
|
||||
.replace("{{publication_date}}", `<p>${article.publication_date}</p>`)
|
||||
.replace("{{main_content}}", md.render(article.main_content || ""))
|
||||
.replace("{{additional_text}}", md.render(websiteData.additional_text || ""));
|
||||
.replace("{{main_content}}", md.render(article.main_content ?? ""))
|
||||
.replace("{{additional_text}}", md.render(websiteData.additional_text ?? ""));
|
||||
|
||||
await writeFile(join(uploadDir, "articles", `${articleFileName}.html`), articleFileContents);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user