Add custom domain prefixes and option to disable user registration

This commit is contained in:
thiloho
2024-09-20 15:56:07 +02:00
parent 4300988463
commit 86ab737429
12 changed files with 368 additions and 57 deletions

View File

@@ -5,7 +5,7 @@
* 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
* $ pg-to-ts generate -c postgres://username:password@localhost:15432/archtika -t article -t change_log -t collab -t docs_category -t domain_prefix -t footer -t header -t home -t legal_information -t media -t settings -t user -t website -s internal
*
*/
@@ -169,6 +169,7 @@ export interface DocsCategory {
user_id: string | null;
category_name: string;
category_weight: number;
created_at: Date;
last_modified_at: Date;
last_modified_by: string | null;
}
@@ -178,6 +179,7 @@ export interface DocsCategoryInput {
user_id?: string | null;
category_name: string;
category_weight: number;
created_at?: Date;
last_modified_at?: Date;
last_modified_by?: string | null;
}
@@ -189,6 +191,7 @@ const docs_category = {
"user_id",
"category_name",
"category_weight",
"created_at",
"last_modified_at",
"last_modified_by"
],
@@ -203,6 +206,34 @@ const docs_category = {
$input: null as unknown as DocsCategoryInput
} as const;
// Table domain_prefix
export interface DomainPrefix {
website_id: string;
prefix: string;
created_at: Date;
last_modified_at: Date;
last_modified_by: string | null;
}
export interface DomainPrefixInput {
website_id: string;
prefix: string;
created_at?: Date;
last_modified_at?: Date;
last_modified_by?: string | null;
}
const domain_prefix = {
tableName: "domain_prefix",
columns: ["website_id", "prefix", "created_at", "last_modified_at", "last_modified_by"],
requiredForInsert: ["website_id", "prefix"],
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 DomainPrefix,
$input: null as unknown as DomainPrefixInput
} as const;
// Table footer
export interface Footer {
website_id: string;
@@ -297,18 +328,20 @@ const home = {
export interface LegalInformation {
website_id: string;
main_content: string;
created_at: Date;
last_modified_at: Date;
last_modified_by: string | null;
}
export interface LegalInformationInput {
website_id: string;
main_content: string;
created_at?: Date;
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"],
columns: ["website_id", "main_content", "created_at", "last_modified_at", "last_modified_by"],
requiredForInsert: ["website_id", "main_content"],
primaryKey: "website_id",
foreignKeys: {
@@ -395,16 +428,18 @@ export interface User {
username: string;
password_hash: string;
role: string;
created_at: Date;
}
export interface UserInput {
id?: string;
username: string;
password_hash: string;
role?: string;
created_at?: Date;
}
const user = {
tableName: "user",
columns: ["id", "username", "password_hash", "role"],
columns: ["id", "username", "password_hash", "role", "created_at"],
requiredForInsert: ["username", "password_hash"],
primaryKey: "id",
foreignKeys: {},
@@ -418,8 +453,8 @@ export interface Website {
user_id: string;
content_type: string;
title: string;
created_at: Date;
is_published: boolean;
created_at: Date;
last_modified_at: Date;
last_modified_by: string | null;
title_search: any | null;
@@ -429,8 +464,8 @@ export interface WebsiteInput {
user_id?: string;
content_type: string;
title: string;
created_at?: Date;
is_published?: boolean;
created_at?: Date;
last_modified_at?: Date;
last_modified_by?: string | null;
title_search?: any | null;
@@ -442,8 +477,8 @@ const website = {
"user_id",
"content_type",
"title",
"created_at",
"is_published",
"created_at",
"last_modified_at",
"last_modified_by",
"title_search"
@@ -475,6 +510,10 @@ export interface TableTypes {
select: DocsCategory;
input: DocsCategoryInput;
};
domain_prefix: {
select: DomainPrefix;
input: DomainPrefixInput;
};
footer: {
select: Footer;
input: FooterInput;
@@ -514,6 +553,7 @@ export const tables = {
change_log,
collab,
docs_category,
domain_prefix,
footer,
header,
home,

View File

@@ -3,3 +3,9 @@ import { dev } from "$app/environment";
export const API_BASE_PREFIX = dev
? "http://localhost:3000"
: `${process.env.ORIGIN ? `${process.env.ORIGIN}/api` : "http://localhost:3000"}`;
export const REGISTRATION_IS_DISABLED = dev
? false
: process.env.REGISTRATION_IS_DISABLED
? JSON.parse(process.env.REGISTRATION_IS_DISABLED)
: false;

View File

@@ -12,7 +12,8 @@ import type {
Footer,
Article,
DocsCategory,
LegalInformation
LegalInformation,
DomainPrefix
} from "$lib/db-schema";
export const ALLOWED_MIME_TYPES = [
@@ -198,4 +199,5 @@ export interface WebsiteOverview extends Website {
footer: Footer;
article: (Article & { docs_category: DocsCategory | null })[];
legal_information?: LegalInformation;
domain_prefix?: DomainPrefix;
}

View File

@@ -1,5 +1,11 @@
import type { Actions } from "./$types";
import { API_BASE_PREFIX } from "$lib/server/utils";
import type { Actions, PageServerLoad } from "./$types";
import { API_BASE_PREFIX, REGISTRATION_IS_DISABLED } from "$lib/server/utils";
export const load: PageServerLoad = async () => {
return {
REGISTRATION_IS_DISABLED
};
};
export const actions: Actions = {
default: async ({ request, fetch }) => {

View File

@@ -1,10 +1,10 @@
<script lang="ts">
import { enhance } from "$app/forms";
import SuccessOrError from "$lib/components/SuccessOrError.svelte";
import type { ActionData } from "./$types";
import type { ActionData, PageServerData } from "./$types";
import LoadingSpinner from "$lib/components/LoadingSpinner.svelte";
const { form }: { form: ActionData } = $props();
const { data, form }: { data: PageServerData; form: ActionData } = $props();
let sending = $state(false);
</script>
@@ -15,24 +15,52 @@
<LoadingSpinner />
{/if}
<form
method="POST"
use:enhance={() => {
sending = true;
return async ({ update }) => {
await update();
sending = false;
};
}}
>
<label>
Username:
<input type="text" name="username" minlength="3" maxlength="16" required />
</label>
<label>
Password:
<input type="password" name="password" minlength="12" maxlength="128" required />
</label>
{#if data.REGISTRATION_IS_DISABLED}
<p class="registration-disabled">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
width="20"
height="20"
color="var(--color-error)"
>
<path
fill-rule="evenodd"
d="M10 1a4.5 4.5 0 0 0-4.5 4.5V9H5a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2v-6a2 2 0 0 0-2-2h-.5V5.5A4.5 4.5 0 0 0 10 1Zm3 8V5.5a3 3 0 1 0-6 0V9h6Z"
clip-rule="evenodd"
></path>
</svg>
Account registration is disabled on this instance
</p>
{:else}
<form
method="POST"
use:enhance={() => {
sending = true;
return async ({ update }) => {
await update();
sending = false;
};
}}
>
<label>
Username:
<input type="text" name="username" minlength="3" maxlength="16" required />
</label>
<label>
Password:
<input type="password" name="password" minlength="12" maxlength="128" required />
</label>
<button type="submit">Submit</button>
</form>
<button type="submit">Submit</button>
</form>
{/if}
<style>
.registration-disabled {
display: flex;
gap: 0.5rem;
align-items: center;
}
</style>

View File

@@ -103,6 +103,19 @@ export const actions: Actions = {
deleteWebsite: async ({ request, cookies, fetch }) => {
const data = await request.formData();
const oldDomainPrefixData = await fetch(
`${API_BASE_PREFIX}/domain_prefix?website_id=eq.${data.get("id")}`,
{
method: "GET",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${cookies.get("session_token")}`,
Accept: "application/vnd.pgrst.object+json"
}
}
);
const oldDomainPrefix = await oldDomainPrefixData.json();
const res = await fetch(`${API_BASE_PREFIX}/website?id=eq.${data.get("id")}`, {
method: "DELETE",
headers: {
@@ -116,11 +129,19 @@ export const actions: Actions = {
return { success: false, message: response.message };
}
await rm(join("/", "var", "www", "archtika-websites", data.get("id") as string), {
await rm(join("/", "var", "www", "archtika-websites", "previews", data.get("id") as string), {
recursive: true,
force: true
});
await rm(
join("/", "var", "www", "archtika-websites", oldDomainPrefix.prefix ?? data.get("id")),
{
recursive: true,
force: true
}
);
return { success: true, message: "Successfully deleted website" };
}
};

View File

@@ -1,4 +1,4 @@
import { readFile, mkdir, writeFile } from "node:fs/promises";
import { readFile, mkdir, writeFile, rename } from "node:fs/promises";
import { join } from "node:path";
import { type WebsiteOverview, slugify } from "$lib/utils";
import type { Actions, PageServerLoad } from "./$types";
@@ -9,10 +9,11 @@ import BlogArticle from "$lib/templates/blog/BlogArticle.svelte";
import DocsIndex from "$lib/templates/docs/DocsIndex.svelte";
import DocsArticle from "$lib/templates/docs/DocsArticle.svelte";
import { dev } from "$app/environment";
import type { DomainPrefixInput } from "$lib/db-schema";
export const load: PageServerLoad = async ({ params, fetch, cookies }) => {
const websiteOverviewData = await fetch(
`${API_BASE_PREFIX}/website?id=eq.${params.websiteId}&select=*,settings(*),header(*),home(*),footer(*),article(*,docs_category(*)),legal_information(*)`,
`${API_BASE_PREFIX}/website?id=eq.${params.websiteId}&select=*,settings(*),header(*),home(*),footer(*),article(*,docs_category(*)),legal_information(*),domain_prefix(*)`,
{
method: "GET",
headers: {
@@ -36,10 +37,13 @@ export const load: PageServerLoad = async ({ params, fetch, cookies }) => {
}/previews/${websiteOverview.id}/`;
const websiteProdUrl = dev
? `http://localhost:18000/${websiteOverview.id}/`
? `http://localhost:18000/${websiteOverview.domain_prefix?.prefix ?? websiteOverview.id}/`
: process.env.ORIGIN
? process.env.ORIGIN.replace("//", `//${websiteOverview.id}.`)
: `http://localhost:18000/${websiteOverview.id}/`;
? process.env.ORIGIN.replace(
"//",
`//${websiteOverview.domain_prefix?.prefix ?? websiteOverview.id}.`
)
: `http://localhost:18000/${websiteOverview.domain_prefix?.prefix ?? websiteOverview.id}/`;
return {
websiteOverview,
@@ -51,7 +55,7 @@ export const load: PageServerLoad = async ({ params, fetch, cookies }) => {
export const actions: Actions = {
publishWebsite: async ({ fetch, params, cookies }) => {
const websiteOverviewData = await fetch(
`${API_BASE_PREFIX}/website?id=eq.${params.websiteId}&select=*,settings(*),header(*),home(*),footer(*),article(*,docs_category(*)),legal_information(*)`,
`${API_BASE_PREFIX}/website?id=eq.${params.websiteId}&select=*,settings(*),header(*),home(*),footer(*),article(*,docs_category(*)),legal_information(*),domain_prefix(*)`,
{
method: "GET",
headers: {
@@ -82,10 +86,85 @@ export const actions: Actions = {
}
return { success: true, message: "Successfully published website" };
},
createUpdateCustomDomainPrefix: async ({ request, fetch, params, cookies }) => {
const data = await request.formData();
const oldDomainPrefixData = await fetch(
`${API_BASE_PREFIX}/domain_prefix?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 oldDomainPrefix = await oldDomainPrefixData.json();
const res = await fetch(`${API_BASE_PREFIX}/domain_prefix`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${cookies.get("session_token")}`,
Prefer: "resolution=merge-duplicates",
Accept: "application/vnd.pgrst.object+json"
},
body: JSON.stringify({
website_id: params.websiteId,
prefix: data.get("domain-prefix") as string
} satisfies DomainPrefixInput)
});
if (!res.ok) {
const response = await res.json();
return { success: false, message: response.message };
}
await rename(
join(
"/",
"var",
"www",
"archtika-websites",
res.status === 201 ? params.websiteId : oldDomainPrefix.prefix
),
join("/", "var", "www", "archtika-websites", data.get("domain-prefix") as string)
);
return {
success: true,
message: `Successfully ${res.status === 201 ? "created" : "updated"} domain prefix`
};
},
deleteCustomDomainPrefix: async ({ fetch, params, cookies }) => {
const res = await fetch(`${API_BASE_PREFIX}/domain_prefix?website_id=eq.${params.websiteId}`, {
method: "DELETE",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${cookies.get("session_token")}`,
Prefer: "return=representation",
Accept: "application/vnd.pgrst.object+json"
}
});
const response = await res.json();
if (!res.ok) {
return { success: false, message: response.message };
}
await rename(
join("/", "var", "www", "archtika-websites", response.prefix),
join("/", "var", "www", "archtika-websites", params.websiteId)
);
return { success: true, message: `Successfully deleted domain prefix` };
}
};
const generateStaticFiles = async (websiteData: WebsiteOverview, isPreview: boolean = true) => {
const generateStaticFiles = async (websiteData: WebsiteOverview, isPreview = true) => {
const fileContents = (head: string, body: string) => {
return `
<!DOCTYPE html>
@@ -112,7 +191,13 @@ const generateStaticFiles = async (websiteData: WebsiteOverview, isPreview: bool
if (isPreview) {
uploadDir = join("/", "var", "www", "archtika-websites", "previews", websiteData.id);
} else {
uploadDir = join("/", "var", "www", "archtika-websites", websiteData.id);
uploadDir = join(
"/",
"var",
"www",
"archtika-websites",
websiteData.domain_prefix?.prefix ?? websiteData.id
);
}
await mkdir(uploadDir, { recursive: true });

View File

@@ -4,6 +4,7 @@
import SuccessOrError from "$lib/components/SuccessOrError.svelte";
import type { ActionData, PageServerData } from "./$types";
import LoadingSpinner from "$lib/components/LoadingSpinner.svelte";
import Modal from "$lib/components/Modal.svelte";
const { data, form }: { data: PageServerData; form: ActionData } = $props();
@@ -45,18 +46,73 @@
>
<button type="submit">Publish</button>
</form>
{#if data.websiteOverview.is_published}
<section id="publication-status">
<h3>
<a href="#publication-status">Publication status</a>
</h3>
<p>
Your website is published at:
<br />
<a href={data.websiteProdUrl}>{data.websiteProdUrl}</a>
</p>
</section>
{/if}
</section>
{#if data.websiteOverview.is_published}
<section id="publication-status">
<h2>
<a href="#publication-status">Publication status</a>
</h2>
<p>
Your website is published at:
<br />
<a href={data.websiteProdUrl}>{data.websiteProdUrl}</a>
</p>
</section>
<section id="custom-domain-prefix">
<h2>
<a href="#custom-domain-prefix">Custom domain prefix</a>
</h2>
<form
method="POST"
action="?/createUpdateCustomDomainPrefix"
use:enhance={() => {
sending = true;
return async ({ update }) => {
await update();
sending = false;
};
}}
>
<label>
Prefix:
<input
type="text"
name="domain-prefix"
value={data.websiteOverview.domain_prefix?.prefix ?? ""}
placeholder="my-blog"
minlength="3"
maxlength="16"
pattern="^[a-z]+(-[a-z]+)*$"
required
/>
</label>
<button type="submit">Submit</button>
</form>
{#if data.websiteOverview.domain_prefix?.prefix}
<Modal id="delete-domain-prefix" text="Delete">
<form
action="?/deleteCustomDomainPrefix"
method="post"
use:enhance={() => {
sending = true;
return async ({ update }) => {
await update();
window.location.hash = "!";
sending = false;
};
}}
>
<h3>Delete domain prefix</h3>
<p>
<strong>Caution!</strong>
This action will remove the domain prefix and reset it to its initial value.
</p>
<button type="submit">Delete domain prefix</button>
</form>
</Modal>
{/if}
</section>
{/if}
</WebsiteEditor>