From 5f38500b9c40bf744a807771302a7bfad8a852ce Mon Sep 17 00:00:00 2001 From: thiloho <123883702+thiloho@users.noreply.github.com> Date: Sat, 14 Sep 2024 15:12:08 +0200 Subject: [PATCH] Show logs and usernames for deleted users and remove svg mimetype for client side --- .../migrations/20240719071602_main_tables.sql | 2 +- ...240720074103_user_management_roles_jwt.sql | 90 +++++++++---------- .../20240805132306_last_modified_triggers.sql | 21 ++++- .../20240808141708_collaborator_not_owner.sql | 13 ++- .../20240810115846_image_upload_function.sql | 28 +++--- .../migrations/20240911070907_change_log.sql | 35 ++++---- web-app/src/lib/db-schema.ts | 7 +- web-app/src/lib/utils.ts | 2 +- .../[websiteId]/collaborators/+page.svelte | 4 +- .../website/[websiteId]/logs/+page.server.ts | 4 +- .../website/[websiteId]/logs/+page.svelte | 8 +- 11 files changed, 119 insertions(+), 95 deletions(-) diff --git a/rest-api/db/migrations/20240719071602_main_tables.sql b/rest-api/db/migrations/20240719071602_main_tables.sql index e1fce1a..0a1ca79 100644 --- a/rest-api/db/migrations/20240719071602_main_tables.sql +++ b/rest-api/db/migrations/20240719071602_main_tables.sql @@ -43,7 +43,7 @@ CREATE TABLE internal.website ( CREATE TABLE internal.media ( id UUID PRIMARY KEY DEFAULT gen_random_uuid (), 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, + user_id UUID REFERENCES internal.user (id) ON DELETE SET NULL DEFAULT (CURRENT_SETTING('request.jwt.claims', TRUE)::JSON ->> 'user_id') ::UUID, blob BYTEA NOT NULL, mimetype TEXT NOT NULL, original_name TEXT NOT NULL, diff --git a/rest-api/db/migrations/20240720074103_user_management_roles_jwt.sql b/rest-api/db/migrations/20240720074103_user_management_roles_jwt.sql index 19ce8ef..a3c4505 100644 --- a/rest-api/db/migrations/20240720074103_user_management_roles_jwt.sql +++ b/rest-api/db/migrations/20240720074103_user_management_roles_jwt.sql @@ -7,18 +7,17 @@ CREATE FUNCTION internal.check_role_exists () RETURNS TRIGGER AS $$ BEGIN - IF NOT EXISTS ( + IF (NOT EXISTS ( SELECT 1 FROM pg_roles AS r WHERE - r.rolname = NEW.role) THEN - RAISE foreign_key_violation - USING message = 'Unknown database role: ' || NEW.role; - RETURN NULL; -END IF; - RETURN NEW; + r.rolname = NEW.role)) THEN + RAISE foreign_key_violation + USING message = 'Unknown database role: ' || NEW.role; + END IF; + RETURN NULL; END $$ LANGUAGE plpgsql; @@ -67,43 +66,42 @@ DECLARE _password_length_min CONSTANT INT := 12; _password_length_max CONSTANT INT := 128; BEGIN - CASE WHEN LENGTH(register.username) - NOT BETWEEN _username_length_min AND _username_length_max THEN - RAISE string_data_length_mismatch - USING message = FORMAT('Username must be between %s and %s characters long', _username_length_min, _username_length_max); - WHEN EXISTS ( - SELECT - 1 - FROM - internal.user AS u - WHERE - u.username = register.username) THEN + IF (LENGTH(register.username) + NOT BETWEEN _username_length_min AND _username_length_max) THEN + RAISE string_data_length_mismatch + USING message = FORMAT('Username must be between %s and %s characters long', _username_length_min, _username_length_max); + ELSIF (EXISTS ( + SELECT + 1 + FROM + internal.user AS u + WHERE + u.username = register.username)) THEN RAISE unique_violation - USING message = 'Username is already taken'; - WHEN LENGTH(register.pass) NOT BETWEEN _password_length_min AND _password_length_max THEN - RAISE string_data_length_mismatch - USING message = FORMAT('Password must be between %s and %s characters long', _password_length_min, _password_length_max); - WHEN register.pass !~ '[a-z]' THEN - RAISE invalid_parameter_value - USING message = 'Password must contain at least one lowercase letter'; - WHEN register.pass !~ '[A-Z]' THEN - RAISE invalid_parameter_value - USING message = 'Password must contain at least one uppercase letter'; - WHEN register.pass !~ '[0-9]' THEN - RAISE invalid_parameter_value - USING message = 'Password must contain at least one number'; - WHEN register.pass !~ '[!@#$%^&*(),.?":{}|<>]' THEN - RAISE invalid_parameter_value - USING message = 'Password must contain at least one special character'; - ELSE - INSERT - INTO internal.user (username, password_hash) - VALUES (register.username, register.pass) - RETURNING - id INTO user_id; - END - CASE; - END; + USING message = 'Username is already taken'; + ELSIF (LENGTH(register.pass) + NOT BETWEEN _password_length_min AND _password_length_max) THEN + RAISE string_data_length_mismatch + USING message = FORMAT('Password must be between %s and %s characters long', _password_length_min, _password_length_max); + ELSIF register.pass !~ '[a-z]' THEN + RAISE invalid_parameter_value + USING message = 'Password must contain at least one lowercase letter'; + ELSIF register.pass !~ '[A-Z]' THEN + RAISE invalid_parameter_value + USING message = 'Password must contain at least one uppercase letter'; + ELSIF register.pass !~ '[0-9]' THEN + RAISE invalid_parameter_value + USING message = 'Password must contain at least one number'; + ELSIF register.pass !~ '[!@#$%^&*(),.?":{}|<>]' THEN + RAISE invalid_parameter_value + USING message = 'Password must contain at least one special character'; + ELSE + INSERT INTO internal.user (username, password_hash) + VALUES (register.username, register.pass) + RETURNING + id INTO user_id; + END IF; +END; $$ LANGUAGE plpgsql SECURITY DEFINER; @@ -120,7 +118,7 @@ BEGIN IF _role IS NULL THEN RAISE invalid_password USING message = 'Invalid username or password'; - END IF; + ELSE SELECT id INTO _user_id FROM @@ -130,6 +128,7 @@ BEGIN _exp := EXTRACT(EPOCH FROM CLOCK_TIMESTAMP())::INTEGER + 86400; SELECT SIGN(JSON_BUILD_OBJECT('role', _role, 'user_id', _user_id, 'username', login.username, 'exp', _exp), CURRENT_SETTING('app.jwt_secret')) INTO token; + END IF; END; $$ LANGUAGE plpgsql @@ -146,10 +145,11 @@ BEGIN IF _role IS NULL THEN RAISE invalid_password USING message = 'Invalid password'; - END IF; + ELSE DELETE FROM internal.user AS u WHERE u.username = _username; was_deleted := TRUE; + END IF; END; $$ LANGUAGE plpgsql diff --git a/rest-api/db/migrations/20240805132306_last_modified_triggers.sql b/rest-api/db/migrations/20240805132306_last_modified_triggers.sql index 9edc6b3..ea8794b 100644 --- a/rest-api/db/migrations/20240805132306_last_modified_triggers.sql +++ b/rest-api/db/migrations/20240805132306_last_modified_triggers.sql @@ -2,19 +2,32 @@ CREATE FUNCTION internal.update_last_modified () RETURNS TRIGGER AS $$ +DECLARE + _user_id UUID := (CURRENT_SETTING('request.jwt.claims', TRUE)::JSON ->> 'user_id')::UUID; BEGIN - NEW.last_modified_at = CLOCK_TIMESTAMP(); - NEW.last_modified_by = (CURRENT_SETTING('request.jwt.claims', TRUE)::JSON ->> 'user_id')::UUID; + IF (NOT EXISTS ( + SELECT + id + FROM + internal.user + WHERE + id = _user_id)) THEN + RETURN COALESCE(NEW, OLD); + END IF; + IF TG_OP != 'DELETE' THEN + NEW.last_modified_at = CLOCK_TIMESTAMP(); + NEW.last_modified_by = _user_id; + END IF; IF TG_TABLE_NAME != 'website' THEN UPDATE internal.website SET last_modified_at = CLOCK_TIMESTAMP(), - last_modified_by = (CURRENT_SETTING('request.jwt.claims', TRUE)::JSON ->> 'user_id')::UUID + last_modified_by = _user_id WHERE id = COALESCE(NEW.website_id, OLD.website_id); END IF; - RETURN NEW; + RETURN COALESCE(NEW, OLD); END; $$ LANGUAGE plpgsql; diff --git a/rest-api/db/migrations/20240808141708_collaborator_not_owner.sql b/rest-api/db/migrations/20240808141708_collaborator_not_owner.sql index 8cbfbc2..bf46316 100644 --- a/rest-api/db/migrations/20240808141708_collaborator_not_owner.sql +++ b/rest-api/db/migrations/20240808141708_collaborator_not_owner.sql @@ -3,18 +3,17 @@ CREATE FUNCTION internal.check_user_not_website_owner () RETURNS TRIGGER AS $$ BEGIN - IF EXISTS ( + IF (EXISTS ( SELECT 1 FROM internal.website AS w WHERE - w.id = NEW.website_id - AND w.user_id = NEW.user_id) THEN - RAISE foreign_key_violation - USING message = 'User cannot be added as a collaborator to their own website'; -END IF; - RETURN NEW; + w.id = NEW.website_id AND w.user_id = NEW.user_id)) THEN + RAISE foreign_key_violation + USING message = 'User cannot be added as a collaborator to their own website'; + END IF; + RETURN NULL; END; $$ LANGUAGE plpgsql; diff --git a/rest-api/db/migrations/20240810115846_image_upload_function.sql b/rest-api/db/migrations/20240810115846_image_upload_function.sql index 7a801bf..e94d73a 100644 --- a/rest-api/db/migrations/20240810115846_image_upload_function.sql +++ b/rest-api/db/migrations/20240810115846_image_upload_function.sql @@ -16,21 +16,21 @@ BEGIN IF OCTET_LENGTH($1) = 0 THEN RAISE invalid_parameter_value USING message = 'No file data was provided'; + ELSIF (_mimetype IS NULL + OR _mimetype NOT IN ( + SELECT + UNNEST(_allowed_mimetypes))) THEN + RAISE invalid_parameter_value + USING message = 'Invalid MIME type. Allowed types are: png, jpg, webp'; + ELSIF OCTET_LENGTH($1) > _max_file_size THEN + RAISE program_limit_exceeded + USING message = FORMAT('File size exceeds the maximum limit of %s MB', _max_file_size / (1024 * 1024)); + ELSE + INSERT INTO internal.media (website_id, blob, mimetype, original_name) + VALUES (_website_id, $1, _mimetype, _original_filename) + RETURNING + id INTO file_id; END IF; - IF _mimetype IS NULL OR _mimetype NOT IN ( - SELECT - UNNEST(_allowed_mimetypes)) THEN - RAISE invalid_parameter_value - USING message = 'Invalid MIME type. Allowed types are: png, jpg, webp'; - END IF; - IF OCTET_LENGTH($1) > _max_file_size THEN - RAISE program_limit_exceeded - USING message = FORMAT('File size exceeds the maximum limit of %s MB', _max_file_size / (1024 * 1024)); - END IF; - INSERT INTO internal.media (website_id, blob, mimetype, original_name) - VALUES (_website_id, $1, _mimetype, _original_filename) - RETURNING - id INTO file_id; END; $$ LANGUAGE plpgsql diff --git a/rest-api/db/migrations/20240911070907_change_log.sql b/rest-api/db/migrations/20240911070907_change_log.sql index 27dcb23..be5574e 100644 --- a/rest-api/db/migrations/20240911070907_change_log.sql +++ b/rest-api/db/migrations/20240911070907_change_log.sql @@ -4,7 +4,8 @@ CREATE EXTENSION hstore; CREATE TABLE internal.change_log ( id UUID PRIMARY KEY DEFAULT gen_random_uuid (), 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, + user_id UUID REFERENCES internal.user (id) ON DELETE SET NULL DEFAULT (CURRENT_SETTING('request.jwt.claims', TRUE)::JSON ->> 'user_id') ::UUID, + username VARCHAR(16) NOT NULL DEFAULT (CURRENT_SETTING('request.jwt.claims', TRUE)::JSON ->> 'username'), tstamp TIMESTAMPTZ NOT NULL DEFAULT CLOCK_TIMESTAMP(), table_name TEXT NOT NULL, operation TEXT NOT NULL, @@ -17,9 +18,16 @@ CREATE FUNCTION internal.track_changes () AS $$ DECLARE _website_id UUID; + _user_id UUID := (CURRENT_SETTING('request.jwt.claims', TRUE)::JSON ->> 'user_id')::UUID; BEGIN - IF (to_jsonb (OLD.*) - 'last_modified_at') = (to_jsonb (NEW.*) - 'last_modified_at') THEN - RETURN NEW; + IF (NOT EXISTS ( + SELECT + id + FROM + internal.user + WHERE + id = _user_id) OR (to_jsonb (OLD.*) - 'last_modified_at' - 'last_modified_by') = (to_jsonb (NEW.*) - 'last_modified_at' - 'last_modified_by')) THEN + RETURN NULL; END IF; IF TG_TABLE_NAME = 'website' THEN _website_id := NEW.id; @@ -29,31 +37,28 @@ BEGIN IF TG_OP = 'INSERT' THEN INSERT INTO internal.change_log (website_id, table_name, operation, new_value) VALUES (_website_id, TG_TABLE_NAME, TG_OP, HSTORE (NEW)); - RETURN NEW; - ELSIF TG_OP = 'UPDATE' + ELSIF (TG_OP = 'UPDATE' AND EXISTS ( SELECT id FROM internal.website WHERE - id = _website_id) THEN - INSERT INTO internal.change_log (website_id, table_name, operation, old_value, new_value) - VALUES (_website_id, TG_TABLE_NAME, TG_OP, HSTORE (OLD) - HSTORE (NEW), HSTORE (NEW) - HSTORE (OLD)); - RETURN NEW; - ELSIF TG_OP = 'DELETE' + id = _website_id)) THEN + INSERT INTO internal.change_log (website_id, table_name, operation, old_value, new_value) + VALUES (_website_id, TG_TABLE_NAME, TG_OP, HSTORE (OLD) - HSTORE (NEW), HSTORE (NEW) - HSTORE (OLD)); + ELSIF (TG_OP = 'DELETE' AND EXISTS ( SELECT id FROM internal.website WHERE - id = _website_id) THEN - INSERT INTO internal.change_log (website_id, table_name, operation, old_value) - VALUES (_website_id, TG_TABLE_NAME, TG_OP, HSTORE (OLD)); - RETURN NEW; + id = _website_id)) THEN + INSERT INTO internal.change_log (website_id, table_name, operation, old_value) + VALUES (_website_id, TG_TABLE_NAME, TG_OP, HSTORE (OLD)); END IF; - RETURN NEW; + RETURN NULL; END; $$ LANGUAGE plpgsql diff --git a/web-app/src/lib/db-schema.ts b/web-app/src/lib/db-schema.ts index 5292996..1e45e9d 100644 --- a/web-app/src/lib/db-schema.ts +++ b/web-app/src/lib/db-schema.ts @@ -83,6 +83,7 @@ export interface ChangeLog { id: string; website_id: string | null; user_id: string | null; + username: string; tstamp: Date; table_name: string; operation: string; @@ -93,6 +94,7 @@ export interface ChangeLogInput { id?: string; website_id?: string | null; user_id?: string | null; + username?: string; tstamp?: Date; table_name: string; operation: string; @@ -105,6 +107,7 @@ const change_log = { "id", "website_id", "user_id", + "username", "tstamp", "table_name", "operation", @@ -320,7 +323,7 @@ const legal_information = { export interface Media { id: string; website_id: string; - user_id: string; + user_id: string | null; blob: string; mimetype: string; original_name: string; @@ -329,7 +332,7 @@ export interface Media { export interface MediaInput { id?: string; website_id: string; - user_id?: string; + user_id?: string | null; blob: string; mimetype: string; original_name: string; diff --git a/web-app/src/lib/utils.ts b/web-app/src/lib/utils.ts index 45dc7fa..9307bb9 100644 --- a/web-app/src/lib/utils.ts +++ b/web-app/src/lib/utils.ts @@ -15,7 +15,7 @@ import type { LegalInformation } from "$lib/db-schema"; -export const ALLOWED_MIME_TYPES = ["image/jpeg", "image/png", "image/svg+xml", "image/webp"]; +export const ALLOWED_MIME_TYPES = ["image/jpeg", "image/png", "image/webp"]; export const slugify = (string: string) => { return string diff --git a/web-app/src/routes/(authenticated)/website/[websiteId]/collaborators/+page.svelte b/web-app/src/routes/(authenticated)/website/[websiteId]/collaborators/+page.svelte index 2269593..1edb9f4 100644 --- a/web-app/src/routes/(authenticated)/website/[websiteId]/collaborators/+page.svelte +++ b/web-app/src/routes/(authenticated)/website/[websiteId]/collaborators/+page.svelte @@ -69,10 +69,10 @@
- {username} ({permission_level}) + {user?.username} ({permission_level})