Eerste commit

This commit is contained in:
jf
2026-09-27 14:21:31 +00:00
commit de4ff128cb
295 changed files with 47374 additions and 0 deletions
+368
View File
@@ -0,0 +1,368 @@
-- ============================================================================
-- MijnBesluit - databaseschema voor MariaDB 10.6 of nieuwer
--
-- Aanmaken (in cmd, NIET in PowerShell - die kent "<" niet):
-- mysql -u root -p < 01_schema.sql
-- In PowerShell of HeidiSQL: zie docs/Website-testen.md.
--
-- Het script mag meerdere keren gedraaid worden; bestaande tabellen blijven ongemoeid.
--
-- De schemanaam staat hieronder één keer. Wil je een bestaande database
-- gebruiken, vervang dan `mijnbesluit` overal (of laat CREATE DATABASE weg
-- en zet er een USE <jouw naam>; voor in de plaats).
--
-- Opzet:
-- * Alles utf8mb4 en InnoDB, zodat namen met accenten en emoji goed gaan.
-- * Geheimen staan als VARBINARY in de tabel: versleuteld met AES-256-GCM,
-- nooit leesbaar. Zie docs/Beveiliging.md voor hoe de sleutels lopen.
-- * Datums worden in UTC bewaard (DATETIME, geen TIMESTAMP), zodat zomertijd
-- nooit een bezorgmoment kan verschuiven.
-- ============================================================================
CREATE DATABASE IF NOT EXISTS `mijnbesluit`
DEFAULT CHARACTER SET utf8mb4
DEFAULT COLLATE utf8mb4_unicode_ci;
USE `mijnbesluit`;
SET FOREIGN_KEY_CHECKS = 0;
-- ---------------------------------------------------------------- personen ---
-- Een profiel. Kan bestaan zonder account: dan is het aangemaakt door iemand
-- anders en wacht het op een uitnodiging.
CREATE TABLE IF NOT EXISTS `persons` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`first_name` VARCHAR(80) NOT NULL DEFAULT '',
`last_name` VARCHAR(80) NOT NULL DEFAULT '',
`nickname` VARCHAR(80) NULL,
`email` VARCHAR(255) NULL,
`mobile_number` VARCHAR(50) NULL,
`birth_date` DATE NULL,
`date_of_death` DATE NULL,
-- Wie het profiel aanmaakte (historie) en wie het nu beheert.
`created_by_user_id` BIGINT NULL,
`managed_by_user_id` BIGINT NULL,
-- Zodra dit gevuld is, is het profiel van de persoon zelf.
`claimed_by_user_id` BIGINT NULL,
`claimed_at` DATETIME(3) NULL,
-- Aantal meldingen dat nodig is om overlijden te bevestigen, en hoeveel dagen
-- de betrokkene daarna nog heeft om bezwaar te maken.
`required_death_declarations` INT NOT NULL DEFAULT 3,
`death_objection_days` INT NOT NULL DEFAULT 14,
`death_confirmed_at` DATETIME(3) NULL,
`death_objected_at` DATETIME(3) NULL,
-- Wie heeft gezegd dat deze aantekening niet over hem gaat. Zie db/17_aantekening_afgewezen.sql:
-- dan komt die vraag niet bij elke volgende keer opnieuw.
`link_declined_by_user_id` BIGINT NULL,
`link_declined_at` DATETIME(3) NULL,
`created_at` DATETIME(3) NOT NULL,
`updated_at` DATETIME(3) NOT NULL,
PRIMARY KEY (`id`),
-- Met opzet NIET uniek. Twee mensen mogen allebei hun eigen aantekening over dezelfde
-- persoon hebben; ze worden pas één zodra die persoon zelf beide uitnodigingen aanneemt.
-- Zie docs/Identiteit.md en db/15_adres_niet_uniek.sql. Bij `users.email` is uniek juist
-- wél goed: dat is een account.
KEY `ix_persons_email` (`email`),
KEY `ix_persons_claimed_by` (`claimed_by_user_id`),
KEY `ix_persons_managed_by` (`managed_by_user_id`),
KEY `ix_persons_death` (`death_confirmed_at`)
) ENGINE=InnoDB;
-- ---------------------------------------------------------------- accounts ---
CREATE TABLE IF NOT EXISTS `users` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`person_id` BIGINT NOT NULL,
`email` VARCHAR(255) NOT NULL,
`security_stamp` CHAR(36) NOT NULL,
-- De datasleutel van deze gebruiker, versleuteld met de mastersleutel.
`wrapped_data_key` VARBINARY(255) NOT NULL,
`status` TINYINT NOT NULL DEFAULT 0, -- 0 actief, 1 geblokkeerd, 2 overleden
`email_confirmed_at` DATETIME(3) NULL,
`two_factor_enabled` TINYINT(1) NOT NULL DEFAULT 0,
`failed_login_count` INT NOT NULL DEFAULT 0,
`locked_until` DATETIME(3) NULL,
`last_login_at` DATETIME(3) NULL,
`created_at` DATETIME(3) NOT NULL,
`updated_at` DATETIME(3) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `ux_users_email` (`email`),
UNIQUE KEY `ux_users_person` (`person_id`),
CONSTRAINT `fk_users_person` FOREIGN KEY (`person_id`) REFERENCES `persons` (`id`)
) ENGINE=InnoDB;
-- `persons` en `users` verwijzen naar elkaar, dus deze twee sleutels kunnen pas gelegd worden
-- als beide tabellen bestaan. ALTER TABLE kent geen "IF NOT EXISTS" voor een constraint, dus
-- wordt er eerst gekeken of hij er al ligt: zo kun je dit script gerust nog een keer draaien.
SET @fk_claimed := (SELECT COUNT(*) FROM information_schema.TABLE_CONSTRAINTS
WHERE CONSTRAINT_SCHEMA = DATABASE()
AND TABLE_NAME = 'persons'
AND CONSTRAINT_NAME = 'fk_persons_claimed_by');
SET @sql := IF(@fk_claimed = 0,
'ALTER TABLE `persons` ADD CONSTRAINT `fk_persons_claimed_by` FOREIGN KEY (`claimed_by_user_id`) REFERENCES `users` (`id`) ON DELETE SET NULL',
'DO 0');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @fk_managed := (SELECT COUNT(*) FROM information_schema.TABLE_CONSTRAINTS
WHERE CONSTRAINT_SCHEMA = DATABASE()
AND TABLE_NAME = 'persons'
AND CONSTRAINT_NAME = 'fk_persons_managed_by');
SET @sql := IF(@fk_managed = 0,
'ALTER TABLE `persons` ADD CONSTRAINT `fk_persons_managed_by` FOREIGN KEY (`managed_by_user_id`) REFERENCES `users` (`id`) ON DELETE SET NULL',
'DO 0');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- ---------------------------------------------------------------- families ---
CREATE TABLE IF NOT EXISTS `families` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`name` VARCHAR(120) NOT NULL,
`owner_person_id` BIGINT NOT NULL,
`is_active` TINYINT(1) NOT NULL DEFAULT 1,
`created_at` DATETIME(3) NOT NULL,
`updated_at` DATETIME(3) NOT NULL,
PRIMARY KEY (`id`),
KEY `ix_families_owner` (`owner_person_id`),
CONSTRAINT `fk_families_owner` FOREIGN KEY (`owner_person_id`) REFERENCES `persons` (`id`)
) ENGINE=InnoDB;
CREATE TABLE IF NOT EXISTS `family_members` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`family_id` BIGINT NOT NULL,
`person_id` BIGINT NOT NULL,
`relation` INT NOT NULL DEFAULT 0,
`relation_label` VARCHAR(50) NULL,
`added_by_user_id` BIGINT NULL,
`created_at` DATETIME(3) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `ux_family_member` (`family_id`, `person_id`),
KEY `ix_family_members_person` (`person_id`),
CONSTRAINT `fk_fm_family` FOREIGN KEY (`family_id`) REFERENCES `families` (`id`) ON DELETE CASCADE,
CONSTRAINT `fk_fm_person` FOREIGN KEY (`person_id`) REFERENCES `persons` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB;
-- ------------------------------------------------------------ uitnodigingen ---
-- Alleen de hash van het token staat hier; het token zelf bestaat enkel in de e-mail.
CREATE TABLE IF NOT EXISTS `invitations` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`person_id` BIGINT NOT NULL,
`family_id` BIGINT NOT NULL,
`email` VARCHAR(255) NOT NULL,
`token_hash` VARBINARY(32) NOT NULL,
`status` INT NOT NULL DEFAULT 0,
`created_by_user_id` BIGINT NOT NULL,
`created_at` DATETIME(3) NOT NULL,
`sent_at` DATETIME(3) NULL,
`expires_at` DATETIME(3) NOT NULL,
`accepted_at` DATETIME(3) NULL,
`resend_count` INT NOT NULL DEFAULT 0,
PRIMARY KEY (`id`),
UNIQUE KEY `ux_invitations_token` (`token_hash`),
KEY `ix_invitations_person` (`person_id`, `status`),
CONSTRAINT `fk_inv_person` FOREIGN KEY (`person_id`) REFERENCES `persons` (`id`) ON DELETE CASCADE,
CONSTRAINT `fk_inv_family` FOREIGN KEY (`family_id`) REFERENCES `families` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB;
-- ------------------------------------------------------------------- kluis ---
-- Bijlagen: de bytes staan versleuteld buiten de database, hier alleen de verwijzing.
CREATE TABLE IF NOT EXISTS `vault_files` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`owner_person_id` BIGINT NOT NULL,
`file_name` VARCHAR(255) NOT NULL,
`content_type` VARCHAR(120) NOT NULL DEFAULT 'application/octet-stream',
`size_bytes` BIGINT NOT NULL DEFAULT 0,
`storage_key` VARCHAR(255) NOT NULL,
`nonce` VARBINARY(12) NOT NULL,
`tag` VARBINARY(16) NOT NULL,
`wrapped_content_key` VARBINARY(96) NOT NULL,
`sha256` VARBINARY(32) NOT NULL,
`created_at` DATETIME(3) NOT NULL,
PRIMARY KEY (`id`),
KEY `ix_vault_files_owner` (`owner_person_id`),
CONSTRAINT `fk_vf_owner` FOREIGN KEY (`owner_person_id`) REFERENCES `persons` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB;
-- Kluisitems. `title` en `category` zijn leesbaar zodat er gezocht kan worden;
-- de inhoud zit versleuteld in `ciphertext`.
CREATE TABLE IF NOT EXISTS `vault_items` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`owner_person_id` BIGINT NOT NULL,
`category` INT NOT NULL,
`title` VARCHAR(150) NOT NULL,
`ciphertext` LONGBLOB NOT NULL,
`nonce` VARBINARY(12) NOT NULL,
`tag` VARBINARY(16) NOT NULL,
`wrapped_content_key` VARBINARY(96) NOT NULL,
`release_policy` INT NOT NULL DEFAULT 0, -- 0 alleen ikzelf, 1 na overlijden, 2 op datum
`release_at` DATETIME(3) NULL,
`vault_file_id` BIGINT NULL,
`created_at` DATETIME(3) NOT NULL,
`updated_at` DATETIME(3) NOT NULL,
`deleted_at` DATETIME(3) NULL,
PRIMARY KEY (`id`),
KEY `ix_vault_items_owner` (`owner_person_id`, `category`),
KEY `ix_vault_items_release` (`release_policy`, `release_at`),
CONSTRAINT `fk_vi_owner` FOREIGN KEY (`owner_person_id`) REFERENCES `persons` (`id`) ON DELETE CASCADE,
CONSTRAINT `fk_vi_file` FOREIGN KEY (`vault_file_id`) REFERENCES `vault_files` (`id`) ON DELETE SET NULL
) ENGINE=InnoDB;
-- Toegang van een ander tot een item: dezelfde contentsleutel, ingepakt met
-- de datasleutel van die ander. Zonder rij hier is er geen toegang, punt.
CREATE TABLE IF NOT EXISTS `content_grants` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`resource_type` VARCHAR(30) NOT NULL, -- VaultItem | VaultFile | Message
`resource_id` BIGINT NOT NULL,
`user_id` BIGINT NOT NULL,
`wrapped_content_key` VARBINARY(96) NOT NULL,
`reason` VARCHAR(50) NOT NULL, -- ontvanger | nalatenschap | gedeeld
`granted_at` DATETIME(3) NOT NULL,
`revoked_at` DATETIME(3) NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `ux_grant` (`resource_type`, `resource_id`, `user_id`),
KEY `ix_grants_user` (`user_id`),
CONSTRAINT `fk_grant_user` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB;
-- ---------------------------------------------------------------- berichten ---
CREATE TABLE IF NOT EXISTS `messages` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`from_person_id` BIGINT NOT NULL,
`to_person_id` BIGINT NOT NULL,
`kind` INT NOT NULL DEFAULT 0, -- 0 tekst, 1 video, 2 audio, 3 document
`subject` VARCHAR(200) NOT NULL,
`ciphertext` LONGBLOB NOT NULL,
`nonce` VARBINARY(12) NOT NULL,
`tag` VARBINARY(16) NOT NULL,
`wrapped_content_key` VARBINARY(96) NOT NULL,
`vault_file_id` BIGINT NULL,
`deliver_at` DATETIME(3) NULL,
`deliver_after_death` TINYINT(1) NOT NULL DEFAULT 0,
`status` INT NOT NULL DEFAULT 0, -- 0 concept, 1 gepland, 2 bezorgd, 3 gelezen, 4 ingetrokken
`delivered_at` DATETIME(3) NULL,
`read_at` DATETIME(3) NULL,
`created_at` DATETIME(3) NOT NULL,
`updated_at` DATETIME(3) NOT NULL,
PRIMARY KEY (`id`),
KEY `ix_messages_to` (`to_person_id`, `status`),
KEY `ix_messages_from` (`from_person_id`),
-- De index waarop de bezorgtaak draait.
KEY `ix_messages_due` (`status`, `deliver_at`, `deliver_after_death`),
CONSTRAINT `fk_msg_from` FOREIGN KEY (`from_person_id`) REFERENCES `persons` (`id`) ON DELETE CASCADE,
CONSTRAINT `fk_msg_to` FOREIGN KEY (`to_person_id`) REFERENCES `persons` (`id`) ON DELETE CASCADE,
CONSTRAINT `fk_msg_file` FOREIGN KEY (`vault_file_id`) REFERENCES `vault_files` (`id`) ON DELETE SET NULL
) ENGINE=InnoDB;
-- ------------------------------------------------------- overlijden & nalatenschap ---
-- Eén melding per gebruiker per profiel; dat dwingt de unieke sleutel af.
CREATE TABLE IF NOT EXISTS `death_declarations` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`person_id` BIGINT NOT NULL,
`declared_by_user_id` BIGINT NOT NULL,
`date_of_death` DATE NULL,
`note` VARCHAR(500) NULL,
`declared_at` DATETIME(3) NOT NULL,
`revoked_at` DATETIME(3) NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `ux_declaration` (`person_id`, `declared_by_user_id`),
CONSTRAINT `fk_dd_person` FOREIGN KEY (`person_id`) REFERENCES `persons` (`id`) ON DELETE CASCADE,
CONSTRAINT `fk_dd_user` FOREIGN KEY (`declared_by_user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB;
CREATE TABLE IF NOT EXISTS `legacy_managers` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`person_id` BIGINT NOT NULL,
`manager_person_id` BIGINT NOT NULL,
`priority` INT NOT NULL DEFAULT 1,
`accepted_at` DATETIME(3) NULL,
`access_granted_at` DATETIME(3) NULL,
`created_at` DATETIME(3) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `ux_legacy_manager` (`person_id`, `manager_person_id`),
CONSTRAINT `fk_lm_person` FOREIGN KEY (`person_id`) REFERENCES `persons` (`id`) ON DELETE CASCADE,
CONSTRAINT `fk_lm_manager` FOREIGN KEY (`manager_person_id`) REFERENCES `persons` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB;
-- ------------------------------------------------------------------ sessies ---
CREATE TABLE IF NOT EXISTS `refresh_tokens` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`user_id` BIGINT NOT NULL,
`token_hash` VARBINARY(32) NOT NULL,
`device` VARCHAR(120) NULL,
`created_by_ip` VARCHAR(45) NULL,
`created_at` DATETIME(3) NOT NULL,
`expires_at` DATETIME(3) NOT NULL,
`revoked_at` DATETIME(3) NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `ux_refresh_token` (`token_hash`),
KEY `ix_refresh_user` (`user_id`, `expires_at`),
CONSTRAINT `fk_rt_user` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB;
-- ---------------------------------------------------------------- sessies ---
-- Een aanmelding van de app. Staat in de database en niet in het geheugen van de SocketServer,
-- want anders logt elke herstart van de service iedereen uit - en dan is de vingerafdruk waarmee
-- je de app opent niets waard. Het token zelf staat er niet in, alleen de SHA-256 ervan.
CREATE TABLE IF NOT EXISTS `sessies` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`token_hash` VARBINARY(32) NOT NULL,
`user_id` BIGINT NOT NULL,
`person_id` BIGINT NOT NULL,
`toestel` VARCHAR(120) NULL,
`aangemaakt_op` DATETIME(3) NOT NULL,
`verloopt_op` DATETIME(3) NOT NULL,
`laatst_gezien` DATETIME(3) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `ux_sessies_token` (`token_hash`),
KEY `ix_sessies_verloopt` (`verloopt_op`),
CONSTRAINT `fk_sessies_user` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB;
-- ------------------------------------------------------------- inlogcodes ---
-- Er zijn geen wachtwoorden: je logt in met een code die per e-mail komt.
-- De code staat hier niet leesbaar in, alleen de hash ervan.
CREATE TABLE IF NOT EXISTS `login_codes` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`email` VARCHAR(255) NOT NULL,
`purpose` INT NOT NULL DEFAULT 0, -- 0 inloggen, 1 registreren
`code_hash` VARCHAR(255) NOT NULL,
`first_name` VARCHAR(80) NULL, -- alleen bij registreren
`last_name` VARCHAR(80) NULL,
`created_at` DATETIME(3) NOT NULL,
`expires_at` DATETIME(3) NOT NULL,
`consumed_at` DATETIME(3) NULL,
`attempts` INT NOT NULL DEFAULT 0,
`requested_from_ip` VARCHAR(45) NULL,
PRIMARY KEY (`id`),
-- De index waarop het opzoeken van een openstaande code draait.
KEY `ix_login_codes_open` (`email`, `purpose`, `expires_at`),
KEY `ix_login_codes_created` (`email`, `created_at`)
) ENGINE=InnoDB;
-- ----------------------------------------------------------------- logboek ---
-- Alleen toevoegen, nooit wijzigen. Bij een kluis wil je kunnen nagaan wie wat wanneer deed.
CREATE TABLE IF NOT EXISTS `audit_log` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`user_id` BIGINT NULL,
`action` INT NOT NULL,
`entity_type` VARCHAR(50) NULL,
`entity_id` BIGINT NULL,
`ip_address` VARCHAR(45) NULL,
`user_agent` VARCHAR(255) NULL,
`details` VARCHAR(1000) NULL,
`created_at` DATETIME(3) NOT NULL,
PRIMARY KEY (`id`),
KEY `ix_audit_user_time` (`user_id`, `created_at`),
KEY `ix_audit_entity` (`entity_type`, `entity_id`)
) ENGINE=InnoDB;
SET FOREIGN_KEY_CHECKS = 1;
+63
View File
@@ -0,0 +1,63 @@
-- ============================================================================
-- Een aparte databasegebruiker voor de applicatie, met alleen de rechten die
-- hij nodig heeft: geen CREATE, geen DROP, geen ALTER. Schemawijzigingen doe
-- je met een migratie onder je eigen account, niet vanuit de draaiende
-- applicatie.
--
-- Vervang het wachtwoord hieronder en bewaar het niet in de broncode --
-- zie docs/Beveiliging.md, "Waar de geheimen staan".
--
-- Draaien (in cmd, NIET in PowerShell):
-- mysql -u root -p < 02_applicatiegebruiker.sql
--
-- Opnieuw draaien mag en is veilig: CREATE USER IF NOT EXISTS slaat de hele regel over als de
-- gebruiker al bestaat, dus het wachtwoord verandert niet. Dat is precies wat je doet als er
-- tabellen bij zijn gekomen.
--
-- LET OP: komt er een tabel bij, dan hoort hij hier ook bij te staan. Zonder recht krijgt de
-- applicatie "SELECT command denied" - en dat merk je pas als iemand dat scherm opent. Er is
-- een zelftest die elke CREATE TABLE in deze map naast deze lijst legt.
--
-- En hier, in DIT bestand, en nergens anders. Sommige migratiescripts deelden vroeger zelf
-- rechten uit, vlak onder hun CREATE TABLE. Dat leest prettig, maar de scripts vanaf 03 worden
-- ook door de uitrolagent gedraaid met een beperkt databaseaccount, en dat mag geen rechten
-- uitdelen: de uitrol struikelde er dan op. Rechten uitdelen is inrichten, en dat doet een mens
-- één keer als root. De zelftest bewaakt ook dat.
-- ============================================================================
CREATE USER IF NOT EXISTS 'mijnbesluit_app'@'localhost'
IDENTIFIED BY 'VERVANG_DIT_WACHTWOORD';
-- De rechten worden per tabel gegeven en niet in één keer op de hele database.
-- Dat is met opzet: rechten in MariaDB zijn optellend, dus een recht dat op
-- databaseniveau is gegeven kun je op tabelniveau niet meer afnemen. Wil je
-- het logboek onwijzigbaar houden, dan mag UPDATE/DELETE op de database dus
-- nooit gegeven worden.
GRANT SELECT, INSERT, UPDATE, DELETE ON `mijnbesluit`.`persons` TO 'mijnbesluit_app'@'localhost';
GRANT SELECT, INSERT, UPDATE, DELETE ON `mijnbesluit`.`users` TO 'mijnbesluit_app'@'localhost';
GRANT SELECT, INSERT, UPDATE, DELETE ON `mijnbesluit`.`families` TO 'mijnbesluit_app'@'localhost';
GRANT SELECT, INSERT, UPDATE, DELETE ON `mijnbesluit`.`family_members` TO 'mijnbesluit_app'@'localhost';
GRANT SELECT, INSERT, UPDATE, DELETE ON `mijnbesluit`.`invitations` TO 'mijnbesluit_app'@'localhost';
GRANT SELECT, INSERT, UPDATE, DELETE ON `mijnbesluit`.`vault_items` TO 'mijnbesluit_app'@'localhost';
GRANT SELECT, INSERT, UPDATE, DELETE ON `mijnbesluit`.`vault_files` TO 'mijnbesluit_app'@'localhost';
GRANT SELECT, INSERT, UPDATE, DELETE ON `mijnbesluit`.`content_grants` TO 'mijnbesluit_app'@'localhost';
GRANT SELECT, INSERT, UPDATE, DELETE ON `mijnbesluit`.`messages` TO 'mijnbesluit_app'@'localhost';
GRANT SELECT, INSERT, UPDATE, DELETE ON `mijnbesluit`.`death_declarations` TO 'mijnbesluit_app'@'localhost';
GRANT SELECT, INSERT, UPDATE, DELETE ON `mijnbesluit`.`legacy_managers` TO 'mijnbesluit_app'@'localhost';
GRANT SELECT, INSERT, UPDATE, DELETE ON `mijnbesluit`.`refresh_tokens` TO 'mijnbesluit_app'@'localhost';
GRANT SELECT, INSERT, UPDATE, DELETE ON `mijnbesluit`.`login_codes` TO 'mijnbesluit_app'@'localhost';
GRANT SELECT, INSERT, UPDATE, DELETE ON `mijnbesluit`.`toestellen` TO 'mijnbesluit_app'@'localhost';
GRANT SELECT, INSERT, UPDATE, DELETE ON `mijnbesluit`.`samenvoegingen` TO 'mijnbesluit_app'@'localhost';
GRANT SELECT, INSERT, UPDATE, DELETE ON `mijnbesluit`.`adverteerders` TO 'mijnbesluit_app'@'localhost';
GRANT SELECT, INSERT, UPDATE, DELETE ON `mijnbesluit`.`campagnes` TO 'mijnbesluit_app'@'localhost';
GRANT SELECT, INSERT, UPDATE, DELETE ON `mijnbesluit`.`campagne_uren` TO 'mijnbesluit_app'@'localhost';
GRANT SELECT, INSERT, UPDATE, DELETE ON `mijnbesluit`.`sessies` TO 'mijnbesluit_app'@'localhost';
-- Het logboek mag er alleen bij komen en uit gelezen worden, niet uit verdwijnen.
GRANT SELECT, INSERT ON `mijnbesluit`.`audit_log` TO 'mijnbesluit_app'@'localhost';
FLUSH PRIVILEGES;
-- Controle: audit_log hoort alleen SELECT en INSERT te hebben, de rest alles behalve DDL.
SHOW GRANTS FOR 'mijnbesluit_app'@'localhost';
+58
View File
@@ -0,0 +1,58 @@
-- ============================================================================
-- Overstap van wachtwoorden naar inloggen met een code per e-mail.
--
-- Draai dit op een database die met een eerdere versie van 01_schema.sql is
-- aangemaakt. Op een verse database is dit niet nodig: 01_schema.sql bevat
-- alles al.
--
-- Draaien (in cmd, NIET in PowerShell), of in HeidiSQL via
-- Bestand -> SQL-bestand uitvoeren:
-- mysql -u root -p < 03_inloggen_met_code.sql
--
-- Het script mag meerdere keren gedraaid worden.
-- ============================================================================
USE `mijnbesluit`;
-- 1. De tabel met eenmalige codes.
CREATE TABLE IF NOT EXISTS `login_codes` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`email` VARCHAR(255) NOT NULL,
`purpose` INT NOT NULL DEFAULT 0, -- 0 inloggen, 1 registreren
`code_hash` VARCHAR(255) NOT NULL,
`first_name` VARCHAR(80) NULL,
`last_name` VARCHAR(80) NULL,
`created_at` DATETIME(3) NOT NULL,
`expires_at` DATETIME(3) NOT NULL,
`consumed_at` DATETIME(3) NULL,
`attempts` INT NOT NULL DEFAULT 0,
`requested_from_ip` VARCHAR(45) NULL,
PRIMARY KEY (`id`),
KEY `ix_login_codes_open` (`email`, `purpose`, `expires_at`),
KEY `ix_login_codes_created` (`email`, `created_at`)
) ENGINE=InnoDB;
-- 2. De wachtwoordkolom weg. ALTER TABLE kent geen "DROP COLUMN IF EXISTS" in
-- elke MariaDB-versie, dus eerst kijken of hij er staat.
SET @kolom := (SELECT COUNT(*) FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'users'
AND COLUMN_NAME = 'password_hash');
SET @sql := IF(@kolom > 0, 'ALTER TABLE `users` DROP COLUMN `password_hash`', 'DO 0');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- 3. De rechten voor de applicatiegebruiker staan NIET hier maar in 02_applicatiegebruiker.sql.
--
-- Waarom: de scripts vanaf 03 worden ook door de uitrolagent gedraaid, met een beperkt
-- databaseaccount dat geen rechten mág uitdelen. Een GRANT hier laat die hele uitrol struikelen,
-- en dat account meer macht geven is de verkeerde kant op. Rechten uitdelen is inrichten; dat
-- doet een mens één keer als root. Een zelfcontrole legt elke CREATE TABLE in deze map naast de
-- lijst in 02, dus vergeten kan niet ongemerkt.
-- Controle.
SELECT 'login_codes' AS tabel,
(SELECT COUNT(*) FROM information_schema.TABLES
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'login_codes') AS bestaat,
(SELECT COUNT(*) FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'users' AND COLUMN_NAME = 'password_hash') AS wachtwoordkolom_nog_aanwezig;
+55
View File
@@ -0,0 +1,55 @@
-- MijnBesluit - bijlagen bij kluisitems
--
-- Voegt twee kolommen toe aan `vault_files`:
-- vault_item_id : bij welk kluisitem dit bestand hoort. Leeg bij een bestand dat op zichzelf
-- staat, zoals de opname bij een videobericht.
-- deleted_at : gevuld zodra het bestand verwijderd is.
--
-- Draaien met:
-- mysql -u root -p mijnbesluit < db\04_bijlagen.sql
--
-- Opnieuw draaien mag: er gebeurt dan niets.
USE `mijnbesluit`;
-- ---------------------------------------------------------------- vault_item_id
SET @bestaat := (
SELECT COUNT(*) FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'vault_files' AND COLUMN_NAME = 'vault_item_id');
SET @sql := IF(@bestaat = 0,
'ALTER TABLE `vault_files` ADD COLUMN `vault_item_id` BIGINT NULL AFTER `owner_person_id`',
'SELECT "vault_item_id bestaat al" AS melding');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- ---------------------------------------------------------------- deleted_at
SET @bestaat := (
SELECT COUNT(*) FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'vault_files' AND COLUMN_NAME = 'deleted_at');
SET @sql := IF(@bestaat = 0,
'ALTER TABLE `vault_files` ADD COLUMN `deleted_at` DATETIME(3) NULL',
'SELECT "deleted_at bestaat al" AS melding');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- ---------------------------------------------------------------- index + relatie
SET @bestaat := (
SELECT COUNT(*) FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'vault_files' AND INDEX_NAME = 'ix_vault_files_item');
SET @sql := IF(@bestaat = 0,
'CREATE INDEX `ix_vault_files_item` ON `vault_files` (`vault_item_id`)',
'SELECT "index bestaat al" AS melding');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- Verdwijnt het kluisitem, dan verdwijnt de koppeling; het bestand zelf ruimt de applicatie op.
SET @bestaat := (
SELECT COUNT(*) FROM information_schema.TABLE_CONSTRAINTS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'vault_files' AND CONSTRAINT_NAME = 'fk_vf_item');
SET @sql := IF(@bestaat = 0,
'ALTER TABLE `vault_files` ADD CONSTRAINT `fk_vf_item` FOREIGN KEY (`vault_item_id`) REFERENCES `vault_items` (`id`) ON DELETE SET NULL',
'SELECT "fk_vf_item bestaat al" AS melding');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SELECT 'Klaar: bijlagen toegevoegd aan vault_files.' AS melding;
+38
View File
@@ -0,0 +1,38 @@
-- MijnBesluit - toestellen voor push-meldingen
--
-- Elke app meldt zich met een kenmerk van Firebase (Android) of APNs (iOS). Daarmee kan de
-- SocketServer een melding sturen als er een bericht wordt bezorgd, ook als de app dicht is.
--
-- Draaien met:
-- mysql -u root -p mijnbesluit < db\05_meldingen.sql
--
-- Opnieuw draaien mag: er gebeurt dan niets.
USE `mijnbesluit`;
CREATE TABLE IF NOT EXISTS `toestellen` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`user_id` BIGINT NOT NULL,
-- Het kenmerk van Google is lang; 512 is ruim genoeg en past nog in een unieke index.
`kenmerk` VARCHAR(512) NOT NULL,
`platform` VARCHAR(20) NOT NULL DEFAULT 'android',
`naam` VARCHAR(120) NULL,
`aangemeld_op` DATETIME(3) NOT NULL,
`laatst_gezien` DATETIME(3) NOT NULL,
`afgemeld_op` DATETIME(3) NULL,
PRIMARY KEY (`id`),
-- Eén kenmerk hoort bij één toestel. Wisselt de gebruiker, dan verhuist de regel mee.
UNIQUE KEY `ux_toestellen_kenmerk` (`kenmerk`),
KEY `ix_toestellen_user` (`user_id`),
CONSTRAINT `fk_toestel_user` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB;
-- De rechten voor de applicatiegebruiker staan NIET hier maar in 02_applicatiegebruiker.sql.
--
-- Waarom: de scripts vanaf 03 worden ook door de uitrolagent gedraaid, met een beperkt
-- databaseaccount dat geen rechten mág uitdelen. Een GRANT hier laat die hele uitrol struikelen,
-- en dat account meer macht geven is de verkeerde kant op. Rechten uitdelen is inrichten; dat
-- doet een mens één keer als root. Een zelfcontrole legt elke CREATE TABLE in deze map naast de
-- lijst in 02, dus vergeten kan niet ongemerkt.
SELECT 'Klaar: tabel toestellen aangemaakt.' AS melding;
+22
View File
@@ -0,0 +1,22 @@
-- MijnBesluit - instelling voor het overzicht
--
-- Voegt aan `users` toe hoeveel dagen vooruit iemand op zijn overzicht wil zien wat eraan komt.
-- 0 betekent: niets tonen. Standaard 30, maximaal 60.
--
-- Draaien met:
-- mysql -u root -p mijnbesluit < db\06_vooruitblik.sql
--
-- Opnieuw draaien mag: er gebeurt dan niets.
USE `mijnbesluit`;
SET @bestaat := (
SELECT COUNT(*) FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'users' AND COLUMN_NAME = 'vooruitblik_dagen');
SET @sql := IF(@bestaat = 0,
'ALTER TABLE `users` ADD COLUMN `vooruitblik_dagen` INT NOT NULL DEFAULT 30',
'SELECT "vooruitblik_dagen bestaat al" AS melding');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SELECT 'Klaar: vooruitblik-instelling toegevoegd aan users.' AS melding;
+89
View File
@@ -0,0 +1,89 @@
-- MijnBesluit - foto's bij personen en berichten
--
-- Tot nu toe kon een bestand alleen bij een kluisitem horen. Er komen twee koppelingen bij,
-- plus een vinkje voor de profielfoto:
--
-- subject_person_id : bij welke persoon deze foto hoort. Let op het verschil met
-- owner_person_id: dat is wie de foto bewaart, dit is wie erop staat.
-- message_id : bij welk bericht deze foto hoort.
-- is_profile_photo : dit is de foto die bij de persoon op de kaart komt te staan.
-- Er is er hoogstens een per persoon; de applicatie bewaakt dat.
--
-- Draaien met:
-- mysql -u root -p mijnbesluit < db\07_fotos.sql
--
-- Opnieuw draaien mag: er gebeurt dan niets.
USE `mijnbesluit`;
-- ---------------------------------------------------------------- subject_person_id
SET @bestaat := (
SELECT COUNT(*) FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'vault_files' AND COLUMN_NAME = 'subject_person_id');
SET @sql := IF(@bestaat = 0,
'ALTER TABLE `vault_files` ADD COLUMN `subject_person_id` BIGINT NULL AFTER `vault_item_id`',
'SELECT "subject_person_id bestaat al" AS melding');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- ---------------------------------------------------------------- message_id
SET @bestaat := (
SELECT COUNT(*) FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'vault_files' AND COLUMN_NAME = 'message_id');
SET @sql := IF(@bestaat = 0,
'ALTER TABLE `vault_files` ADD COLUMN `message_id` BIGINT NULL AFTER `subject_person_id`',
'SELECT "message_id bestaat al" AS melding');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- ---------------------------------------------------------------- is_profile_photo
SET @bestaat := (
SELECT COUNT(*) FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'vault_files' AND COLUMN_NAME = 'is_profile_photo');
SET @sql := IF(@bestaat = 0,
'ALTER TABLE `vault_files` ADD COLUMN `is_profile_photo` TINYINT(1) NOT NULL DEFAULT 0',
'SELECT "is_profile_photo bestaat al" AS melding');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- ---------------------------------------------------------------- indexen
SET @bestaat := (
SELECT COUNT(*) FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'vault_files' AND INDEX_NAME = 'ix_vault_files_subject');
SET @sql := IF(@bestaat = 0,
'CREATE INDEX `ix_vault_files_subject` ON `vault_files` (`subject_person_id`)',
'SELECT "ix_vault_files_subject bestaat al" AS melding');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @bestaat := (
SELECT COUNT(*) FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'vault_files' AND INDEX_NAME = 'ix_vault_files_message');
SET @sql := IF(@bestaat = 0,
'CREATE INDEX `ix_vault_files_message` ON `vault_files` (`message_id`)',
'SELECT "ix_vault_files_message bestaat al" AS melding');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- ---------------------------------------------------------------- relaties
-- Verdwijnt de persoon of het bericht, dan verdwijnt de koppeling; het bestand zelf ruimt de
-- applicatie op, want de versleutelde inhoud staat buiten de database.
SET @bestaat := (
SELECT COUNT(*) FROM information_schema.TABLE_CONSTRAINTS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'vault_files' AND CONSTRAINT_NAME = 'fk_vf_subject');
SET @sql := IF(@bestaat = 0,
'ALTER TABLE `vault_files` ADD CONSTRAINT `fk_vf_subject` FOREIGN KEY (`subject_person_id`) REFERENCES `persons` (`id`) ON DELETE SET NULL',
'SELECT "fk_vf_subject bestaat al" AS melding');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @bestaat := (
SELECT COUNT(*) FROM information_schema.TABLE_CONSTRAINTS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'vault_files' AND CONSTRAINT_NAME = 'fk_vf_message');
SET @sql := IF(@bestaat = 0,
'ALTER TABLE `vault_files` ADD CONSTRAINT `fk_vf_message` FOREIGN KEY (`message_id`) REFERENCES `messages` (`id`) ON DELETE SET NULL',
'SELECT "fk_vf_message bestaat al" AS melding');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SELECT 'Klaar: foto''s bij personen en berichten toegevoegd aan vault_files.' AS melding;
+55
View File
@@ -0,0 +1,55 @@
-- MijnBesluit - kleine versie van een foto, om in lijstjes te tonen
--
-- Een profielfoto op de kaart van een persoon is een plaatje van hooguit veertig pixels. De
-- foto zelf is al gauw een paar honderd kilobyte, en tien personen in een lijst zijn dan een
-- paar megabyte - over een mobiele verbinding merk je dat meteen.
--
-- Daarom slaan we bij een foto meteen een kleine versie op. Die is versleuteld met dezelfde
-- sleutel als de foto zelf, dus er staat niets leesbaars in de database:
--
-- thumbnail : de kleine versie, versleuteld
-- thumbnail_nonce : het eenmalige getal van die versleuteling
-- thumbnail_tag : het echtheidskenmerk van die versleuteling
--
-- Alle drie leeg betekent: er is geen kleine versie. Dat is geen fout - bij een document of een
-- video is er niets te verkleinen, en bij foto's van voor deze wijziging is hij er nog niet.
-- Het scherm valt dan terug op de beginletters, precies zoals het zonder foto ook doet.
--
-- Draaien met:
-- mysql -u root -p mijnbesluit < db\08_thumbnails.sql
--
-- Opnieuw draaien mag: er gebeurt dan niets.
USE `mijnbesluit`;
-- ---------------------------------------------------------------- thumbnail
SET @bestaat := (
SELECT COUNT(*) FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'vault_files' AND COLUMN_NAME = 'thumbnail');
SET @sql := IF(@bestaat = 0,
'ALTER TABLE `vault_files` ADD COLUMN `thumbnail` BLOB NULL',
'SELECT "thumbnail bestaat al" AS melding');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- ---------------------------------------------------------------- thumbnail_nonce
SET @bestaat := (
SELECT COUNT(*) FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'vault_files' AND COLUMN_NAME = 'thumbnail_nonce');
SET @sql := IF(@bestaat = 0,
'ALTER TABLE `vault_files` ADD COLUMN `thumbnail_nonce` VARBINARY(12) NULL',
'SELECT "thumbnail_nonce bestaat al" AS melding');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- ---------------------------------------------------------------- thumbnail_tag
SET @bestaat := (
SELECT COUNT(*) FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'vault_files' AND COLUMN_NAME = 'thumbnail_tag');
SET @sql := IF(@bestaat = 0,
'ALTER TABLE `vault_files` ADD COLUMN `thumbnail_tag` VARBINARY(16) NULL',
'SELECT "thumbnail_tag bestaat al" AS melding');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SELECT 'Klaar. De kleine versies worden vanaf nu bij het opslaan van een foto meegemaakt.' AS melding;
+54
View File
@@ -0,0 +1,54 @@
-- MijnBesluit - profielen samenvoegen
--
-- Twee mensen kunnen allebei een profiel aanmaken voor dezelfde persoon. Jij voegt je dochter
-- toe, je ex-partner doet dat ook: twee rijen in `persons`, allebei een eerlijke aantekening
-- van degene die hem maakte. Dat is geen fout, zolang niemand het profiel heeft geclaimd.
--
-- Zodra zij beide uitnodigingen aanneemt, zegt zij zelf dat het om haar gaat. Dan gaat het ene
-- profiel op in het andere. Het duplicaat blijft staan - er hangt geschiedenis aan, en een
-- verwijderde rij is niet terug te halen - maar wijst voortaan naar waar hij in is opgegaan:
--
-- merged_into_person_id : het profiel waarin deze is opgegaan. Leeg = gewoon in gebruik.
--
-- Alles wat naar het duplicaat verwees is op dat moment verhuisd: lidmaatschappen, berichten,
-- foto's, uitnodigingen en rollen in een nalatenschap.
--
-- Draaien met:
-- mysql -u root -p mijnbesluit < db\09_samenvoegen.sql
--
-- Opnieuw draaien mag: er gebeurt dan niets.
USE `mijnbesluit`;
-- ---------------------------------------------------------------- merged_into_person_id
SET @bestaat := (
SELECT COUNT(*) FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'persons' AND COLUMN_NAME = 'merged_into_person_id');
SET @sql := IF(@bestaat = 0,
'ALTER TABLE `persons` ADD COLUMN `merged_into_person_id` BIGINT NULL',
'SELECT "merged_into_person_id bestaat al" AS melding');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- ---------------------------------------------------------------- index
-- Elke lijst met mensen filtert hierop, dus hij wordt vaak gelezen.
SET @bestaat := (
SELECT COUNT(*) FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'persons' AND INDEX_NAME = 'ix_persons_merged');
SET @sql := IF(@bestaat = 0,
'CREATE INDEX `ix_persons_merged` ON `persons` (`merged_into_person_id`)',
'SELECT "ix_persons_merged bestaat al" AS melding');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- ---------------------------------------------------------------- verwijzing naar zichzelf
SET @bestaat := (
SELECT COUNT(*) FROM information_schema.TABLE_CONSTRAINTS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'persons' AND CONSTRAINT_NAME = 'fk_persons_merged');
SET @sql := IF(@bestaat = 0,
'ALTER TABLE `persons` ADD CONSTRAINT `fk_persons_merged` FOREIGN KEY (`merged_into_person_id`) REFERENCES `persons` (`id`) ON DELETE SET NULL',
'SELECT "fk_persons_merged bestaat al" AS melding');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SELECT 'Klaar. Profielen kunnen vanaf nu worden samengevoegd.' AS melding;
+53
View File
@@ -0,0 +1,53 @@
-- MijnBesluit - bezwaar maken zonder in te loggen
--
-- Als er gemeld wordt dat je bent overleden, krijg je daar bericht van met de vraag of dat
-- klopt. Tot nu toe moest je inloggen om te zeggen dat je er nog bent. Dat gaat mis op precies
-- het moment dat het ertoe doet: zodra het overlijden bevestigd is, staat je account op
-- "overleden" en kun je niet meer inloggen. Dan is er geen weg terug.
--
-- Daarom gaat er voortaan een link mee in die e-mail, die werkt zonder inloggen - net als een
-- uitnodiging. Alleen de versleutelde vorm van dat kenmerk staat hier; de link zelf staat
-- uitsluitend in de e-mail.
--
-- objection_token_hash : SHA-256 van het kenmerk uit de link
-- objection_token_expires : tot wanneer die link werkt
--
-- Draaien met:
-- mysql -u root -p mijnbesluit < db\10_bezwaarlink.sql
--
-- Opnieuw draaien mag: er gebeurt dan niets.
USE `mijnbesluit`;
-- ---------------------------------------------------------------- objection_token_hash
SET @bestaat := (
SELECT COUNT(*) FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'persons' AND COLUMN_NAME = 'objection_token_hash');
SET @sql := IF(@bestaat = 0,
'ALTER TABLE `persons` ADD COLUMN `objection_token_hash` VARBINARY(32) NULL',
'SELECT "objection_token_hash bestaat al" AS melding');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- ---------------------------------------------------------------- objection_token_expires
SET @bestaat := (
SELECT COUNT(*) FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'persons' AND COLUMN_NAME = 'objection_token_expires');
SET @sql := IF(@bestaat = 0,
'ALTER TABLE `persons` ADD COLUMN `objection_token_expires` DATETIME(3) NULL',
'SELECT "objection_token_expires bestaat al" AS melding');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- ---------------------------------------------------------------- index
-- De link wordt opgezocht op dit kenmerk, dus zonder index is dat een tabelscan.
SET @bestaat := (
SELECT COUNT(*) FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'persons' AND INDEX_NAME = 'ix_persons_objection');
SET @sql := IF(@bestaat = 0,
'CREATE INDEX `ix_persons_objection` ON `persons` (`objection_token_hash`)',
'SELECT "ix_persons_objection bestaat al" AS melding');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SELECT 'Klaar. De waarschuwingsmail krijgt vanaf nu een bezwaarlink.' AS melding;
+37
View File
@@ -0,0 +1,37 @@
-- MijnBesluit - een beheerder
--
-- Er is één situatie waar niemand meer uit komt: een overlijden dat ten onrechte is bevestigd,
-- terwijl de bezwaarlink uit de waarschuwingsmail is verlopen. Het account staat dan op
-- "overleden", inloggen kan niet meer, en de enige weg terug is weg.
--
-- Daarvoor is een beheerder. Bewust zo klein mogelijk gehouden:
--
-- * een beheerder kan een profiel opzoeken op het VOLLEDIGE e-mailadres, niet op naam. Zoeken
-- op naam zou een zoekmachine op mensen zijn - zie docs/Identiteit.md.
-- * een beheerder kan een bevestigd overlijden terugdraaien, met een reden die wordt vastgelegd.
-- * een beheerder kan GEEN kluis openen. Die handeling geeft nooit toegang, hij neemt alleen
-- toegang weg.
--
-- Beheerder word je alleen hier, met de hand:
-- UPDATE `users` SET `is_beheerder` = 1 WHERE `email` = 'jij@voorbeeld.nl';
--
-- Er is met opzet geen scherm om iemand beheerder te maken. Wat niet via de applicatie kan,
-- kan ook niet via een lek in de applicatie.
--
-- Draaien met:
-- mysql -u root -p mijnbesluit < db\11_beheerder.sql
--
-- Opnieuw draaien mag: er gebeurt dan niets.
USE `mijnbesluit`;
SET @bestaat := (
SELECT COUNT(*) FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'users' AND COLUMN_NAME = 'is_beheerder');
SET @sql := IF(@bestaat = 0,
'ALTER TABLE `users` ADD COLUMN `is_beheerder` TINYINT(1) NOT NULL DEFAULT 0',
'SELECT "is_beheerder bestaat al" AS melding');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SELECT 'Klaar. Maak iemand beheerder met: UPDATE `users` SET `is_beheerder` = 1 WHERE `email` = ...' AS melding;
+51
View File
@@ -0,0 +1,51 @@
-- MijnBesluit - het bonnetje van een samenvoeging
--
-- Twee profielen van dezelfde persoon worden er één zodra zij zelf beide uitnodigingen aanneemt.
-- Tot nu toe was dat onomkeerbaar, en dat stond ook zo in docs/Identiteit.md bij "wat hier nog
-- niet is opgelost".
--
-- Waarom terugdraaien niet zomaar kan: na afloop is niet meer te zien wélke lidmaatschappen,
-- berichten en bestanden bij het duplicaat hoorden. Ze staan gewoon bij het doel, tussen alles
-- wat daar al stond. Gokken zou andermans gegevens meesleuren.
--
-- Daarom deze tabel. Bij elke samenvoeging komt hier één regel in te staan met precies wat er is
-- verhuisd, als JSON. Terugdraaien is dan die lijst in omgekeerde volgorde aflopen. Regels die
-- bij het samenvoegen zijn weggegooid - een dubbel lidmaatschap, een dubbele beheerder - staan
-- er als volledige kopie in, want die moeten opnieuw worden aangemaakt.
--
-- De regel blijft ook na het terugdraaien staan, met undone_at gevuld. Dat er ooit is
-- samengevoegd is zelf ook geschiedenis.
--
-- Let op: samenvoegingen van VOOR deze tabel hebben geen bonnetje en zijn dus niet terug te
-- draaien. Dat is geen fout die te repareren valt; de gegevens om het mee te doen bestaan niet.
--
-- Draaien met:
-- mysql -u root -p mijnbesluit < db\12_samenvoegingen.sql
--
-- Opnieuw draaien mag: er gebeurt dan niets.
USE `mijnbesluit`;
CREATE TABLE IF NOT EXISTS `samenvoegingen` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`duplicate_person_id` BIGINT NOT NULL,
`target_person_id` BIGINT NOT NULL,
`by_user_id` BIGINT NOT NULL,
`merged_at` DATETIME(6) NOT NULL,
`steps` LONGTEXT NOT NULL,
`undone_at` DATETIME(6) NULL,
`undone_by_user_id` BIGINT NULL,
`reason` VARCHAR(500) NULL,
PRIMARY KEY (`id`),
KEY `ix_samenvoegingen_duplicaat` (`duplicate_person_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- De rechten voor de applicatiegebruiker staan NIET hier maar in 02_applicatiegebruiker.sql.
--
-- Waarom: de scripts vanaf 03 worden ook door de uitrolagent gedraaid, met een beperkt
-- databaseaccount dat geen rechten mág uitdelen. Een GRANT hier laat die hele uitrol struikelen,
-- en dat account meer macht geven is de verkeerde kant op. Rechten uitdelen is inrichten; dat
-- doet een mens één keer als root. Een zelfcontrole legt elke CREATE TABLE in deze map naast de
-- lijst in 02, dus vergeten kan niet ongemerkt.
SELECT 'Klaar. Samenvoegingen worden vanaf nu vastgelegd en kunnen worden teruggedraaid.' AS melding;
+123
View File
@@ -0,0 +1,123 @@
-- ============================================================================
-- Adverteren op MijnBesluit: adverteerders, campagnes en tellers.
--
-- Lees docs/Adverteren.md voordat je hier iets aan verandert. Daar staat waarom de balk laag
-- is, hoe er wordt gekozen wie er getoond wordt, en waarom goedkeuring vooraf gaat.
--
-- Draaien met:
-- mysql -u root -p mijnbesluit < db\13_adverteren.sql
--
-- Opnieuw draaien mag: er gebeurt dan niets.
-- ============================================================================
USE `mijnbesluit`;
-- ------------------------------------------------------------ adverteerders ---
-- Een bedrijf dat adverteert. Bewust een eigen tabel en géén regel in `users`:
--
-- * Een adverteerder is een bedrijf, geen persoon met een kluis, familie of nalatenschap.
-- Hem in `users` zetten zou betekenen dat elke query over gebruikers hem moet uitzonderen,
-- en één vergeten uitzondering is er één te veel in een applicatie die over nalatenschappen
-- gaat.
-- * Het scheidt de twee werelden ook fysiek: wie in het adverteerdersdeel inbreekt, staat nog
-- steeds nergens bij de kluizen in de buurt.
--
-- Inloggen gaat net als bij gebruikers met een code per e-mail; die codes staan in dezelfde
-- tabel `login_codes`, met een eigen doel-nummer.
CREATE TABLE IF NOT EXISTS `adverteerders` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`company_name` VARCHAR(160) NOT NULL,
`contact_name` VARCHAR(160) NOT NULL DEFAULT '',
`email` VARCHAR(255) NOT NULL,
`phone` VARCHAR(50) NULL,
`address` VARCHAR(160) NULL,
`postal_code` VARCHAR(20) NULL,
`city` VARCHAR(120) NULL,
`country` VARCHAR(80) NULL,
`coc_number` VARCHAR(40) NULL, -- KvK
`vat_number` VARCHAR(40) NULL, -- btw
`created_at` DATETIME(6) NOT NULL,
`last_seen_at` DATETIME(6) NULL,
`blocked_at` DATETIME(6) NULL, -- geen nieuwe campagnes meer; bestaande blijven staan
PRIMARY KEY (`id`),
UNIQUE KEY `ux_adverteerders_email` (`email`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- ---------------------------------------------------------------- campagnes ---
-- Eén geboekte advertentie: wat er staat, wanneer hij mag draaien, hoe vaak per uur, wat hij
-- kost en hoe ver hij is.
--
-- De afbeelding staat hier als blob en niet in `vault_files`. Die tabel is voor spullen van
-- gebruikers en is versleuteld met sleutels per gebruiker; een advertentielogo is juist
-- openbaar en hoort daar niet tussen te staan. Hij wordt bij het opslaan verkleind, dus groot
-- kan hij niet worden.
--
-- `amount_cents` wordt bij het boeken berekend en dan vastgelegd. Verandert het tarief later,
-- dan verandert een al geboekte campagne niet mee - je hebt afgesproken wat je afsprak.
CREATE TABLE IF NOT EXISTS `campagnes` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`advertiser_id` BIGINT NOT NULL,
`text` VARCHAR(300) NOT NULL,
`link` VARCHAR(500) NOT NULL,
`image` LONGBLOB NULL,
`image_type` VARCHAR(100) NULL,
`starts_on` DATE NOT NULL,
`ends_on` DATE NOT NULL,
`per_hour` INT NOT NULL,
`amount_cents` INT NOT NULL,
`rate_cents` INT NOT NULL, -- het tarief waarmee gerekend is, voor de verantwoording
`payment_reference` VARCHAR(40) NULL, -- wat de adverteerder bij zijn bunq-betaling zet
`paid_at` DATETIME(6) NULL,
`paid_by_user_id` BIGINT NULL, -- welke beheerder het afvinkte
`state` TINYINT NOT NULL DEFAULT 0,
`approved_at` DATETIME(6) NULL,
`approved_by_user_id` BIGINT NULL,
`rejection_reason` VARCHAR(500) NULL,
`created_at` DATETIME(6) NOT NULL,
`updated_at` DATETIME(6) NULL,
PRIMARY KEY (`id`),
KEY `ix_campagnes_adverteerder` (`advertiser_id`),
-- De vraag "welke campagnes mogen nu draaien" komt bij élke keer dat iemand het overzicht
-- opent. Die moet op een index kunnen landen en niet op de hele tabel.
KEY `ix_campagnes_lopend` (`state`, `starts_on`, `ends_on`),
CONSTRAINT `fk_campagnes_adverteerder` FOREIGN KEY (`advertiser_id`)
REFERENCES `adverteerders` (`id`) ON DELETE RESTRICT
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- --------------------------------------------------------------- vertoningen ---
-- Eén regel per campagne per klokuur, met tellers erin. Níét één regel per vertoning: bij een
-- paar honderd bezoekers per uur loopt zo'n tabel eindeloos vol, en voor het kiezen én voor de
-- verantwoording is een teller per uur genoeg.
--
-- Het uur staat in UTC, op het hele uur afgerond. De unieke sleutel is wat het optellen veilig
-- maakt: twee bezoekers tegelijk leveren een INSERT ... ON DUPLICATE KEY UPDATE op en geen
-- tweede regel.
CREATE TABLE IF NOT EXISTS `campagne_uren` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`campaign_id` BIGINT NOT NULL,
`hour_utc` DATETIME(6) NOT NULL,
`shown` INT NOT NULL DEFAULT 0,
`clicks` INT NOT NULL DEFAULT 0,
PRIMARY KEY (`id`),
UNIQUE KEY `ux_campagne_uren` (`campaign_id`, `hour_utc`),
CONSTRAINT `fk_campagne_uren_campagne` FOREIGN KEY (`campaign_id`)
REFERENCES `campagnes` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- De rechten voor de applicatiegebruiker staan NIET hier maar in 02_applicatiegebruiker.sql.
--
-- Waarom: de scripts vanaf 03 worden ook door de uitrolagent gedraaid, met een beperkt
-- databaseaccount dat geen rechten mág uitdelen. Een GRANT hier laat die hele uitrol struikelen,
-- en dat account meer macht geven is de verkeerde kant op. Rechten uitdelen is inrichten; dat
-- doet een mens één keer als root. Een zelfcontrole legt elke CREATE TABLE in deze map naast de
-- lijst in 02, dus vergeten kan niet ongemerkt.
SELECT 'Klaar. Adverteerders, campagnes en tellers staan klaar.' AS melding;
+38
View File
@@ -0,0 +1,38 @@
-- MijnBesluit - onthouden welke inlogcode ECHT is gebruikt
--
-- Er zit een rem op het aanvragen van codes: niet meer dan vijf per uur voor hetzelfde adres.
-- Die rem is er tegen één ding: iemand die het postvak van een ander volgooit door steeds
-- opnieuw een code voor dat adres aan te vragen.
--
-- Die telling keek naar alle codes van het afgelopen uur, ook de codes die gewoon zijn gebruikt
-- om in te loggen. Wie vijf keer in een uur inlogde, sloot zichzelf dus een uur buiten - en dat
-- gebeurt precies wanneer er íéts anders aan de hand is en je het een paar keer opnieuw
-- probeert. Dat is de verkeerde kant om te falen: de rem hoort een vreemde te stoppen, niet de
-- eigenaar.
--
-- Waarom daar een nieuwe kolom voor nodig is: `consumed_at` betekent "deze code werkt niet
-- meer", en dat wordt ook gezet als een nieuwe aanvraag de vorige ongeldig maakt, en als een
-- code te vaak fout is ingevuld. Precies die twee gevallen MOETEN meetellen - anders kan een
-- vreemde de rem leegtrekken door codes aan te vragen of expres fout in te vullen.
--
-- `signed_in_at` staat alleen gevuld als de code goed is ingevuld en er iemand mee is
-- binnengekomen. Alleen dát is het bewijs dat de aanvrager zelf in dat postvak zit, en alleen
-- die codes tellen niet mee voor de rem.
--
-- Draaien met:
-- mysql -u root -p mijnbesluit < db\14_inlogcode_gebruikt.sql
--
-- Opnieuw draaien mag: er gebeurt dan niets.
USE `mijnbesluit`;
SET @bestaat := (
SELECT COUNT(*) FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'login_codes' AND COLUMN_NAME = 'signed_in_at');
SET @sql := IF(@bestaat = 0,
'ALTER TABLE `login_codes` ADD COLUMN `signed_in_at` DATETIME(3) NULL',
'SELECT "signed_in_at bestaat al" AS melding');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SELECT 'Klaar.' AS melding;
+46
View File
@@ -0,0 +1,46 @@
-- MijnBesluit - twee mensen mogen dezelfde persoon kennen
--
-- Op `persons.email` stond een UNIQUE-index. Dat botste met het uitgangspunt van
-- docs/Identiteit.md: iedereen heeft zijn EIGEN aantekening over een ander, en die aantekeningen
-- worden pas één zodra de persoon zelf beide uitnodigingen aanneemt.
--
-- Wat er misging: voegde je iemand toe met een e-mailadres dat al ergens in de database stond -
-- bij wie dan ook, ook bij een wildvreemde - dan liep het opslaan stuk op die index. Voor de
-- gebruiker was dat een knop die niets deed. En het gevolg was erger dan een foutmelding: wie
-- het adres van iemand het eerst invulde, bepaalde daarmee dat niemand anders die persoon nog
-- kon toevoegen. In een applicatie waarin twee ouders allebei hun dochter toevoegen is dat
-- precies de verkeerde kant om.
--
-- Uniek blijft het wél bij `users.email`: één account per e-mailadres. Dat is iets anders dan
-- een aantekening over iemand, en dat moet uniek blijven, anders weet niemand wie er inlogt.
--
-- De index zelf blijft staan, alleen niet meer uniek: er wordt op adres gezocht.
--
-- Draaien met:
-- mysql -u root -p mijnbesluit < db\15_adres_niet_uniek.sql
--
-- Opnieuw draaien mag: er gebeurt dan niets.
USE `mijnbesluit`;
SET @uniek := (
SELECT COUNT(*) FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'persons'
AND INDEX_NAME = 'ux_persons_email');
SET @sql := IF(@uniek > 0,
'ALTER TABLE `persons` DROP INDEX `ux_persons_email`',
'SELECT "ux_persons_email bestaat niet (meer)" AS melding');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @gewoon := (
SELECT COUNT(*) FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'persons'
AND INDEX_NAME = 'ix_persons_email');
SET @sql := IF(@gewoon = 0,
'ALTER TABLE `persons` ADD INDEX `ix_persons_email` (`email`)',
'SELECT "ix_persons_email bestaat al" AS melding');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SELECT 'Klaar.' AS melding;
+54
View File
@@ -0,0 +1,54 @@
-- MijnBesluit - aanmeldingen van de app bewaren, zodat ze een herstart overleven
--
-- Waarom deze tabel er komt: de app blijft aangemeld en komt er daarna in met de vingerafdruk
-- of de code van de telefoon. Dat werkt alleen zolang de server het sessietoken nog kent. De
-- sessies stonden in het geheugen van de SocketServer, dus elke herstart van de service - een
-- uitrol, een Windows-update 's nachts, een reboot van de server - gooide ze allemaal weg. Het
-- toestel wist dan nog wie je was, de server niet meer, en je moest weer een code uit je mail
-- halen. Precies wat de vingerafdruk zou moeten voorkomen.
--
-- Het token zelf staat er NIET in, alleen de SHA-256 ervan - dezelfde afspraak als bij de
-- inlogcodes. Wie de database in handen krijgt, krijgt daarmee dus geen werkende sessies mee:
-- uit een hash valt geen token terug te rekenen.
--
-- `verloopt_op` schuift op bij gebruik, maar niet bij elk verzoek. Dat zou een schrijfactie per
-- aanroep betekenen voor iets waar een uur speling volstaat bij een termijn van veertien dagen.
--
-- Draaien met:
-- mysql -u root -p mijnbesluit < db\16_sessies.sql
--
-- Opnieuw draaien mag: er gebeurt dan niets.
USE `mijnbesluit`;
CREATE TABLE IF NOT EXISTS `sessies` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`token_hash` VARBINARY(32) NOT NULL, -- SHA-256 van het token; nooit het token zelf
`user_id` BIGINT NOT NULL,
`person_id` BIGINT NOT NULL,
`toestel` VARCHAR(120) NULL,
`aangemaakt_op` DATETIME(3) NOT NULL,
`verloopt_op` DATETIME(3) NOT NULL,
`laatst_gezien` DATETIME(3) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `ux_sessies_token` (`token_hash`),
KEY `ix_sessies_verloopt` (`verloopt_op`),
CONSTRAINT `fk_sessies_user` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB;
-- DE RECHTEN STAAN HIER MET OPZET NIET.
--
-- De verleiding was groot: een nieuwe tabel zonder GRANT is onzichtbaar tot iemand hem gebruikt,
-- en dan zegt de server "INSERT command denied" terwijl de gebruiker alleen "Er ging iets mis"
-- ziet. Precies wat er op 17 september gebeurde - inloggen in de app liep erop stuk.
--
-- Maar de scripts vanaf 03 worden ook door de uitrolagent gedraaid, met een beperkt
-- databaseaccount dat helemaal geen rechten mág uitdelen. Een GRANT hier zou die hele uitrol
-- laten struikelen, en de oplossing "geef dat account dan GRANT-rechten" maakt het uitrolkanaal
-- machtiger dan het hoeft te zijn. Rechten uitdelen is inrichten, en dat doet een mens één keer.
--
-- De rechten staan dus in 02_applicatiegebruiker.sql, en een zelfcontrole
-- ("Elke tabel geeft de applicatiegebruiker rechten") legt elke CREATE TABLE in deze map naast
-- die lijst. Die vangt het al vóór de uitrol, en dat is de goede plek.
SELECT 'Klaar. Vergeet 02_applicatiegebruiker.sql niet als dit een nieuwe server is.' AS melding;
+34
View File
@@ -0,0 +1,34 @@
-- MijnBesluit - onthouden dat iemand "nee, dat ben ik niet" heeft gezegd
--
-- Waar dit voor is: maakt iemand een account aan op een adres waarover een ander al een
-- aantekening heeft staan, dan vragen we hem of hij dat is. Hij beslist; koppelen op adres doen
-- we niet, want een adres is geen identiteit (twee mensen kunnen er een delen, en één mens heeft
-- er vaak meerdere). Zie docs/Identiteit.md.
--
-- Zegt hij nee, dan hoort die vraag niet bij elke volgende keer opnieuw te komen. Dat is wat
-- hier wordt bewaard. Alleen "nee": een "ja" is geen vlaggetje maar een samenvoeging, en dan
-- bestaat die aantekening als aparte rij niet meer (merged_into_person_id).
--
-- Degene die de aantekening maakte merkt hier niets van. Hij krijgt geen melding dat er iets
-- gevraagd is en geen melding dat het antwoord nee was - anders zou het weigeren zelf een
-- mededeling worden over wie er wel of niet een account heeft.
--
-- Draaien met:
-- mysql -u root -p mijnbesluit < db\17_aantekening_afgewezen.sql
--
-- Opnieuw draaien mag: er gebeurt dan niets.
USE `mijnbesluit`;
SET @bestaat := (
SELECT COUNT(*) FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'persons' AND COLUMN_NAME = 'link_declined_by_user_id');
SET @sql := IF(@bestaat = 0,
'ALTER TABLE `persons`
ADD COLUMN `link_declined_by_user_id` BIGINT NULL,
ADD COLUMN `link_declined_at` DATETIME(3) NULL',
'SELECT "link_declined_by_user_id bestaat al" AS melding');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SELECT 'Klaar.' AS melding;
+105
View File
@@ -0,0 +1,105 @@
-- MijnBesluit - kijklink voor een moment aan iemand zonder account
--
-- Zet je een moment klaar voor iemand die (nog) geen MijnBesluit-account heeft, dan krijgt hij
-- voortaan een e-mail met een link naar een landingspagina: daar is het bericht - tekst en een
-- eventuele foto, video of document - te zien zonder in te loggen. Zelfde opzet als de
-- uitnodigings- en bezwaarlink: alleen de hash van het token staat in de database, het token
-- zelf staat uitsluitend in de e-mail.
--
-- view_token_hash : SHA-256 van het token uit de link
-- view_token_expires : tot wanneer die link werkt (30 dagen na het versturen)
-- view_token_sent_at : wanneer de linkmail voor het eerst is verstuurd
-- viewed_at : wanneer de landingspagina daadwerkelijk is geopend
-- view_reminder_sent_at : wanneer de herinnering is verstuurd (nooit meer dan één keer)
-- wrapped_content_key_for_token: de contentsleutel, ingepakt met een sleutel uit het token zelf
-- wrapped_vault_key_for_token : dezelfde inpakslag, voor de bijlage bij dit bericht
--
-- Draaien met:
-- mysql -u root -p mijnbesluit < db\18_kijklink.sql
--
-- Opnieuw draaien mag: er gebeurt dan niets.
USE `mijnbesluit`;
-- ---------------------------------------------------------------- view_token_hash
SET @bestaat := (
SELECT COUNT(*) FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'messages' AND COLUMN_NAME = 'view_token_hash');
SET @sql := IF(@bestaat = 0,
'ALTER TABLE `messages` ADD COLUMN `view_token_hash` VARBINARY(32) NULL',
'SELECT "view_token_hash bestaat al" AS melding');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- ---------------------------------------------------------------- view_token_expires
SET @bestaat := (
SELECT COUNT(*) FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'messages' AND COLUMN_NAME = 'view_token_expires');
SET @sql := IF(@bestaat = 0,
'ALTER TABLE `messages` ADD COLUMN `view_token_expires` DATETIME(3) NULL',
'SELECT "view_token_expires bestaat al" AS melding');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- ---------------------------------------------------------------- view_token_sent_at
SET @bestaat := (
SELECT COUNT(*) FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'messages' AND COLUMN_NAME = 'view_token_sent_at');
SET @sql := IF(@bestaat = 0,
'ALTER TABLE `messages` ADD COLUMN `view_token_sent_at` DATETIME(3) NULL',
'SELECT "view_token_sent_at bestaat al" AS melding');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- ---------------------------------------------------------------- viewed_at
SET @bestaat := (
SELECT COUNT(*) FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'messages' AND COLUMN_NAME = 'viewed_at');
SET @sql := IF(@bestaat = 0,
'ALTER TABLE `messages` ADD COLUMN `viewed_at` DATETIME(3) NULL',
'SELECT "viewed_at bestaat al" AS melding');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- ---------------------------------------------------------------- view_reminder_sent_at
SET @bestaat := (
SELECT COUNT(*) FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'messages' AND COLUMN_NAME = 'view_reminder_sent_at');
SET @sql := IF(@bestaat = 0,
'ALTER TABLE `messages` ADD COLUMN `view_reminder_sent_at` DATETIME(3) NULL',
'SELECT "view_reminder_sent_at bestaat al" AS melding');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- ---------------------------------------------------------------- wrapped_content_key_for_token
SET @bestaat := (
SELECT COUNT(*) FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'messages' AND COLUMN_NAME = 'wrapped_content_key_for_token');
SET @sql := IF(@bestaat = 0,
'ALTER TABLE `messages` ADD COLUMN `wrapped_content_key_for_token` VARBINARY(96) NULL',
'SELECT "wrapped_content_key_for_token bestaat al" AS melding');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- ---------------------------------------------------------------- wrapped_vault_key_for_token
SET @bestaat := (
SELECT COUNT(*) FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'messages' AND COLUMN_NAME = 'wrapped_vault_key_for_token');
SET @sql := IF(@bestaat = 0,
'ALTER TABLE `messages` ADD COLUMN `wrapped_vault_key_for_token` VARBINARY(96) NULL',
'SELECT "wrapped_vault_key_for_token bestaat al" AS melding');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- ---------------------------------------------------------------- index
-- De landingspagina zoekt het bericht op dit kenmerk op, dus zonder index is dat een tabelscan.
SET @bestaat := (
SELECT COUNT(*) FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'messages' AND INDEX_NAME = 'ix_messages_view_token');
SET @sql := IF(@bestaat = 0,
'CREATE INDEX `ix_messages_view_token` ON `messages` (`view_token_hash`)',
'SELECT "ix_messages_view_token bestaat al" AS melding');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SELECT 'Klaar. Momenten voor iemand zonder account krijgen vanaf nu een kijklink per e-mail.' AS melding;
+28
View File
@@ -0,0 +1,28 @@
-- MijnBesluit - beheerder kiezen om een uitvaartwens vooraf te bespreken
--
-- Bij een kluisitem van het soort Uitvaartwens kan de eigenaar voortaan één van zijn
-- nalatenschapsbeheerders kiezen. Die beheerder krijgt dan meteen leestoegang tot dit ene item
-- (via content_grants, reden "gedeeld"), nog vóór er sprake is van een overlijden - zodat het te
-- bespreken is en vooraf al duidelijk is hoe en wat. Kiest de eigenaar niemand, dan blijft het
-- item zoals altijd alleen voor hemzelf.
--
-- shared_manager_person_id : de gekozen beheerder (persons.id); NULL = niemand gekozen
--
-- Draaien met:
-- mysql -u root -p mijnbesluit < db\19_uitvaartwens_beheerder.sql
--
-- Opnieuw draaien mag: er gebeurt dan niets.
USE `mijnbesluit`;
-- ---------------------------------------------------------------- shared_manager_person_id
SET @bestaat := (
SELECT COUNT(*) FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'vault_items' AND COLUMN_NAME = 'shared_manager_person_id');
SET @sql := IF(@bestaat = 0,
'ALTER TABLE `vault_items` ADD COLUMN `shared_manager_person_id` BIGINT NULL',
'SELECT "shared_manager_person_id bestaat al" AS melding');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SELECT 'Klaar. Bij een uitvaartwens is voortaan een beheerder te kiezen om vooraf mee te bespreken.' AS melding;
+8
View File
@@ -0,0 +1,8 @@
-- MijnBesluit - een bezorgd bericht uit je eigen lijst verwijderen
--
-- Wie een bezorgd bericht "verwijdert" haalt het alleen bij zichzelf weg; de ander heeft er nog
-- steeds recht op. Vandaar twee vlaggen in plaats van één verwijderdatum: de afzender en de
-- ontvanger ruimen onafhankelijk van elkaar op. De rij zelf blijft altijd bestaan.
ALTER TABLE `messages`
ADD COLUMN `hidden_by_sender` TINYINT(1) NOT NULL DEFAULT 0 AFTER `read_at`,
ADD COLUMN `hidden_by_recipient` TINYINT(1) NOT NULL DEFAULT 0 AFTER `hidden_by_sender`;