diff --git a/src/deploy/function_account_password_reset_request.sql b/src/deploy/function_account_password_reset_request.sql index fe3c26dc..abf5efde 100644 --- a/src/deploy/function_account_password_reset_request.sql +++ b/src/deploy/function_account_password_reset_request.sql @@ -5,31 +5,36 @@ CREATE FUNCTION vibetype.account_password_reset_request( language TEXT ) RETURNS VOID AS $$ DECLARE - _notify_data RECORD; + _account vibetype_private.account%ROWTYPE; + _notify_data RECORD := NULL; BEGIN - WITH updated AS ( - UPDATE vibetype_private.account - SET password_reset_verification = gen_random_uuid() - WHERE account.email_address = account_password_reset_request.email_address - RETURNING * - ) SELECT - account.username, - updated.email_address, - updated.password_reset_verification, - updated.password_reset_verification_valid_until - FROM updated, vibetype.account - WHERE updated.id = account.id - INTO _notify_data; + + UPDATE vibetype_private.account + SET password_reset_verification = gen_random_uuid() + WHERE account.email_address = account_password_reset_request.email_address + RETURNING * INTO _account; + + IF _account IS NOT NULL THEN + SELECT + username, + _account.email_address, + _account.password_reset_verification, + _account.password_reset_verification_valid_until + INTO _notify_data + FROM vibetype.account + WHERE id = _account.id; + END IF; IF (_notify_data IS NULL) THEN -- noop ELSE - INSERT INTO vibetype_private.notification (channel, payload) VALUES ( + INSERT INTO vibetype.notification (channel, payload, created_by) VALUES ( 'account_password_reset_request', jsonb_pretty(jsonb_build_object( 'account', _notify_data, 'template', jsonb_build_object('language', account_password_reset_request.language) - )) + )), + _account.id ); END IF; END; diff --git a/src/deploy/function_account_registration.sql b/src/deploy/function_account_registration.sql index 17c31359..c25afd9f 100644 --- a/src/deploy/function_account_registration.sql +++ b/src/deploy/function_account_registration.sql @@ -9,8 +9,8 @@ CREATE FUNCTION vibetype.account_registration( username TEXT ) RETURNS VOID AS $$ DECLARE - _new_account_private vibetype_private.account; - _new_account_public vibetype.account; + _new_account_private vibetype_private.account%ROWTYPE; + _new_account_public vibetype.account%ROWTYPE; _new_account_notify RECORD; BEGIN IF account_registration.birth_date > CURRENT_DATE - INTERVAL '18 years' THEN @@ -50,12 +50,13 @@ BEGIN INSERT INTO vibetype.contact(account_id, created_by) VALUES (_new_account_private.id, _new_account_private.id); - INSERT INTO vibetype_private.notification (channel, payload) VALUES ( + INSERT INTO vibetype.notification (channel, payload, created_by) VALUES ( 'account_registration', jsonb_pretty(jsonb_build_object( 'account', row_to_json(_new_account_notify), 'template', jsonb_build_object('language', account_registration.language) - )) + )), + _new_account_private.id ); -- not possible to return data here as this would make the silent return above for email address duplicates distinguishable from a successful registration diff --git a/src/deploy/function_account_registration_refresh.sql b/src/deploy/function_account_registration_refresh.sql index 995fcceb..7bda51c2 100644 --- a/src/deploy/function_account_registration_refresh.sql +++ b/src/deploy/function_account_registration_refresh.sql @@ -23,16 +23,16 @@ BEGIN updated.email_address, updated.email_address_verification, updated.email_address_verification_valid_until - FROM updated, vibetype.account - WHERE updated.id = account.id + FROM updated JOIN vibetype.account ON updated.id = account.id INTO _new_account_notify; - INSERT INTO vibetype_private.notification (channel, payload) VALUES ( + INSERT INTO vibetype.notification (channel, payload, created_by) VALUES ( 'account_registration', jsonb_pretty(jsonb_build_object( 'account', row_to_json(_new_account_notify), 'template', jsonb_build_object('language', account_registration_refresh.language) - )) + )), + account_registration_refresh.account_id ); END; $$ LANGUAGE PLPGSQL STRICT SECURITY DEFINER; diff --git a/src/deploy/function_invite.sql b/src/deploy/function_invite.sql index 5d33109b..794cf8cc 100644 --- a/src/deploy/function_invite.sql +++ b/src/deploy/function_invite.sql @@ -3,18 +3,20 @@ BEGIN; CREATE FUNCTION vibetype.invite( guest_id UUID, language TEXT -) RETURNS VOID AS $$ +) RETURNS UUID AS $$ DECLARE _contact RECORD; _email_address TEXT; _event RECORD; - _event_creator_profile_picture_upload_id UUID; _event_creator_profile_picture_upload_storage_key TEXT; _event_creator_username TEXT; _guest RECORD; + _id UUID; BEGIN -- Guest UUID - SELECT * FROM vibetype.guest INTO _guest WHERE guest.id = invite.guest_id; + SELECT * INTO _guest + FROM vibetype.guest + WHERE guest.id = invite.guest_id; IF ( _guest IS NULL @@ -41,14 +43,17 @@ BEGIN visibility, created_at, created_by - FROM vibetype.event INTO _event WHERE "event".id = _guest.event_id; + INTO _event + FROM vibetype.event WHERE id = _guest.event_id; IF (_event IS NULL) THEN RAISE 'Event not accessible!' USING ERRCODE = 'no_data_found'; END IF; -- Contact - SELECT account_id, email_address FROM vibetype.contact INTO _contact WHERE contact.id = _guest.contact_id; + SELECT account_id, email_address INTO _contact + FROM vibetype.contact + WHERE id = _guest.contact_id; IF (_contact IS NULL) THEN RAISE 'Contact not accessible!' USING ERRCODE = 'no_data_found'; @@ -62,7 +67,9 @@ BEGIN END IF; ELSE -- Account - SELECT email_address FROM vibetype_private.account INTO _email_address WHERE account.id = _contact.account_id; + SELECT email_address INTO _email_address + FROM vibetype_private.account + WHERE id = _contact.account_id; IF (_email_address IS NULL) THEN RAISE 'Account email address not accessible!' USING ERRCODE = 'no_data_found'; @@ -70,14 +77,19 @@ BEGIN END IF; -- Event creator username - SELECT username FROM vibetype.account INTO _event_creator_username WHERE account.id = _event.created_by; + SELECT username INTO _event_creator_username + FROM vibetype.account + WHERE id = _event.created_by; -- Event creator profile picture storage key - SELECT upload_id FROM vibetype.profile_picture INTO _event_creator_profile_picture_upload_id WHERE profile_picture.account_id = _event.created_by; - SELECT storage_key FROM vibetype.upload INTO _event_creator_profile_picture_upload_storage_key WHERE upload.id = _event_creator_profile_picture_upload_id; + SELECT u.storage_key INTO _event_creator_profile_picture_upload_storage_key + FROM vibetype.profile_picture p + JOIN vibetype.upload u ON p.upload_id = u.id + WHERE p.account_id = _event.created_by; - INSERT INTO vibetype_private.notification (channel, payload) + INSERT INTO vibetype.notification_invitation (guest_id, channel, payload, created_by) VALUES ( + invite.guest_id, 'event_invitation', jsonb_pretty(jsonb_build_object( 'data', jsonb_build_object( @@ -88,8 +100,12 @@ BEGIN 'guestId', _guest.id ), 'template', jsonb_build_object('language', invite.language) - )) - ); + )), + vibetype.invoker_account_id() + ) + RETURNING id INTO _id; + + RETURN _id; END; $$ LANGUAGE PLPGSQL STRICT SECURITY DEFINER; diff --git a/src/deploy/function_notification_acknowledge.sql b/src/deploy/function_notification_acknowledge.sql index be59321c..8eec35a5 100644 --- a/src/deploy/function_notification_acknowledge.sql +++ b/src/deploy/function_notification_acknowledge.sql @@ -4,10 +4,15 @@ CREATE FUNCTION vibetype.notification_acknowledge( id UUID, is_acknowledged BOOLEAN ) RETURNS VOID AS $$ +DECLARE + update_count INTEGER; BEGIN - IF (EXISTS (SELECT 1 FROM vibetype_private.notification WHERE "notification".id = notification_acknowledge.id)) THEN - UPDATE vibetype_private.notification SET is_acknowledged = notification_acknowledge.is_acknowledged WHERE "notification".id = notification_acknowledge.id; - ELSE + UPDATE vibetype.notification SET + is_acknowledged = notification_acknowledge.is_acknowledged + WHERE id = notification_acknowledge.id; + + GET DIAGNOSTICS update_count = ROW_COUNT; + IF update_count = 0 THEN RAISE 'Notification with given id not found!' USING ERRCODE = 'no_data_found'; END IF; END; diff --git a/src/deploy/table_notification.sql b/src/deploy/table_notification.sql index beac4e7d..030cfdc8 100644 --- a/src/deploy/table_notification.sql +++ b/src/deploy/table_notification.sql @@ -1,23 +1,36 @@ BEGIN; -CREATE TABLE vibetype_private.notification ( +CREATE TABLE vibetype.notification ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), channel TEXT NOT NULL, is_acknowledged BOOLEAN, payload TEXT NOT NULL CHECK (octet_length(payload) <= 8000), - created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP + created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, + created_by UUID NOT NULL REFERENCES vibetype.account(id) ON DELETE CASCADE ); -COMMENT ON TABLE vibetype_private.notification IS 'A notification.'; -COMMENT ON COLUMN vibetype_private.notification.id IS 'The notification''s internal id.'; -COMMENT ON COLUMN vibetype_private.notification.channel IS 'The notification''s channel.'; -COMMENT ON COLUMN vibetype_private.notification.is_acknowledged IS 'Whether the notification was acknowledged.'; -COMMENT ON COLUMN vibetype_private.notification.payload IS 'The notification''s payload.'; -COMMENT ON COLUMN vibetype_private.notification.created_at IS 'The timestamp of the notification''s creation.'; +CREATE INDEX idx_notification_created_by ON vibetype.notification USING btree (created_by); + +COMMENT ON TABLE vibetype.notification IS 'A notification.'; +COMMENT ON COLUMN vibetype.notification.id IS 'The notification''s internal id.'; +COMMENT ON COLUMN vibetype.notification.channel IS 'The notification''s channel.'; +COMMENT ON COLUMN vibetype.notification.is_acknowledged IS 'Whether the notification was acknowledged.'; +COMMENT ON COLUMN vibetype.notification.payload IS 'The notification''s payload.'; +COMMENT ON COLUMN vibetype.notification.created_at IS 'The timestamp of the notification''s creation.'; +COMMENT ON COLUMN vibetype.notification.created_by IS 'Reference to the account that created the notification.'; + +GRANT SELECT ON vibetype.notification TO vibetype_account; + +ALTER TABLE vibetype.notification ENABLE ROW LEVEL SECURITY; + +CREATE POLICY notification_all ON vibetype.notification FOR ALL +USING ( + created_by = vibetype.invoker_account_id() +); \set role_service_grafana_username `cat /run/secrets/postgres_role_service_grafana_username` -GRANT SELECT ON TABLE vibetype_private.notification TO :role_service_grafana_username; +GRANT SELECT ON TABLE vibetype.notification TO :role_service_grafana_username; COMMIT; diff --git a/src/deploy/table_notification_invitation.sql b/src/deploy/table_notification_invitation.sql new file mode 100644 index 00000000..5fb5022a --- /dev/null +++ b/src/deploy/table_notification_invitation.sql @@ -0,0 +1,17 @@ +BEGIN; + +CREATE TABLE vibetype.notification_invitation ( + guest_id UUID NOT NULL REFERENCES vibetype.guest(id) +) +INHERITS (vibetype.notification); + +CREATE INDEX idx_invitation_guest_id ON vibetype.notification_invitation USING btree (guest_id); + +COMMENT ON TABLE vibetype.notification_invitation IS '@omit update,delete\nStores invitations and their statuses.'; +COMMENT ON COLUMN vibetype.notification_invitation.guest_id IS 'The ID of the guest associated with this invitation.'; + +GRANT SELECT ON vibetype.notification_invitation TO vibetype_account; + +ALTER TABLE vibetype.notification_invitation ENABLE ROW LEVEL SECURITY; + +COMMIT; diff --git a/src/revert/table_notification.sql b/src/revert/table_notification.sql index 1c1b3517..f1d545f3 100644 --- a/src/revert/table_notification.sql +++ b/src/revert/table_notification.sql @@ -1,5 +1,7 @@ BEGIN; -DROP TABLE vibetype_private.notification; +DROP POLICY notification_all ON vibetype.notification; + +DROP TABLE vibetype.notification; COMMIT; diff --git a/src/revert/table_notification_invitation.sql b/src/revert/table_notification_invitation.sql new file mode 100644 index 00000000..0e1c2a70 --- /dev/null +++ b/src/revert/table_notification_invitation.sql @@ -0,0 +1,5 @@ +BEGIN; + +DROP TABLE vibetype.notification_invitation; + +COMMIT; diff --git a/src/sqitch.plan b/src/sqitch.plan index cff21a3b..f1f3972e 100644 --- a/src/sqitch.plan +++ b/src/sqitch.plan @@ -16,9 +16,9 @@ function_invoker_account_id [privilege_execute_revoke schema_public role_account enum_invitation_feedback [schema_public] 1970-01-01T00:00:00Z Jonas Thelemann # Possible answers to an invitation: accepted, canceled. enum_event_visibility [schema_public] 1970-01-01T00:00:00Z Jonas Thelemann # Possible visibilities of events and event groups: public, private and unlisted. function_trigger_metadata_update [privilege_execute_revoke schema_public function_invoker_account_id role_account] 1970-01-01T00:00:00Z Jonas Thelemann # Trigger function to automatically update metadata fields `updated_at` and `updated_by` when a row is modified. Sets `updated_at` to the current timestamp and `updated_by` to the account ID of the invoker. -table_notification [schema_private] 1970-01-01T00:00:00Z Jonas Thelemann # Notifications that are sent via pg_notify. table_account_private [schema_private] 1970-01-01T00:00:00Z Jonas Thelemann # Add private table account. table_account_public [schema_public schema_private table_account_private] 1970-01-01T00:00:00Z Jonas Thelemann # Add public table account. +table_notification [schema_private table_account_public] 1970-01-01T00:00:00Z Jonas Thelemann # Notifications that are sent via pg_notify. table_account_block [schema_public table_account_public] 1970-01-01T00:00:00Z Sven Thelemann # Blocking of an account by another account. function_account_block_ids [schema_public table_account_block role_account role_anonymous function_invoker_account_id] 1970-01-01T00:00:00Z Sven Thelemann # Function returning all account id's involved in blocking. enum_language [schema_public] 1970-01-01T00:00:00Z Jonas Thelemann # Supported ISO 639 language codes. @@ -87,6 +87,7 @@ table_event_format_mapping [schema_public table_event table_event_format role_an table_audit_log [schema_private] 1970-01-01T00:00:00Z Sven Thelemann # Table for storing audit log records. view_audit_log_trigger [schema_private] 1970-01-01T00:00:00Z Sven Thelemann # View showing all audit log triggers. function_audit_log [schema_private table_audit_log view_audit_log_trigger] 1970-01-01T00:00:00Z Sven Thelemann # Utility functions for managing audit log triggers. +table_notification_invitation [schema_public table_guest] 1970-01-01T00:00:00Z Sven Thelemann # A table for tracking actions around invitations. table_preference_event_format [schema_public table_account_public table_event_category] 1970-01-01T00:00:00Z Sven Thelemann # Event formats a user account is interested in (M:N relationship). function_account_location_update [privilege_execute_revoke schema_public table_account_private function_invoker_account_id role_account] 1970-01-01T00:00:00Z Jonas Thelemann # Sets the location for the invoker's account. table_preference_event_location [schema_public table_account_public role_account function_invoker_account_id] 1970-01-01T00:00:00Z Jonas Thelemann # Stores preferred event locations for user accounts, including coordinates and search radius. diff --git a/src/verify/table_notification.sql b/src/verify/table_notification.sql index dce56cd3..d5c8f157 100644 --- a/src/verify/table_notification.sql +++ b/src/verify/table_notification.sql @@ -4,7 +4,20 @@ SELECT id, channel, is_acknowledged, payload, + created_by, created_at -FROM vibetype_private.notification WHERE FALSE; +FROM vibetype.notification WHERE FALSE; + +DO $$ +BEGIN + ASSERT (SELECT pg_catalog.has_table_privilege('vibetype_account', 'vibetype.notification', 'SELECT')); + ASSERT NOT (SELECT pg_catalog.has_table_privilege('vibetype_account', 'vibetype.notification', 'INSERT')); + ASSERT NOT (SELECT pg_catalog.has_table_privilege('vibetype_account', 'vibetype.notification', 'UPDATE')); + ASSERT NOT (SELECT pg_catalog.has_table_privilege('vibetype_account', 'vibetype.notification', 'DELETE')); + ASSERT NOT (SELECT pg_catalog.has_table_privilege('vibetype_anonymous', 'vibetype.notification', 'SELECT')); + ASSERT NOT (SELECT pg_catalog.has_table_privilege('vibetype_anonymous', 'vibetype.notification', 'INSERT')); + ASSERT NOT (SELECT pg_catalog.has_table_privilege('vibetype_anonymous', 'vibetype.notification', 'UPDATE')); + ASSERT NOT (SELECT pg_catalog.has_table_privilege('vibetype_anonymous', 'vibetype.notification', 'DELETE')); +END $$; ROLLBACK; diff --git a/src/verify/table_notification_invitation.sql b/src/verify/table_notification_invitation.sql new file mode 100644 index 00000000..07dab49d --- /dev/null +++ b/src/verify/table_notification_invitation.sql @@ -0,0 +1,28 @@ +BEGIN; + +SELECT + -- inherited from vibetype.notification + id, + channel, + is_acknowledged, + payload, + created_by, + created_at, + -- columns specific for vibetype.notification_invitation + guest_id +FROM vibetype.notification_invitation +WHERE FALSE; + +DO $$ +BEGIN + ASSERT (SELECT pg_catalog.has_table_privilege('vibetype_account', 'vibetype.notification_invitation', 'SELECT')); + ASSERT NOT (SELECT pg_catalog.has_table_privilege('vibetype_account', 'vibetype.notification_invitation', 'INSERT')); + ASSERT NOT (SELECT pg_catalog.has_table_privilege('vibetype_account', 'vibetype.notification_invitation', 'UPDATE')); + ASSERT NOT (SELECT pg_catalog.has_table_privilege('vibetype_account', 'vibetype.notification_invitation', 'DELETE')); + ASSERT NOT (SELECT pg_catalog.has_table_privilege('vibetype_anonymous', 'vibetype.notification_invitation', 'SELECT')); + ASSERT NOT (SELECT pg_catalog.has_table_privilege('vibetype_anonymous', 'vibetype.notification_invitation', 'INSERT')); + ASSERT NOT (SELECT pg_catalog.has_table_privilege('vibetype_anonymous', 'vibetype.notification_invitation', 'UPDATE')); + ASSERT NOT (SELECT pg_catalog.has_table_privilege('vibetype_anonymous', 'vibetype.notification_invitation', 'DELETE')); +END $$; + +ROLLBACK; diff --git a/test/fixture/schema.definition.sql b/test/fixture/schema.definition.sql index 04152a5e..79469a0b 100644 --- a/test/fixture/schema.definition.sql +++ b/test/fixture/schema.definition.sql @@ -548,31 +548,36 @@ CREATE FUNCTION vibetype.account_password_reset_request(email_address text, lang LANGUAGE plpgsql STRICT SECURITY DEFINER AS $$ DECLARE - _notify_data RECORD; + _account vibetype_private.account%ROWTYPE; + _notify_data RECORD := NULL; BEGIN - WITH updated AS ( - UPDATE vibetype_private.account - SET password_reset_verification = gen_random_uuid() - WHERE account.email_address = account_password_reset_request.email_address - RETURNING * - ) SELECT - account.username, - updated.email_address, - updated.password_reset_verification, - updated.password_reset_verification_valid_until - FROM updated, vibetype.account - WHERE updated.id = account.id - INTO _notify_data; + + UPDATE vibetype_private.account + SET password_reset_verification = gen_random_uuid() + WHERE account.email_address = account_password_reset_request.email_address + RETURNING * INTO _account; + + IF _account IS NOT NULL THEN + SELECT + username, + _account.email_address, + _account.password_reset_verification, + _account.password_reset_verification_valid_until + INTO _notify_data + FROM vibetype.account + WHERE id = _account.id; + END IF; IF (_notify_data IS NULL) THEN -- noop ELSE - INSERT INTO vibetype_private.notification (channel, payload) VALUES ( + INSERT INTO vibetype.notification (channel, payload, created_by) VALUES ( 'account_password_reset_request', jsonb_pretty(jsonb_build_object( 'account', _notify_data, 'template', jsonb_build_object('language', account_password_reset_request.language) - )) + )), + _account.id ); END IF; END; @@ -596,8 +601,8 @@ CREATE FUNCTION vibetype.account_registration(birth_date date, email_address tex LANGUAGE plpgsql STRICT SECURITY DEFINER AS $$ DECLARE - _new_account_private vibetype_private.account; - _new_account_public vibetype.account; + _new_account_private vibetype_private.account%ROWTYPE; + _new_account_public vibetype.account%ROWTYPE; _new_account_notify RECORD; BEGIN IF account_registration.birth_date > CURRENT_DATE - INTERVAL '18 years' THEN @@ -637,12 +642,13 @@ BEGIN INSERT INTO vibetype.contact(account_id, created_by) VALUES (_new_account_private.id, _new_account_private.id); - INSERT INTO vibetype_private.notification (channel, payload) VALUES ( + INSERT INTO vibetype.notification (channel, payload, created_by) VALUES ( 'account_registration', jsonb_pretty(jsonb_build_object( 'account', row_to_json(_new_account_notify), 'template', jsonb_build_object('language', account_registration.language) - )) + )), + _new_account_private.id ); -- not possible to return data here as this would make the silent return above for email address duplicates distinguishable from a successful registration @@ -685,16 +691,16 @@ BEGIN updated.email_address, updated.email_address_verification, updated.email_address_verification_valid_until - FROM updated, vibetype.account - WHERE updated.id = account.id + FROM updated JOIN vibetype.account ON updated.id = account.id INTO _new_account_notify; - INSERT INTO vibetype_private.notification (channel, payload) VALUES ( + INSERT INTO vibetype.notification (channel, payload, created_by) VALUES ( 'account_registration', jsonb_pretty(jsonb_build_object( 'account', row_to_json(_new_account_notify), 'template', jsonb_build_object('language', account_registration_refresh.language) - )) + )), + account_registration_refresh.account_id ); END; $$; @@ -1477,20 +1483,22 @@ COMMENT ON FUNCTION vibetype.guest_count(event_id uuid) IS 'Returns the guest co -- Name: invite(uuid, text); Type: FUNCTION; Schema: vibetype; Owner: ci -- -CREATE FUNCTION vibetype.invite(guest_id uuid, language text) RETURNS void +CREATE FUNCTION vibetype.invite(guest_id uuid, language text) RETURNS uuid LANGUAGE plpgsql STRICT SECURITY DEFINER AS $$ DECLARE _contact RECORD; _email_address TEXT; _event RECORD; - _event_creator_profile_picture_upload_id UUID; _event_creator_profile_picture_upload_storage_key TEXT; _event_creator_username TEXT; _guest RECORD; + _id UUID; BEGIN -- Guest UUID - SELECT * FROM vibetype.guest INTO _guest WHERE guest.id = invite.guest_id; + SELECT * INTO _guest + FROM vibetype.guest + WHERE guest.id = invite.guest_id; IF ( _guest IS NULL @@ -1517,14 +1525,17 @@ BEGIN visibility, created_at, created_by - FROM vibetype.event INTO _event WHERE "event".id = _guest.event_id; + INTO _event + FROM vibetype.event WHERE id = _guest.event_id; IF (_event IS NULL) THEN RAISE 'Event not accessible!' USING ERRCODE = 'no_data_found'; END IF; -- Contact - SELECT account_id, email_address FROM vibetype.contact INTO _contact WHERE contact.id = _guest.contact_id; + SELECT account_id, email_address INTO _contact + FROM vibetype.contact + WHERE id = _guest.contact_id; IF (_contact IS NULL) THEN RAISE 'Contact not accessible!' USING ERRCODE = 'no_data_found'; @@ -1538,7 +1549,9 @@ BEGIN END IF; ELSE -- Account - SELECT email_address FROM vibetype_private.account INTO _email_address WHERE account.id = _contact.account_id; + SELECT email_address INTO _email_address + FROM vibetype_private.account + WHERE id = _contact.account_id; IF (_email_address IS NULL) THEN RAISE 'Account email address not accessible!' USING ERRCODE = 'no_data_found'; @@ -1546,14 +1559,19 @@ BEGIN END IF; -- Event creator username - SELECT username FROM vibetype.account INTO _event_creator_username WHERE account.id = _event.created_by; + SELECT username INTO _event_creator_username + FROM vibetype.account + WHERE id = _event.created_by; -- Event creator profile picture storage key - SELECT upload_id FROM vibetype.profile_picture INTO _event_creator_profile_picture_upload_id WHERE profile_picture.account_id = _event.created_by; - SELECT storage_key FROM vibetype.upload INTO _event_creator_profile_picture_upload_storage_key WHERE upload.id = _event_creator_profile_picture_upload_id; + SELECT u.storage_key INTO _event_creator_profile_picture_upload_storage_key + FROM vibetype.profile_picture p + JOIN vibetype.upload u ON p.upload_id = u.id + WHERE p.account_id = _event.created_by; - INSERT INTO vibetype_private.notification (channel, payload) + INSERT INTO vibetype.notification_invitation (guest_id, channel, payload, created_by) VALUES ( + invite.guest_id, 'event_invitation', jsonb_pretty(jsonb_build_object( 'data', jsonb_build_object( @@ -1564,8 +1582,12 @@ BEGIN 'guestId', _guest.id ), 'template', jsonb_build_object('language', invite.language) - )) - ); + )), + vibetype.invoker_account_id() + ) + RETURNING id INTO _id; + + RETURN _id; END; $$; @@ -1723,10 +1745,15 @@ ALTER FUNCTION vibetype.legal_term_change() OWNER TO ci; CREATE FUNCTION vibetype.notification_acknowledge(id uuid, is_acknowledged boolean) RETURNS void LANGUAGE plpgsql STRICT SECURITY DEFINER AS $$ +DECLARE + update_count INTEGER; BEGIN - IF (EXISTS (SELECT 1 FROM vibetype_private.notification WHERE "notification".id = notification_acknowledge.id)) THEN - UPDATE vibetype_private.notification SET is_acknowledged = notification_acknowledge.is_acknowledged WHERE "notification".id = notification_acknowledge.id; - ELSE + UPDATE vibetype.notification SET + is_acknowledged = notification_acknowledge.is_acknowledged + WHERE id = notification_acknowledge.id; + + GET DIAGNOSTICS update_count = ROW_COUNT; + IF update_count = 0 THEN RAISE 'Notification with given id not found!' USING ERRCODE = 'no_data_found'; END IF; END; @@ -4145,6 +4172,98 @@ COMMENT ON COLUMN vibetype.legal_term_acceptance.created_at IS '@omit create Timestamp showing when the legal terms were accepted, set automatically at the time of acceptance.'; +-- +-- Name: notification; Type: TABLE; Schema: vibetype; Owner: ci +-- + +CREATE TABLE vibetype.notification ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + channel text NOT NULL, + is_acknowledged boolean, + payload text NOT NULL, + created_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, + created_by uuid NOT NULL, + CONSTRAINT notification_payload_check CHECK ((octet_length(payload) <= 8000)) +); + + +ALTER TABLE vibetype.notification OWNER TO ci; + +-- +-- Name: TABLE notification; Type: COMMENT; Schema: vibetype; Owner: ci +-- + +COMMENT ON TABLE vibetype.notification IS 'A notification.'; + + +-- +-- Name: COLUMN notification.id; Type: COMMENT; Schema: vibetype; Owner: ci +-- + +COMMENT ON COLUMN vibetype.notification.id IS 'The notification''s internal id.'; + + +-- +-- Name: COLUMN notification.channel; Type: COMMENT; Schema: vibetype; Owner: ci +-- + +COMMENT ON COLUMN vibetype.notification.channel IS 'The notification''s channel.'; + + +-- +-- Name: COLUMN notification.is_acknowledged; Type: COMMENT; Schema: vibetype; Owner: ci +-- + +COMMENT ON COLUMN vibetype.notification.is_acknowledged IS 'Whether the notification was acknowledged.'; + + +-- +-- Name: COLUMN notification.payload; Type: COMMENT; Schema: vibetype; Owner: ci +-- + +COMMENT ON COLUMN vibetype.notification.payload IS 'The notification''s payload.'; + + +-- +-- Name: COLUMN notification.created_at; Type: COMMENT; Schema: vibetype; Owner: ci +-- + +COMMENT ON COLUMN vibetype.notification.created_at IS 'The timestamp of the notification''s creation.'; + + +-- +-- Name: COLUMN notification.created_by; Type: COMMENT; Schema: vibetype; Owner: ci +-- + +COMMENT ON COLUMN vibetype.notification.created_by IS 'Reference to the account that created the notification.'; + + +-- +-- Name: notification_invitation; Type: TABLE; Schema: vibetype; Owner: ci +-- + +CREATE TABLE vibetype.notification_invitation ( + guest_id uuid NOT NULL +) +INHERITS (vibetype.notification); + + +ALTER TABLE vibetype.notification_invitation OWNER TO ci; + +-- +-- Name: TABLE notification_invitation; Type: COMMENT; Schema: vibetype; Owner: ci +-- + +COMMENT ON TABLE vibetype.notification_invitation IS '@omit update,delete\nStores invitations and their statuses.'; + + +-- +-- Name: COLUMN notification_invitation.guest_id; Type: COMMENT; Schema: vibetype; Owner: ci +-- + +COMMENT ON COLUMN vibetype.notification_invitation.guest_id IS 'The ID of the guest associated with this invitation.'; + + -- -- Name: preference_event_category; Type: TABLE; Schema: vibetype; Owner: ci -- @@ -4903,61 +5022,17 @@ COMMENT ON COLUMN vibetype_private.jwt.token IS 'The token.'; -- --- Name: notification; Type: TABLE; Schema: vibetype_private; Owner: ci --- - -CREATE TABLE vibetype_private.notification ( - id uuid DEFAULT gen_random_uuid() NOT NULL, - channel text NOT NULL, - is_acknowledged boolean, - payload text NOT NULL, - created_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, - CONSTRAINT notification_payload_check CHECK ((octet_length(payload) <= 8000)) -); - - -ALTER TABLE vibetype_private.notification OWNER TO ci; - --- --- Name: TABLE notification; Type: COMMENT; Schema: vibetype_private; Owner: ci --- - -COMMENT ON TABLE vibetype_private.notification IS 'A notification.'; - - --- --- Name: COLUMN notification.id; Type: COMMENT; Schema: vibetype_private; Owner: ci --- - -COMMENT ON COLUMN vibetype_private.notification.id IS 'The notification''s internal id.'; - - --- --- Name: COLUMN notification.channel; Type: COMMENT; Schema: vibetype_private; Owner: ci --- - -COMMENT ON COLUMN vibetype_private.notification.channel IS 'The notification''s channel.'; - - --- --- Name: COLUMN notification.is_acknowledged; Type: COMMENT; Schema: vibetype_private; Owner: ci --- - -COMMENT ON COLUMN vibetype_private.notification.is_acknowledged IS 'Whether the notification was acknowledged.'; - - --- --- Name: COLUMN notification.payload; Type: COMMENT; Schema: vibetype_private; Owner: ci +-- Name: notification_invitation id; Type: DEFAULT; Schema: vibetype; Owner: ci -- -COMMENT ON COLUMN vibetype_private.notification.payload IS 'The notification''s payload.'; +ALTER TABLE ONLY vibetype.notification_invitation ALTER COLUMN id SET DEFAULT gen_random_uuid(); -- --- Name: COLUMN notification.created_at; Type: COMMENT; Schema: vibetype_private; Owner: ci +-- Name: notification_invitation created_at; Type: DEFAULT; Schema: vibetype; Owner: ci -- -COMMENT ON COLUMN vibetype_private.notification.created_at IS 'The timestamp of the notification''s creation.'; +ALTER TABLE ONLY vibetype.notification_invitation ALTER COLUMN created_at SET DEFAULT CURRENT_TIMESTAMP; -- @@ -5315,6 +5390,14 @@ ALTER TABLE ONLY vibetype.legal_term ADD CONSTRAINT legal_term_pkey PRIMARY KEY (id); +-- +-- Name: notification notification_pkey; Type: CONSTRAINT; Schema: vibetype; Owner: ci +-- + +ALTER TABLE ONLY vibetype.notification + ADD CONSTRAINT notification_pkey PRIMARY KEY (id); + + -- -- Name: preference_event_category preference_event_category_account_id_category_id_key; Type: CONSTRAINT; Schema: vibetype; Owner: ci -- @@ -5498,14 +5581,6 @@ ALTER TABLE ONLY vibetype_private.jwt ADD CONSTRAINT jwt_token_key UNIQUE (token); --- --- Name: notification notification_pkey; Type: CONSTRAINT; Schema: vibetype_private; Owner: ci --- - -ALTER TABLE ONLY vibetype_private.notification - ADD CONSTRAINT notification_pkey PRIMARY KEY (id); - - -- -- Name: idx_address_created_by; Type: INDEX; Schema: vibetype; Owner: ci -- @@ -5632,6 +5707,20 @@ CREATE INDEX idx_guest_updated_by ON vibetype.guest USING btree (updated_by); COMMENT ON INDEX vibetype.idx_guest_updated_by IS 'B-Tree index to optimize lookups by updater.'; +-- +-- Name: idx_invitation_guest_id; Type: INDEX; Schema: vibetype; Owner: ci +-- + +CREATE INDEX idx_invitation_guest_id ON vibetype.notification_invitation USING btree (guest_id); + + +-- +-- Name: idx_notification_created_by; Type: INDEX; Schema: vibetype; Owner: ci +-- + +CREATE INDEX idx_notification_created_by ON vibetype.notification USING btree (created_by); + + -- -- Name: idx_account_private_location; Type: INDEX; Schema: vibetype_private; Owner: ci -- @@ -6042,6 +6131,22 @@ ALTER TABLE ONLY vibetype.legal_term_acceptance ADD CONSTRAINT legal_term_acceptance_legal_term_id_fkey FOREIGN KEY (legal_term_id) REFERENCES vibetype.legal_term(id) ON DELETE RESTRICT; +-- +-- Name: notification notification_created_by_fkey; Type: FK CONSTRAINT; Schema: vibetype; Owner: ci +-- + +ALTER TABLE ONLY vibetype.notification + ADD CONSTRAINT notification_created_by_fkey FOREIGN KEY (created_by) REFERENCES vibetype.account(id) ON DELETE CASCADE; + + +-- +-- Name: notification_invitation notification_invitation_guest_id_fkey; Type: FK CONSTRAINT; Schema: vibetype; Owner: ci +-- + +ALTER TABLE ONLY vibetype.notification_invitation + ADD CONSTRAINT notification_invitation_guest_id_fkey FOREIGN KEY (guest_id) REFERENCES vibetype.guest(id); + + -- -- Name: preference_event_category preference_event_category_account_id_fkey; Type: FK CONSTRAINT; Schema: vibetype; Owner: ci -- @@ -6532,6 +6637,25 @@ CREATE POLICY legal_term_acceptance_all ON vibetype.legal_term_acceptance USING CREATE POLICY legal_term_select ON vibetype.legal_term FOR SELECT USING (true); +-- +-- Name: notification; Type: ROW SECURITY; Schema: vibetype; Owner: ci +-- + +ALTER TABLE vibetype.notification ENABLE ROW LEVEL SECURITY; + +-- +-- Name: notification notification_all; Type: POLICY; Schema: vibetype; Owner: ci +-- + +CREATE POLICY notification_all ON vibetype.notification USING ((created_by = vibetype.invoker_account_id())); + + +-- +-- Name: notification_invitation; Type: ROW SECURITY; Schema: vibetype; Owner: ci +-- + +ALTER TABLE vibetype.notification_invitation ENABLE ROW LEVEL SECURITY; + -- -- Name: preference_event_category; Type: ROW SECURITY; Schema: vibetype; Owner: ci -- @@ -7494,6 +7618,21 @@ GRANT SELECT ON TABLE vibetype.legal_term TO vibetype_anonymous; GRANT SELECT,INSERT ON TABLE vibetype.legal_term_acceptance TO vibetype_account; +-- +-- Name: TABLE notification; Type: ACL; Schema: vibetype; Owner: ci +-- + +GRANT SELECT ON TABLE vibetype.notification TO vibetype_account; +GRANT SELECT ON TABLE vibetype.notification TO grafana; + + +-- +-- Name: TABLE notification_invitation; Type: ACL; Schema: vibetype; Owner: ci +-- + +GRANT SELECT ON TABLE vibetype.notification_invitation TO vibetype_account; + + -- -- Name: TABLE preference_event_category; Type: ACL; Schema: vibetype; Owner: ci -- @@ -7561,13 +7700,6 @@ GRANT SELECT ON TABLE vibetype_private.account TO grafana; GRANT SELECT ON TABLE vibetype_private.achievement_code TO vibetype; --- --- Name: TABLE notification; Type: ACL; Schema: vibetype_private; Owner: ci --- - -GRANT SELECT ON TABLE vibetype_private.notification TO grafana; - - -- -- Name: DEFAULT PRIVILEGES FOR FUNCTIONS; Type: DEFAULT ACL; Schema: -; Owner: ci -- diff --git a/test/logic/main.sql b/test/logic/main.sql index be97bbca..cc98bc35 100644 --- a/test/logic/main.sql +++ b/test/logic/main.sql @@ -45,7 +45,7 @@ GRANT USAGE ON SCHEMA vibetype_test TO vibetype_anonymous, vibetype_account; \i scenario/model/event.sql \i scenario/model/friendship.sql \i scenario/model/guest.sql --- \i scenario/model/invite.sql -- TODO: remove comment when PR "feat(notification)!: inherit invitations" has been merged +\i scenario/model/invite.sql \i scenario/model/language_iso_full_text_search.sql \echo all tests completed sucessfully. diff --git a/test/logic/scenario/model/account_registration.sql b/test/logic/scenario/model/account_registration.sql index 5ebb7792..f474f8bb 100644 --- a/test/logic/scenario/model/account_registration.sql +++ b/test/logic/scenario/model/account_registration.sql @@ -117,7 +117,6 @@ BEGIN PERFORM vibetype_test.invoker_set(_account_id); -/* TODO: remove comments when PR "feat(notification)!: inherit invitations" has been merged IF NOT EXISTS ( SELECT 1 FROM vibetype.notification WHERE channel = 'account_registration' @@ -125,7 +124,6 @@ BEGIN ) THEN RAISE EXCEPTION 'Test failed: Notification not generated'; END IF; -*/ PERFORM vibetype_test.invoker_set_empty();