Remove NGINX website directories from API and fix some minor issues

This commit is contained in:
thiloho
2024-10-17 16:53:31 +02:00
parent 1b74e1e6fb
commit 185aeea4e5
20 changed files with 166 additions and 126 deletions

View File

@@ -15,6 +15,10 @@
acmeEmail = "thilo.hohlt@tutanota.com"; acmeEmail = "thilo.hohlt@tutanota.com";
dnsProvider = "porkbun"; dnsProvider = "porkbun";
dnsEnvironmentFile = /var/lib/porkbun.env; dnsEnvironmentFile = /var/lib/porkbun.env;
settings = {
disableRegistration = true; disableRegistration = true;
maxWebsiteStorageSize = 250;
maxUserWebsites = 3;
};
}; };
} }

View File

@@ -52,6 +52,15 @@
postgresql = { postgresql = {
enable = true; enable = true;
package = pkgs.postgresql_16; package = pkgs.postgresql_16;
/*
PL/Perl:
overrideAttrs (
finalAttrs: previousAttrs: {
buildInputs = previousAttrs.buildInputs ++ [ pkgs.perl ];
configureFlags = previousAttrs.configureFlags ++ [ "--with-perl" ];
}
);
*/
ensureDatabases = [ "archtika" ]; ensureDatabases = [ "archtika" ];
authentication = lib.mkForce '' authentication = lib.mkForce ''
local all all trust local all all trust

View File

@@ -76,11 +76,27 @@ in
description = "API secrets for the DNS-01 challenge (required for wildcard domains)."; description = "API secrets for the DNS-01 challenge (required for wildcard domains).";
}; };
settings = mkOption {
type = types.submodule {
options = {
disableRegistration = mkOption { disableRegistration = mkOption {
type = types.bool; type = types.bool;
default = false; default = false;
description = "By default any user can create an account. That behavior can be disabled by using this option."; description = "By default any user can create an account. That behavior can be disabled by using this option.";
}; };
maxUserWebsites = mkOption {
type = types.int;
default = 2;
description = "Maximum number of websites allowed per user by default.";
};
maxWebsiteStorageSize = mkOption {
type = types.int;
default = 500;
description = "Maximum amount of disk space in MB allowed per user website by default.";
};
};
};
};
}; };
config = mkIf cfg.enable { config = mkIf cfg.enable {
@@ -91,7 +107,7 @@ in
users.groups.${cfg.group} = { }; users.groups.${cfg.group} = { };
systemd.tmpfiles.rules = [ "d /var/www/archtika-websites 0755 ${cfg.user} ${cfg.group} -" ]; systemd.tmpfiles.rules = [ "d /var/www/archtika-websites 0777 ${cfg.user} ${cfg.group} -" ];
systemd.services.archtika-api = { systemd.services.archtika-api = {
description = "archtika API service"; description = "archtika API service";
@@ -112,6 +128,8 @@ in
JWT_SECRET=$(tr -dc 'A-Za-z0-9' < /dev/urandom | head -c64) JWT_SECRET=$(tr -dc 'A-Za-z0-9' < /dev/urandom | head -c64)
${pkgs.postgresql_16}/bin/psql postgres://postgres@localhost:5432/${cfg.databaseName} -c "ALTER DATABASE ${cfg.databaseName} SET \"app.jwt_secret\" TO '$JWT_SECRET'" ${pkgs.postgresql_16}/bin/psql postgres://postgres@localhost:5432/${cfg.databaseName} -c "ALTER DATABASE ${cfg.databaseName} SET \"app.jwt_secret\" TO '$JWT_SECRET'"
${pkgs.postgresql_16}/bin/psql postgres://postgres@localhost:5432/${cfg.databaseName} -c "ALTER DATABASE ${cfg.databaseName} SET \"app.website_max_storage_size\" TO ${toString cfg.settings.maxWebsiteStorageSize}"
${pkgs.postgresql_16}/bin/psql postgres://postgres@localhost:5432/${cfg.databaseName} -c "ALTER DATABASE ${cfg.databaseName} SET \"app.website_max_number_user\" TO ${toString cfg.settings.maxUserWebsites}"
${pkgs.dbmate}/bin/dbmate --url postgres://postgres@localhost:5432/archtika?sslmode=disable --migrations-dir ${cfg.package}/rest-api/db/migrations up ${pkgs.dbmate}/bin/dbmate --url postgres://postgres@localhost:5432/archtika?sslmode=disable --migrations-dir ${cfg.package}/rest-api/db/migrations up
@@ -132,7 +150,7 @@ in
}; };
script = '' script = ''
REGISTRATION_IS_DISABLED=${toString cfg.disableRegistration} BODY_SIZE_LIMIT=10M ORIGIN=https://${cfg.domain} PORT=${toString cfg.webAppPort} ${pkgs.nodejs_22}/bin/node ${cfg.package}/web-app REGISTRATION_IS_DISABLED=${toString cfg.settings.disableRegistration} BODY_SIZE_LIMIT=10M ORIGIN=https://${cfg.domain} PORT=${toString cfg.webAppPort} ${pkgs.nodejs_22}/bin/node ${cfg.package}/web-app
''; '';
}; };
@@ -189,7 +207,7 @@ in
default_type application/json; default_type application/json;
''; '';
}; };
"/api/rpc/register" = mkIf cfg.disableRegistration { "/api/rpc/register" = mkIf cfg.settings.disableRegistration {
extraConfig = '' extraConfig = ''
deny all; deny all;
''; '';

View File

@@ -29,7 +29,7 @@ ALTER DEFAULT PRIVILEGES REVOKE EXECUTE ON FUNCTIONS FROM PUBLIC;
CREATE TABLE internal.user ( CREATE TABLE internal.user (
id UUID PRIMARY KEY DEFAULT gen_random_uuid (), id UUID PRIMARY KEY DEFAULT gen_random_uuid (),
username VARCHAR(16) UNIQUE NOT NULL CHECK (LENGTH(username) >= 3), username VARCHAR(16) UNIQUE NOT NULL CHECK (LENGTH(username) >= 3 AND username ~ '^[a-zA-Z0-9_-]+$'),
password_hash CHAR(60) NOT NULL, password_hash CHAR(60) NOT NULL,
user_role NAME NOT NULL DEFAULT 'authenticated_user', user_role NAME NOT NULL DEFAULT 'authenticated_user',
max_number_websites INT NOT NULL DEFAULT CURRENT_SETTING('app.website_max_number_user') ::INT, max_number_websites INT NOT NULL DEFAULT CURRENT_SETTING('app.website_max_number_user') ::INT,

View File

@@ -26,19 +26,7 @@ CREATE VIEW api.website WITH ( security_invoker = ON
SELECT SELECT
* *
FROM FROM
internal.website AS w internal.website;
WHERE
w.user_id = (
CURRENT_SETTING(
'request.jwt.claims', TRUE
)::JSON ->> 'user_id')::UUID
OR w.id IN (
SELECT
c.website_id
FROM
internal.collab AS c
WHERE
c.user_id = (CURRENT_SETTING('request.jwt.claims', TRUE)::JSON ->> 'user_id')::UUID);
CREATE VIEW api.settings WITH ( security_invoker = ON CREATE VIEW api.settings WITH ( security_invoker = ON
) AS ) AS

View File

@@ -146,22 +146,6 @@ CREATE TRIGGER _prevent_storage_excess_settings
FOR EACH ROW FOR EACH ROW
EXECUTE FUNCTION internal.prevent_website_storage_size_excess (); EXECUTE FUNCTION internal.prevent_website_storage_size_excess ();
CREATE VIEW api.all_user_websites AS
SELECT
u.id AS user_id,
u.username,
u.created_at AS user_created_at,
u.max_number_websites,
COALESCE(JSONB_AGG(JSONB_BUILD_OBJECT('id', w.id, 'title', w.title, 'max_storage_size', w.max_storage_size)
ORDER BY w.created_at DESC) FILTER (WHERE w.id IS NOT NULL), '[]'::JSONB) AS websites
FROM
internal.user AS u
LEFT JOIN internal.website AS w ON u.id = w.user_id
GROUP BY
u.id;
GRANT SELECT ON api.all_user_websites TO administrator;
GRANT UPDATE (max_storage_size) ON internal.website TO administrator; GRANT UPDATE (max_storage_size) ON internal.website TO administrator;
GRANT UPDATE, DELETE ON internal.user TO administrator; GRANT UPDATE, DELETE ON internal.user TO administrator;
@@ -193,8 +177,6 @@ DROP TRIGGER _prevent_storage_excess_settings ON internal.settings;
DROP FUNCTION internal.prevent_website_storage_size_excess; DROP FUNCTION internal.prevent_website_storage_size_excess;
DROP VIEW api.all_user_websites;
REVOKE UPDATE (max_storage_size) ON internal.website FROM administrator; REVOKE UPDATE (max_storage_size) ON internal.website FROM administrator;
REVOKE UPDATE, DELETE ON internal.user FROM administrator; REVOKE UPDATE, DELETE ON internal.user FROM administrator;

View File

@@ -0,0 +1,52 @@
-- migrate:up
CREATE FUNCTION internal.cleanup_filesystem ()
RETURNS TRIGGER
AS $$
DECLARE
_website_id UUID;
_domain_prefix VARCHAR(16);
BEGIN
IF TG_TABLE_NAME = 'website' THEN
_website_id := OLD.id;
SELECT
d.prefix INTO _domain_prefix
FROM
internal.domain_prefix AS d
WHERE
d.website_id = _website_id;
EXECUTE FORMAT('COPY (SELECT '''') TO PROGRAM ''rm -rf /var/www/archtika-websites/previews/%s''', _website_id);
EXECUTE FORMAT('COPY (SELECT '''') TO PROGRAM ''rm -rf /var/www/archtika-websites/%s''', COALESCE(_domain_prefix, _website_id::VARCHAR));
ELSE
_website_id := OLD.website_id;
SELECT
d.prefix INTO _domain_prefix
FROM
internal.domain_prefix AS d
WHERE
d.website_id = _website_id;
EXECUTE FORMAT('COPY (SELECT '''') TO PROGRAM ''rm -rf /var/www/archtika-websites/previews/%s/legal-information.html''', _website_id);
EXECUTE FORMAT('COPY (SELECT '''') TO PROGRAM ''rm -rf /var/www/archtika-websites/%s/legal-information.html''', COALESCE(_domain_prefix, _website_id::VARCHAR));
END IF;
RETURN OLD;
END;
$$
LANGUAGE plpgsql
SECURITY DEFINER;
CREATE TRIGGER _cleanup_filesystem_website
BEFORE DELETE ON internal.website
FOR EACH ROW
EXECUTE FUNCTION internal.cleanup_filesystem ();
CREATE TRIGGER _cleanup_filesystem_legal_information
BEFORE DELETE ON internal.legal_information
FOR EACH ROW
EXECUTE FUNCTION internal.cleanup_filesystem ();
-- migrate:down
DROP TRIGGER _cleanup_filesystem_website ON internal.website;
DROP TRIGGER _cleanup_filesystem_legal_information ON internal.legal_information;
DROP FUNCTION internal.cleanup_filesystem;

View File

@@ -32,7 +32,7 @@
{websiteUrl} {websiteUrl}
/> />
<Nav {websiteOverview} isDocsTemplate={false} isIndexPage={true} {apiUrl} /> <Nav {websiteOverview} isDocsTemplate={false} isIndexPage={true} {isLegalPage} {apiUrl} />
<header> <header>
<div class="container"> <div class="container">

View File

@@ -6,12 +6,14 @@
websiteOverview, websiteOverview,
isDocsTemplate, isDocsTemplate,
isIndexPage, isIndexPage,
apiUrl apiUrl,
isLegalPage
}: { }: {
websiteOverview: WebsiteOverview; websiteOverview: WebsiteOverview;
isDocsTemplate: boolean; isDocsTemplate: boolean;
isIndexPage: boolean; isIndexPage: boolean;
apiUrl: string; apiUrl: string;
isLegalPage?: boolean;
} = $props(); } = $props();
const categorizedArticles = Object.fromEntries( const categorizedArticles = Object.fromEntries(
@@ -70,7 +72,10 @@
</ul> </ul>
</section> </section>
{/if} {/if}
<svelte:element this={isIndexPage ? "span" : "a"} href=".."> <svelte:element
this={isIndexPage && !isLegalPage ? "span" : "a"}
href={`${isLegalPage ? "./" : "../"}`}
>
{#if websiteOverview.header.logo_type === "text"} {#if websiteOverview.header.logo_type === "text"}
<strong>{websiteOverview.header.logo_text}</strong> <strong>{websiteOverview.header.logo_text}</strong>
{:else} {:else}

View File

@@ -26,7 +26,7 @@
{websiteUrl} {websiteUrl}
/> />
<Nav {websiteOverview} isDocsTemplate={true} isIndexPage={true} {apiUrl} /> <Nav {websiteOverview} isDocsTemplate={true} isIndexPage={true} {isLegalPage} {apiUrl} />
<header> <header>
<div class="container"> <div class="container">

View File

@@ -38,7 +38,14 @@
<form method="POST" use:enhance={enhanceForm()}> <form method="POST" use:enhance={enhanceForm()}>
<label> <label>
Username: Username:
<input type="text" name="username" minlength="3" maxlength="16" required /> <input
type="text"
name="username"
minlength="3"
maxlength="16"
pattern="^[a-zA-Z0-9_\-]+$"
required
/>
</label> </label>
<label> <label>
Password: Password:

View File

@@ -1,8 +1,6 @@
import type { Actions, PageServerLoad } from "./$types"; import type { Actions, PageServerLoad } from "./$types";
import { apiRequest } from "$lib/server/utils"; import { apiRequest } from "$lib/server/utils";
import { API_BASE_PREFIX } from "$lib/server/utils"; import { API_BASE_PREFIX } from "$lib/server/utils";
import { rm } from "node:fs/promises";
import { join } from "node:path";
import type { Website } from "$lib/db-schema"; import type { Website } from "$lib/db-schema";
export const load: PageServerLoad = async ({ fetch, url, locals }) => { export const load: PageServerLoad = async ({ fetch, url, locals }) => {
@@ -11,7 +9,7 @@ export const load: PageServerLoad = async ({ fetch, url, locals }) => {
const params = new URLSearchParams(); const params = new URLSearchParams();
const baseFetchUrl = `${API_BASE_PREFIX}/website?order=last_modified_at.desc,created_at.desc`; const baseFetchUrl = `${API_BASE_PREFIX}/website?select=*,collab(user_id)&collab.user_id=eq.${locals.user.id}&or=(user_id.eq.${locals.user.id},collab.not.is.null)&order=last_modified_at.desc,created_at.desc`;
if (searchQuery) { if (searchQuery) {
params.append("title", `wfts.${searchQuery}`); params.append("title", `wfts.${searchQuery}`);
@@ -77,15 +75,6 @@ export const actions: Actions = {
const data = await request.formData(); const data = await request.formData();
const id = data.get("id"); const id = data.get("id");
const oldDomainPrefix = (
await apiRequest(fetch, `${API_BASE_PREFIX}/domain_prefix?website_id=eq.${id}`, "GET", {
headers: {
Accept: "application/vnd.pgrst.object+json"
},
returnData: true
})
).data;
const deleteWebsite = await apiRequest( const deleteWebsite = await apiRequest(
fetch, fetch,
`${API_BASE_PREFIX}/website?id=eq.${id}`, `${API_BASE_PREFIX}/website?id=eq.${id}`,
@@ -99,16 +88,6 @@ export const actions: Actions = {
return deleteWebsite; return deleteWebsite;
} }
await rm(join("/", "var", "www", "archtika-websites", "previews", id as string), {
recursive: true,
force: true
});
await rm(join("/", "var", "www", "archtika-websites", oldDomainPrefix?.prefix ?? id), {
recursive: true,
force: true
});
return deleteWebsite; return deleteWebsite;
} }
}; };

View File

@@ -21,7 +21,7 @@ export const actions: Actions = {
logout: async ({ cookies }) => { logout: async ({ cookies }) => {
cookies.delete("session_token", { path: "/" }); cookies.delete("session_token", { path: "/" });
return { success: true, message: "Successfully logged out" }; return { success: true, message: "Successfully logged out, you can refresh the page" };
}, },
deleteAccount: async ({ request, fetch, cookies }) => { deleteAccount: async ({ request, fetch, cookies }) => {
const data = await request.formData(); const data = await request.formData();

View File

@@ -33,7 +33,7 @@
</ul> </ul>
</section> </section>
{#if data.storageSizes.data.length > 0} {#if (data.storageSizes.data ?? []).length > 0}
<section id="storage"> <section id="storage">
<h2> <h2>
<a href="#storage">Storage</a> <a href="#storage">Storage</a>

View File

@@ -1,12 +1,13 @@
import type { Actions, PageServerLoad } from "./$types"; import type { Actions, PageServerLoad } from "./$types";
import { API_BASE_PREFIX } from "$lib/server/utils"; import { API_BASE_PREFIX } from "$lib/server/utils";
import { apiRequest } from "$lib/server/utils"; import { apiRequest } from "$lib/server/utils";
import type { Website, User } from "$lib/db-schema";
export const load: PageServerLoad = async ({ fetch }) => { export const load: PageServerLoad = async ({ fetch }) => {
const allUsers = ( const usersWithWebsites: (User & { website: Website[] })[] = (
await apiRequest( await apiRequest(
fetch, fetch,
`${API_BASE_PREFIX}/all_user_websites?order=user_created_at.desc`, `${API_BASE_PREFIX}/user?select=*,website!user_id(*)&order=created_at`,
"GET", "GET",
{ {
returnData: true returnData: true
@@ -15,7 +16,7 @@ export const load: PageServerLoad = async ({ fetch }) => {
).data; ).data;
return { return {
allUsers, usersWithWebsites,
API_BASE_PREFIX API_BASE_PREFIX
}; };
}; };
@@ -39,8 +40,6 @@ export const actions: Actions = {
updateStorageLimit: async ({ request, fetch }) => { updateStorageLimit: async ({ request, fetch }) => {
const data = await request.formData(); const data = await request.formData();
console.log(`${API_BASE_PREFIX}/website?id=eq.${data.get("website-id")}`);
return await apiRequest( return await apiRequest(
fetch, fetch,
`${API_BASE_PREFIX}/website?id=eq.${data.get("website-id")}`, `${API_BASE_PREFIX}/website?id=eq.${data.get("website-id")}`,

View File

@@ -32,15 +32,15 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{#each data.allUsers as { user_id, user_created_at, username, max_number_websites, websites }} {#each data.usersWithWebsites as { id, created_at, username, max_number_websites, website }}
<tr> <tr>
<td> <td>
<DateTime date={user_created_at} /> <DateTime date={created_at} />
</td> </td>
<td>{user_id}</td> <td>{id}</td>
<td>{username}</td> <td>{username}</td>
<td> <td>
<Modal id="manage-user-{user_id}" text="Manage"> <Modal id="manage-user-{id}" text="Manage">
<hgroup> <hgroup>
<h3>Manage user</h3> <h3>Manage user</h3>
<p>User "{username}"</p> <p>User "{username}"</p>
@@ -51,7 +51,7 @@
action="?/updateMaxWebsiteAmount" action="?/updateMaxWebsiteAmount"
use:enhance={enhanceForm({ reset: false })} use:enhance={enhanceForm({ reset: false })}
> >
<input type="hidden" name="user-id" value={user_id} /> <input type="hidden" name="user-id" value={id} />
<label> <label>
Number of websites allowed: Number of websites allowed:
<input <input
@@ -64,9 +64,9 @@
<button type="submit">Submit</button> <button type="submit">Submit</button>
</form> </form>
{#if websites.length > 0} {#if website.length > 0}
<h4>Websites</h4> <h4>Websites</h4>
{#each websites as { id, title, max_storage_size }} {#each website as { id, title, max_storage_size }}
<details> <details>
<summary>{title}</summary> <summary>{title}</summary>
<div> <div>
@@ -105,7 +105,7 @@
action="?/deleteUser" action="?/deleteUser"
use:enhance={enhanceForm({ closeModal: true })} use:enhance={enhanceForm({ closeModal: true })}
> >
<input type="hidden" name="user-id" value={user_id} /> <input type="hidden" name="user-id" value={id} />
<button type="submit">Delete user</button> <button type="submit">Delete user</button>
</form> </form>
</div> </div>

View File

@@ -4,37 +4,24 @@ import { apiRequest } from "$lib/server/utils";
import type { Settings, Header, Footer } from "$lib/db-schema"; import type { Settings, Header, Footer } from "$lib/db-schema";
export const load: PageServerLoad = async ({ params, fetch }) => { export const load: PageServerLoad = async ({ params, fetch }) => {
const globalSettings: Settings = ( const [globalSettingsResponse, headerResponse, footerResponse] = await Promise.all([
await apiRequest( apiRequest(fetch, `${API_BASE_PREFIX}/settings?website_id=eq.${params.websiteId}`, "GET", {
fetch, headers: { Accept: "application/vnd.pgrst.object+json" },
`${API_BASE_PREFIX}/settings?website_id=eq.${params.websiteId}`,
"GET",
{
headers: {
Accept: "application/vnd.pgrst.object+json"
},
returnData: true returnData: true
} }),
) apiRequest(fetch, `${API_BASE_PREFIX}/header?website_id=eq.${params.websiteId}`, "GET", {
).data; headers: { Accept: "application/vnd.pgrst.object+json" },
returnData: true
const header: Header = ( }),
await apiRequest(fetch, `${API_BASE_PREFIX}/header?website_id=eq.${params.websiteId}`, "GET", { apiRequest(fetch, `${API_BASE_PREFIX}/footer?website_id=eq.${params.websiteId}`, "GET", {
headers: { headers: { Accept: "application/vnd.pgrst.object+json" },
Accept: "application/vnd.pgrst.object+json"
},
returnData: true returnData: true
}) })
).data; ]);
const footer: Footer = ( const globalSettings: Settings = globalSettingsResponse.data;
await apiRequest(fetch, `${API_BASE_PREFIX}/footer?website_id=eq.${params.websiteId}`, "GET", { const header: Header = headerResponse.data;
headers: { const footer: Footer = footerResponse.data;
Accept: "application/vnd.pgrst.object+json"
},
returnData: true
})
).data;
return { return {
globalSettings, globalSettings,

View File

@@ -44,6 +44,7 @@
<input type="number" name="article-weight" value={data.article.article_weight} min="0" /> <input type="number" name="article-weight" value={data.article.article_weight} min="0" />
</label> </label>
{#if data.categories.length > 0}
<label> <label>
Category: Category:
<select name="category"> <select name="category">
@@ -53,6 +54,7 @@
</select> </select>
</label> </label>
{/if} {/if}
{/if}
<label> <label>
Title: Title:

View File

@@ -1,7 +1,5 @@
import type { Actions, PageServerLoad } from "./$types"; import type { Actions, PageServerLoad } from "./$types";
import { API_BASE_PREFIX, apiRequest } from "$lib/server/utils"; import { API_BASE_PREFIX, apiRequest } from "$lib/server/utils";
import { rm } from "node:fs/promises";
import { join } from "node:path";
import type { LegalInformation } from "$lib/db-schema"; import type { LegalInformation } from "$lib/db-schema";
export const load: PageServerLoad = async ({ parent, fetch, params }) => { export const load: PageServerLoad = async ({ parent, fetch, params }) => {
@@ -58,11 +56,6 @@ export const actions: Actions = {
return deleteLegalInformation; return deleteLegalInformation;
} }
await rm(
join("/", "var", "www", "archtika-websites", params.websiteId, "legal-information.html"),
{ force: true }
);
return deleteLegalInformation; return deleteLegalInformation;
} }
}; };

View File

@@ -5,7 +5,7 @@ import BlogIndex from "$lib/templates/blog/BlogIndex.svelte";
import DocsArticle from "$lib/templates/docs/DocsArticle.svelte"; import DocsArticle from "$lib/templates/docs/DocsArticle.svelte";
import DocsIndex from "$lib/templates/docs/DocsIndex.svelte"; import DocsIndex from "$lib/templates/docs/DocsIndex.svelte";
import { type WebsiteOverview, hexToHSL, slugify } from "$lib/utils"; import { type WebsiteOverview, hexToHSL, slugify } from "$lib/utils";
import { mkdir, readFile, rename, writeFile } from "node:fs/promises"; import { mkdir, readFile, rename, writeFile, chmod, readdir } from "node:fs/promises";
import { join } from "node:path"; import { join } from "node:path";
import { render } from "svelte/server"; import { render } from "svelte/server";
import type { Actions, PageServerLoad } from "./$types"; import type { Actions, PageServerLoad } from "./$types";
@@ -290,5 +290,20 @@ const generateStaticFiles = async (websiteData: WebsiteOverview, isPreview = tru
await writeFile(join(uploadDir, "common.css"), commonStyles); await writeFile(join(uploadDir, "common.css"), commonStyles);
await writeFile(join(uploadDir, "scoped.css"), specificStyles); await writeFile(join(uploadDir, "scoped.css"), specificStyles);
await setPermissions(isPreview ? join(uploadDir, "../") : uploadDir);
return { websitePreviewUrl, websiteProdUrl }; return { websitePreviewUrl, websiteProdUrl };
}; };
const setPermissions = async (dir: string) => {
await chmod(dir, 0o777);
const entries = await readdir(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = join(dir, entry.name);
if (entry.isDirectory()) {
await setPermissions(fullPath);
} else {
await chmod(fullPath, 0o777);
}
}
};