From 86ab7374296e8a2847831acfc3ec7da37328829e Mon Sep 17 00:00:00 2001
From: thiloho <123883702+thiloho@users.noreply.github.com>
Date: Fri, 20 Sep 2024 15:56:07 +0200
Subject: [PATCH] Add custom domain prefixes and option to disable user
registration
---
nix/deploy/qs/default.nix | 1 +
nix/module.nix | 15 ++-
.../20240805132306_last_modified_triggers.sql | 2 +-
.../20240920090915_custom_domain_prefix.sql | 57 ++++++++++
web-app/src/lib/db-schema.ts | 52 +++++++--
web-app/src/lib/server/utils.ts | 6 ++
web-app/src/lib/utils.ts | 4 +-
.../(anonymous)/register/+page.server.ts | 10 +-
.../routes/(anonymous)/register/+page.svelte | 72 +++++++++----
.../routes/(authenticated)/+page.server.ts | 23 +++-
.../[websiteId]/publish/+page.server.ts | 101 ++++++++++++++++--
.../website/[websiteId]/publish/+page.svelte | 82 +++++++++++---
12 files changed, 368 insertions(+), 57 deletions(-)
create mode 100644 rest-api/db/migrations/20240920090915_custom_domain_prefix.sql
diff --git a/nix/deploy/qs/default.nix b/nix/deploy/qs/default.nix
index dabd468..22e14dc 100644
--- a/nix/deploy/qs/default.nix
+++ b/nix/deploy/qs/default.nix
@@ -15,5 +15,6 @@
acmeEmail = "thilo.hohlt@tutanota.com";
dnsProvider = "porkbun";
dnsEnvironmentFile = /var/lib/porkbun.env;
+ disableRegistration = true;
};
}
diff --git a/nix/module.nix b/nix/module.nix
index d31d2e5..52fc37c 100644
--- a/nix/module.nix
+++ b/nix/module.nix
@@ -75,6 +75,12 @@ in
default = null;
description = "API secrets for the DNS-01 challenge (required for wildcard domains).";
};
+
+ disableRegistration = mkOption {
+ type = types.bool;
+ default = false;
+ description = "By default any user can create an account. That behavior can be disabled by using this option.";
+ };
};
config = mkIf cfg.enable {
@@ -125,7 +131,7 @@ in
};
script = ''
- BODY_SIZE_LIMIT=Infinity ORIGIN=https://${cfg.domain} PORT=${toString cfg.webAppPort} ${pkgs.nodejs_22}/bin/node ${cfg.package}/web-app
+ REGISTRATION_IS_DISABLED=${toString cfg.disableRegistration} BODY_SIZE_LIMIT=Infinity ORIGIN=https://${cfg.domain} PORT=${toString cfg.webAppPort} ${pkgs.nodejs_22}/bin/node ${cfg.package}/web-app
'';
};
@@ -165,10 +171,13 @@ in
"/api/" = {
proxyPass = "http://localhost:${toString cfg.apiPort}/";
extraConfig = ''
- default_type application/json;
+ default_type application/json;
proxy_set_header Connection "";
proxy_http_version 1.1;
- allow 127.0.0.1;
+ '';
+ };
+ "/api/rpc/register" = mkIf cfg.disableRegistration {
+ extraConfig = ''
deny all;
'';
};
diff --git a/rest-api/db/migrations/20240805132306_last_modified_triggers.sql b/rest-api/db/migrations/20240805132306_last_modified_triggers.sql
index ea8794b..2714d42 100644
--- a/rest-api/db/migrations/20240805132306_last_modified_triggers.sql
+++ b/rest-api/db/migrations/20240805132306_last_modified_triggers.sql
@@ -68,7 +68,7 @@ CREATE TRIGGER update_footer_last_modified
EXECUTE FUNCTION internal.update_last_modified ();
CREATE TRIGGER update_legal_information_last_modified
- BEFORE INSERT OR DELETE ON internal.legal_information
+ BEFORE INSERT OR UPDATE OR DELETE ON internal.legal_information
FOR EACH ROW
EXECUTE FUNCTION internal.update_last_modified ();
diff --git a/rest-api/db/migrations/20240920090915_custom_domain_prefix.sql b/rest-api/db/migrations/20240920090915_custom_domain_prefix.sql
new file mode 100644
index 0000000..fe4a428
--- /dev/null
+++ b/rest-api/db/migrations/20240920090915_custom_domain_prefix.sql
@@ -0,0 +1,57 @@
+-- migrate:up
+CREATE TABLE internal.domain_prefix (
+ website_id UUID PRIMARY KEY REFERENCES internal.website (id) ON DELETE CASCADE,
+ prefix VARCHAR(16) UNIQUE NOT NULL CHECK (LENGTH(prefix) >= 3 AND prefix ~ '^[a-z]+(-[a-z]+)*$'),
+ 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
+);
+
+CREATE VIEW api.domain_prefix WITH ( security_invoker = ON
+) AS
+SELECT
+ *
+FROM
+ internal.domain_prefix;
+
+GRANT SELECT, INSERT, UPDATE, DELETE ON internal.domain_prefix TO authenticated_user;
+
+GRANT SELECT, INSERT, UPDATE, DELETE ON api.domain_prefix TO authenticated_user;
+
+ALTER TABLE internal.domain_prefix ENABLE ROW LEVEL SECURITY;
+
+CREATE POLICY view_domain_prefix ON internal.domain_prefix
+ FOR SELECT
+ USING (internal.user_has_website_access (website_id, 10));
+
+CREATE POLICY update_domain_prefix ON internal.domain_prefix
+ FOR UPDATE
+ USING (internal.user_has_website_access (website_id, 30));
+
+CREATE POLICY delete_domain_prefix ON internal.domain_prefix
+ FOR DELETE
+ USING (internal.user_has_website_access (website_id, 30));
+
+CREATE POLICY insert_domain_prefix ON internal.domain_prefix
+ FOR INSERT
+ WITH CHECK (internal.user_has_website_access (website_id, 30));
+
+CREATE TRIGGER update_domain_prefix_last_modified
+ BEFORE INSERT OR UPDATE OR DELETE ON internal.domain_prefix
+ FOR EACH ROW
+ EXECUTE FUNCTION internal.update_last_modified ();
+
+CREATE TRIGGER domain_prefix_track_changes
+ AFTER INSERT OR UPDATE OR DELETE ON internal.domain_prefix
+ FOR EACH ROW
+ EXECUTE FUNCTION internal.track_changes ();
+
+-- migrate:down
+DROP TRIGGER domain_prefix_track_changes ON internal.domain_prefix;
+
+DROP TRIGGER update_domain_prefix_last_modified ON internal.domain_prefix;
+
+DROP VIEW api.domain_prefix;
+
+DROP TABLE internal.domain_prefix;
+
diff --git a/web-app/src/lib/db-schema.ts b/web-app/src/lib/db-schema.ts
index 1e45e9d..1706d8e 100644
--- a/web-app/src/lib/db-schema.ts
+++ b/web-app/src/lib/db-schema.ts
@@ -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,
diff --git a/web-app/src/lib/server/utils.ts b/web-app/src/lib/server/utils.ts
index e745aeb..22d499a 100644
--- a/web-app/src/lib/server/utils.ts
+++ b/web-app/src/lib/server/utils.ts
@@ -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;
diff --git a/web-app/src/lib/utils.ts b/web-app/src/lib/utils.ts
index 1af3833..a16e396 100644
--- a/web-app/src/lib/utils.ts
+++ b/web-app/src/lib/utils.ts
@@ -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;
}
diff --git a/web-app/src/routes/(anonymous)/register/+page.server.ts b/web-app/src/routes/(anonymous)/register/+page.server.ts
index d8dfc0c..4fd0aef 100644
--- a/web-app/src/routes/(anonymous)/register/+page.server.ts
+++ b/web-app/src/routes/(anonymous)/register/+page.server.ts
@@ -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 }) => {
diff --git a/web-app/src/routes/(anonymous)/register/+page.svelte b/web-app/src/routes/(anonymous)/register/+page.svelte
index 3c5a620..2ec07f3 100644
--- a/web-app/src/routes/(anonymous)/register/+page.svelte
+++ b/web-app/src/routes/(anonymous)/register/+page.svelte
@@ -1,10 +1,10 @@
@@ -15,24 +15,52 @@
- Your website is published at:
-
- {data.websiteProdUrl}
-
+ Your website is published at:
+
+ {data.websiteProdUrl}
+