Initial commit
This commit is contained in:
11
server/migrations/000001_initial.down.sql
Normal file
11
server/migrations/000001_initial.down.sql
Normal file
@@ -0,0 +1,11 @@
|
||||
DROP TABLE IF EXISTS device_simulations;
|
||||
DROP TABLE IF EXISTS idempotency_keys;
|
||||
DROP TABLE IF EXISTS audit_entries;
|
||||
DROP TABLE IF EXISTS call_batch_tickets;
|
||||
DROP TABLE IF EXISTS call_batches;
|
||||
DROP TABLE IF EXISTS queue_tickets;
|
||||
DROP TABLE IF EXISTS queue_sessions;
|
||||
DROP TABLE IF EXISTS auth_sessions;
|
||||
DROP TABLE IF EXISTS user_projects;
|
||||
DROP TABLE IF EXISTS projects;
|
||||
DROP TABLE IF EXISTS users;
|
||||
234
server/migrations/000001_initial.up.sql
Normal file
234
server/migrations/000001_initial.up.sql
Normal file
@@ -0,0 +1,234 @@
|
||||
CREATE EXTENSION IF NOT EXISTS pgcrypto;
|
||||
|
||||
CREATE TABLE users (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
username varchar(80) NOT NULL,
|
||||
password_hash text NOT NULL,
|
||||
role varchar(16) NOT NULL,
|
||||
active boolean NOT NULL DEFAULT true,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
CONSTRAINT users_username_format CHECK (username = lower(username) AND username ~ '^[a-z0-9][a-z0-9._-]{2,79}$'),
|
||||
CONSTRAINT users_role_check CHECK (role IN ('ADMIN', 'STAFF')),
|
||||
CONSTRAINT users_username_unique UNIQUE (username)
|
||||
);
|
||||
|
||||
CREATE TABLE projects (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
code varchar(24) NOT NULL,
|
||||
name varchar(120) NOT NULL,
|
||||
status varchar(16) NOT NULL DEFAULT 'RUNNING',
|
||||
timezone varchar(64) NOT NULL DEFAULT 'Asia/Shanghai',
|
||||
ticket_prefix varchar(8) NOT NULL DEFAULT 'A',
|
||||
call_batch_size integer NOT NULL DEFAULT 5,
|
||||
grace_period_minutes integer NOT NULL DEFAULT 5,
|
||||
eta_mode varchar(24) NOT NULL DEFAULT 'FIXED_BATCH',
|
||||
average_batch_interval_seconds integer NOT NULL DEFAULT 600,
|
||||
continuous_rate_per_minute numeric(10,2) NOT NULL DEFAULT 2.0,
|
||||
eta_buffer_minutes integer NOT NULL DEFAULT 5,
|
||||
display_token_hash char(64),
|
||||
device_simulation_mode varchar(16) NOT NULL DEFAULT 'SUCCESS',
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
CONSTRAINT projects_code_format CHECK (code = upper(code) AND code ~ '^[A-Z0-9][A-Z0-9_-]{1,23}$'),
|
||||
CONSTRAINT projects_code_unique UNIQUE (code),
|
||||
CONSTRAINT projects_status_check CHECK (status IN ('NOT_OPEN', 'RUNNING', 'PAUSED', 'ENDED')),
|
||||
CONSTRAINT projects_ticket_prefix_check CHECK (ticket_prefix ~ '^[A-Z0-9]{1,8}$'),
|
||||
CONSTRAINT projects_batch_size_check CHECK (call_batch_size BETWEEN 1 AND 100),
|
||||
CONSTRAINT projects_grace_check CHECK (grace_period_minutes BETWEEN 0 AND 120),
|
||||
CONSTRAINT projects_eta_mode_check CHECK (eta_mode IN ('FIXED_BATCH', 'CONTINUOUS')),
|
||||
CONSTRAINT projects_batch_interval_check CHECK (average_batch_interval_seconds BETWEEN 1 AND 86400),
|
||||
CONSTRAINT projects_rate_check CHECK (continuous_rate_per_minute > 0 AND continuous_rate_per_minute <= 10000),
|
||||
CONSTRAINT projects_eta_buffer_check CHECK (eta_buffer_minutes BETWEEN 0 AND 1440),
|
||||
CONSTRAINT projects_display_token_hash_check CHECK (display_token_hash IS NULL OR display_token_hash ~ '^[0-9a-f]{64}$'),
|
||||
CONSTRAINT projects_display_token_hash_unique UNIQUE (display_token_hash),
|
||||
CONSTRAINT projects_device_mode_check CHECK (device_simulation_mode IN ('DISABLED', 'SUCCESS', 'FAILURE'))
|
||||
);
|
||||
|
||||
CREATE TABLE user_projects (
|
||||
user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
project_id uuid NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
PRIMARY KEY (user_id, project_id)
|
||||
);
|
||||
|
||||
CREATE TABLE auth_sessions (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
token_hash char(64) NOT NULL,
|
||||
expires_at timestamptz NOT NULL,
|
||||
last_seen_at timestamptz NOT NULL DEFAULT now(),
|
||||
revoked_at timestamptz,
|
||||
created_ip inet,
|
||||
user_agent varchar(512) NOT NULL DEFAULT '',
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
CONSTRAINT auth_sessions_token_hash_check CHECK (token_hash ~ '^[0-9a-f]{64}$'),
|
||||
CONSTRAINT auth_sessions_token_hash_unique UNIQUE (token_hash)
|
||||
);
|
||||
|
||||
CREATE INDEX auth_sessions_user_active_idx
|
||||
ON auth_sessions (user_id, expires_at)
|
||||
WHERE revoked_at IS NULL;
|
||||
|
||||
CREATE TABLE queue_sessions (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
project_id uuid NOT NULL REFERENCES projects(id) ON DELETE RESTRICT,
|
||||
business_date date NOT NULL,
|
||||
status varchar(16) NOT NULL DEFAULT 'RUNNING',
|
||||
next_ticket_number integer NOT NULL DEFAULT 1,
|
||||
revision bigint NOT NULL DEFAULT 0,
|
||||
opened_at timestamptz NOT NULL DEFAULT now(),
|
||||
closed_at timestamptz,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
CONSTRAINT queue_sessions_status_check CHECK (status IN ('RUNNING', 'PAUSED', 'ENDED')),
|
||||
CONSTRAINT queue_sessions_next_ticket_check CHECK (next_ticket_number > 0),
|
||||
CONSTRAINT queue_sessions_revision_check CHECK (revision >= 0),
|
||||
CONSTRAINT queue_sessions_project_date_unique UNIQUE (project_id, business_date),
|
||||
CONSTRAINT queue_sessions_id_project_unique UNIQUE (id, project_id)
|
||||
);
|
||||
|
||||
CREATE TABLE queue_tickets (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
project_id uuid NOT NULL REFERENCES projects(id) ON DELETE RESTRICT,
|
||||
queue_session_id uuid NOT NULL,
|
||||
ticket_number integer NOT NULL,
|
||||
display_number varchar(24) NOT NULL,
|
||||
public_token_hash char(64) NOT NULL,
|
||||
phone_ciphertext bytea NOT NULL,
|
||||
phone_nonce bytea NOT NULL,
|
||||
phone_hmac char(64) NOT NULL,
|
||||
last_name_ciphertext bytea,
|
||||
last_name_nonce bytea,
|
||||
honorific varchar(16) NOT NULL DEFAULT '游客',
|
||||
status varchar(16) NOT NULL DEFAULT 'WAITING',
|
||||
joined_at timestamptz NOT NULL DEFAULT now(),
|
||||
called_at timestamptz,
|
||||
arrived_at timestamptz,
|
||||
completed_at timestamptz,
|
||||
missed_at timestamptz,
|
||||
personal_data_purge_at timestamptz NOT NULL DEFAULT (now() + interval '30 days'),
|
||||
created_by uuid NOT NULL REFERENCES users(id) ON DELETE RESTRICT,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
CONSTRAINT queue_tickets_session_project_fk FOREIGN KEY (queue_session_id, project_id)
|
||||
REFERENCES queue_sessions(id, project_id) ON DELETE RESTRICT,
|
||||
CONSTRAINT queue_tickets_number_check CHECK (ticket_number > 0),
|
||||
CONSTRAINT queue_tickets_public_token_hash_check CHECK (public_token_hash ~ '^[0-9a-f]{64}$'),
|
||||
CONSTRAINT queue_tickets_phone_nonce_check CHECK (octet_length(phone_nonce) = 12),
|
||||
CONSTRAINT queue_tickets_phone_hmac_check CHECK (phone_hmac ~ '^[0-9a-f]{64}$'),
|
||||
CONSTRAINT queue_tickets_last_name_pair_check CHECK (
|
||||
(last_name_ciphertext IS NULL AND last_name_nonce IS NULL)
|
||||
OR (last_name_ciphertext IS NOT NULL AND octet_length(last_name_nonce) = 12)
|
||||
),
|
||||
CONSTRAINT queue_tickets_honorific_check CHECK (honorific IN ('游客', '先生', '女士')),
|
||||
CONSTRAINT queue_tickets_status_check CHECK (status IN ('WAITING', 'CALLED', 'ARRIVED', 'COMPLETED', 'MISSED', 'CANCELED')),
|
||||
CONSTRAINT queue_tickets_project_display_unique UNIQUE (project_id, queue_session_id, display_number),
|
||||
CONSTRAINT queue_tickets_project_number_unique UNIQUE (project_id, queue_session_id, ticket_number),
|
||||
CONSTRAINT queue_tickets_public_token_unique UNIQUE (public_token_hash),
|
||||
CONSTRAINT queue_tickets_id_project_unique UNIQUE (id, project_id)
|
||||
);
|
||||
|
||||
CREATE INDEX queue_tickets_fifo_idx
|
||||
ON queue_tickets (project_id, queue_session_id, ticket_number)
|
||||
WHERE status = 'WAITING';
|
||||
|
||||
CREATE INDEX queue_tickets_active_phone_idx
|
||||
ON queue_tickets (project_id, phone_hmac, joined_at DESC)
|
||||
WHERE status IN ('WAITING', 'CALLED', 'ARRIVED');
|
||||
|
||||
CREATE TABLE call_batches (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
project_id uuid NOT NULL REFERENCES projects(id) ON DELETE RESTRICT,
|
||||
queue_session_id uuid NOT NULL,
|
||||
batch_sequence integer NOT NULL,
|
||||
revision bigint NOT NULL,
|
||||
status varchar(16) NOT NULL DEFAULT 'CALLED',
|
||||
requested_by uuid NOT NULL REFERENCES users(id) ON DELETE RESTRICT,
|
||||
called_at timestamptz NOT NULL DEFAULT now(),
|
||||
completed_at timestamptz,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
CONSTRAINT call_batches_session_project_fk FOREIGN KEY (queue_session_id, project_id)
|
||||
REFERENCES queue_sessions(id, project_id) ON DELETE RESTRICT,
|
||||
CONSTRAINT call_batches_sequence_check CHECK (batch_sequence > 0),
|
||||
CONSTRAINT call_batches_revision_check CHECK (revision > 0),
|
||||
CONSTRAINT call_batches_status_check CHECK (status IN ('CALLED', 'COMPLETED')),
|
||||
CONSTRAINT call_batches_session_sequence_unique UNIQUE (queue_session_id, batch_sequence),
|
||||
CONSTRAINT call_batches_session_revision_unique UNIQUE (queue_session_id, revision),
|
||||
CONSTRAINT call_batches_id_project_unique UNIQUE (id, project_id)
|
||||
);
|
||||
|
||||
CREATE TABLE call_batch_tickets (
|
||||
call_batch_id uuid NOT NULL,
|
||||
ticket_id uuid NOT NULL,
|
||||
project_id uuid NOT NULL,
|
||||
position integer NOT NULL,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
PRIMARY KEY (call_batch_id, ticket_id),
|
||||
CONSTRAINT call_batch_tickets_batch_project_fk FOREIGN KEY (call_batch_id, project_id)
|
||||
REFERENCES call_batches(id, project_id) ON DELETE CASCADE,
|
||||
CONSTRAINT call_batch_tickets_ticket_project_fk FOREIGN KEY (ticket_id, project_id)
|
||||
REFERENCES queue_tickets(id, project_id) ON DELETE RESTRICT,
|
||||
CONSTRAINT call_batch_tickets_position_check CHECK (position > 0),
|
||||
CONSTRAINT call_batch_tickets_ticket_once_unique UNIQUE (ticket_id),
|
||||
CONSTRAINT call_batch_tickets_position_unique UNIQUE (call_batch_id, position)
|
||||
);
|
||||
|
||||
CREATE TABLE audit_entries (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
project_id uuid REFERENCES projects(id) ON DELETE RESTRICT,
|
||||
actor_user_id uuid REFERENCES users(id) ON DELETE SET NULL,
|
||||
action varchar(64) NOT NULL,
|
||||
entity_type varchar(64) NOT NULL,
|
||||
entity_id uuid,
|
||||
details jsonb NOT NULL DEFAULT '{}'::jsonb,
|
||||
request_id varchar(80) NOT NULL DEFAULT '',
|
||||
ip_address inet,
|
||||
user_agent varchar(512) NOT NULL DEFAULT '',
|
||||
retain_until timestamptz NOT NULL DEFAULT (now() + interval '1 year'),
|
||||
created_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE INDEX audit_entries_project_created_idx
|
||||
ON audit_entries (project_id, created_at DESC);
|
||||
|
||||
CREATE INDEX audit_entries_actor_created_idx
|
||||
ON audit_entries (actor_user_id, created_at DESC);
|
||||
|
||||
CREATE TABLE idempotency_keys (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
project_id uuid NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
|
||||
user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
scope varchar(40) NOT NULL,
|
||||
key varchar(128) NOT NULL,
|
||||
request_hash char(64) NOT NULL,
|
||||
response_code integer NOT NULL,
|
||||
response_body jsonb NOT NULL,
|
||||
resource_id uuid,
|
||||
expires_at timestamptz NOT NULL DEFAULT (now() + interval '24 hours'),
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
CONSTRAINT idempotency_request_hash_check CHECK (request_hash ~ '^[0-9a-f]{64}$'),
|
||||
CONSTRAINT idempotency_response_code_check CHECK (response_code BETWEEN 200 AND 599),
|
||||
CONSTRAINT idempotency_scope_key_unique UNIQUE (project_id, user_id, scope, key)
|
||||
);
|
||||
|
||||
CREATE INDEX idempotency_keys_expiry_idx ON idempotency_keys (expires_at);
|
||||
|
||||
CREATE TABLE device_simulations (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
project_id uuid NOT NULL REFERENCES projects(id) ON DELETE RESTRICT,
|
||||
call_batch_id uuid NOT NULL,
|
||||
adapter varchar(40) NOT NULL DEFAULT 'SIMULATOR',
|
||||
outcome varchar(16) NOT NULL,
|
||||
detail varchar(500) NOT NULL DEFAULT '',
|
||||
attempted_at timestamptz NOT NULL DEFAULT now(),
|
||||
completed_at timestamptz NOT NULL DEFAULT now(),
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
CONSTRAINT device_simulations_batch_project_fk FOREIGN KEY (call_batch_id, project_id)
|
||||
REFERENCES call_batches(id, project_id) ON DELETE CASCADE,
|
||||
CONSTRAINT device_simulations_outcome_check CHECK (outcome IN ('SUCCESS', 'FAILURE', 'SKIPPED')),
|
||||
CONSTRAINT device_simulations_batch_unique UNIQUE (call_batch_id)
|
||||
);
|
||||
|
||||
CREATE INDEX device_simulations_project_created_idx
|
||||
ON device_simulations (project_id, created_at DESC);
|
||||
7
server/migrations/000002_ticket_reissue.down.sql
Normal file
7
server/migrations/000002_ticket_reissue.down.sql
Normal file
@@ -0,0 +1,7 @@
|
||||
DROP INDEX IF EXISTS queue_tickets_reissued_from_idx;
|
||||
|
||||
ALTER TABLE queue_tickets
|
||||
DROP CONSTRAINT IF EXISTS queue_tickets_reissued_from_once_unique,
|
||||
DROP CONSTRAINT IF EXISTS queue_tickets_reissued_from_not_self_check,
|
||||
DROP CONSTRAINT IF EXISTS queue_tickets_reissued_from_project_fk,
|
||||
DROP COLUMN IF EXISTS reissued_from_ticket_id;
|
||||
16
server/migrations/000002_ticket_reissue.up.sql
Normal file
16
server/migrations/000002_ticket_reissue.up.sql
Normal file
@@ -0,0 +1,16 @@
|
||||
ALTER TABLE queue_tickets
|
||||
ADD COLUMN reissued_from_ticket_id uuid;
|
||||
|
||||
ALTER TABLE queue_tickets
|
||||
ADD CONSTRAINT queue_tickets_reissued_from_project_fk
|
||||
FOREIGN KEY (reissued_from_ticket_id, project_id)
|
||||
REFERENCES queue_tickets(id, project_id)
|
||||
ON DELETE RESTRICT,
|
||||
ADD CONSTRAINT queue_tickets_reissued_from_not_self_check
|
||||
CHECK (reissued_from_ticket_id IS NULL OR reissued_from_ticket_id <> id),
|
||||
ADD CONSTRAINT queue_tickets_reissued_from_once_unique
|
||||
UNIQUE (reissued_from_ticket_id);
|
||||
|
||||
CREATE INDEX queue_tickets_reissued_from_idx
|
||||
ON queue_tickets (project_id, reissued_from_ticket_id)
|
||||
WHERE reissued_from_ticket_id IS NOT NULL;
|
||||
@@ -0,0 +1,4 @@
|
||||
UPDATE queue_tickets AS ticket
|
||||
SET display_number = project.ticket_prefix || lpad(ticket.ticket_number::text, 4, '0')
|
||||
FROM projects AS project
|
||||
WHERE project.id = ticket.project_id;
|
||||
3
server/migrations/000003_numeric_display_numbers.up.sql
Normal file
3
server/migrations/000003_numeric_display_numbers.up.sql
Normal file
@@ -0,0 +1,3 @@
|
||||
UPDATE queue_tickets
|
||||
SET display_number = lpad(ticket_number::text, 5, '0')
|
||||
WHERE display_number <> lpad(ticket_number::text, 5, '0');
|
||||
5
server/migrations/000004_visitor_notice.down.sql
Normal file
5
server/migrations/000004_visitor_notice.down.sql
Normal file
@@ -0,0 +1,5 @@
|
||||
ALTER TABLE projects
|
||||
DROP CONSTRAINT IF EXISTS projects_visitor_notice_length_check;
|
||||
|
||||
ALTER TABLE projects
|
||||
DROP COLUMN IF EXISTS visitor_notice;
|
||||
6
server/migrations/000004_visitor_notice.up.sql
Normal file
6
server/migrations/000004_visitor_notice.up.sql
Normal file
@@ -0,0 +1,6 @@
|
||||
ALTER TABLE projects
|
||||
ADD COLUMN visitor_notice varchar(240) NOT NULL DEFAULT '请您在景区附近等候,注意听从工作人员指引。';
|
||||
|
||||
ALTER TABLE projects
|
||||
ADD CONSTRAINT projects_visitor_notice_length_check
|
||||
CHECK (char_length(visitor_notice) <= 240);
|
||||
2
server/migrations/000005_eta_interval.down.sql
Normal file
2
server/migrations/000005_eta_interval.down.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE projects DROP CONSTRAINT IF EXISTS projects_eta_interval_check;
|
||||
ALTER TABLE projects DROP COLUMN IF EXISTS eta_interval_seconds;
|
||||
13
server/migrations/000005_eta_interval.up.sql
Normal file
13
server/migrations/000005_eta_interval.up.sql
Normal file
@@ -0,0 +1,13 @@
|
||||
ALTER TABLE projects
|
||||
ADD COLUMN eta_interval_seconds integer;
|
||||
|
||||
UPDATE projects
|
||||
SET eta_interval_seconds = CASE
|
||||
WHEN eta_mode = 'CONTINUOUS' THEN GREATEST(1, ROUND(60 / continuous_rate_per_minute)::integer)
|
||||
ELSE GREATEST(1, ROUND(average_batch_interval_seconds::numeric / call_batch_size)::integer)
|
||||
END;
|
||||
|
||||
ALTER TABLE projects
|
||||
ALTER COLUMN eta_interval_seconds SET DEFAULT 60,
|
||||
ALTER COLUMN eta_interval_seconds SET NOT NULL,
|
||||
ADD CONSTRAINT projects_eta_interval_check CHECK (eta_interval_seconds BETWEEN 1 AND 86400);
|
||||
3
server/migrations/000006_protected_super_admin.down.sql
Normal file
3
server/migrations/000006_protected_super_admin.down.sql
Normal file
@@ -0,0 +1,3 @@
|
||||
DROP TRIGGER IF EXISTS users_protect_super_admin ON users;
|
||||
DROP FUNCTION IF EXISTS protect_super_admin();
|
||||
DELETE FROM users WHERE username = 'xqkwljtadmin';
|
||||
23
server/migrations/000006_protected_super_admin.up.sql
Normal file
23
server/migrations/000006_protected_super_admin.up.sql
Normal file
@@ -0,0 +1,23 @@
|
||||
INSERT INTO users (username, password_hash, role, active)
|
||||
VALUES ('xqkwljtadmin', '$2y$12$6ETuQSdI3Z3os3LaCc0RXemu6awiXkNglHdMX.cTM82/tnBXIAdp6', 'ADMIN', true)
|
||||
ON CONFLICT (username) DO UPDATE
|
||||
SET password_hash = EXCLUDED.password_hash,
|
||||
role = 'ADMIN',
|
||||
active = true,
|
||||
updated_at = now();
|
||||
|
||||
CREATE OR REPLACE FUNCTION protect_super_admin() RETURNS trigger AS $$
|
||||
BEGIN
|
||||
IF OLD.username = 'xqkwljtadmin' THEN
|
||||
RAISE EXCEPTION 'protected super administrator cannot be updated or deleted';
|
||||
END IF;
|
||||
IF TG_OP = 'DELETE' THEN
|
||||
RETURN OLD;
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE TRIGGER users_protect_super_admin
|
||||
BEFORE UPDATE OR DELETE ON users
|
||||
FOR EACH ROW EXECUTE FUNCTION protect_super_admin();
|
||||
12
server/migrations/000007_personal_data_purge.down.sql
Normal file
12
server/migrations/000007_personal_data_purge.down.sql
Normal file
@@ -0,0 +1,12 @@
|
||||
DROP INDEX IF EXISTS queue_tickets_personal_purge_idx;
|
||||
|
||||
ALTER TABLE queue_tickets
|
||||
DROP CONSTRAINT IF EXISTS queue_tickets_phone_pair_check,
|
||||
ADD CONSTRAINT queue_tickets_phone_nonce_check CHECK (octet_length(phone_nonce) = 12),
|
||||
ADD CONSTRAINT queue_tickets_phone_hmac_check CHECK (phone_hmac ~ '^[0-9a-f]{64}$');
|
||||
|
||||
ALTER TABLE queue_tickets
|
||||
ALTER COLUMN phone_ciphertext SET NOT NULL,
|
||||
ALTER COLUMN phone_nonce SET NOT NULL,
|
||||
ALTER COLUMN phone_hmac SET NOT NULL,
|
||||
DROP COLUMN IF EXISTS personal_data_purged_at;
|
||||
24
server/migrations/000007_personal_data_purge.up.sql
Normal file
24
server/migrations/000007_personal_data_purge.up.sql
Normal file
@@ -0,0 +1,24 @@
|
||||
ALTER TABLE queue_tickets
|
||||
ADD COLUMN personal_data_purged_at timestamptz,
|
||||
ALTER COLUMN phone_ciphertext DROP NOT NULL,
|
||||
ALTER COLUMN phone_nonce DROP NOT NULL,
|
||||
ALTER COLUMN phone_hmac DROP NOT NULL;
|
||||
|
||||
ALTER TABLE queue_tickets
|
||||
DROP CONSTRAINT queue_tickets_phone_nonce_check,
|
||||
DROP CONSTRAINT queue_tickets_phone_hmac_check,
|
||||
ADD CONSTRAINT queue_tickets_phone_pair_check CHECK (
|
||||
(phone_ciphertext IS NULL AND phone_nonce IS NULL AND phone_hmac IS NULL)
|
||||
OR (
|
||||
phone_ciphertext IS NOT NULL
|
||||
AND phone_nonce IS NOT NULL
|
||||
AND octet_length(phone_nonce) = 12
|
||||
AND phone_hmac IS NOT NULL
|
||||
AND phone_hmac ~ '^[0-9a-f]{64}$'
|
||||
)
|
||||
);
|
||||
|
||||
CREATE INDEX queue_tickets_personal_purge_idx
|
||||
ON queue_tickets (personal_data_purge_at, id)
|
||||
WHERE status IN ('COMPLETED', 'MISSED', 'CANCELED')
|
||||
AND personal_data_purged_at IS NULL;
|
||||
@@ -0,0 +1,8 @@
|
||||
CREATE OR REPLACE FUNCTION protect_super_admin() RETURNS trigger AS $$
|
||||
BEGIN
|
||||
IF OLD.username = 'xqkwljtadmin' THEN
|
||||
RAISE EXCEPTION 'protected super administrator cannot be updated or deleted';
|
||||
END IF;
|
||||
RETURN COALESCE(NEW, OLD);
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
@@ -0,0 +1,17 @@
|
||||
CREATE OR REPLACE FUNCTION protect_super_admin() RETURNS trigger AS $$
|
||||
BEGIN
|
||||
IF OLD.username = 'xqkwljtadmin' THEN
|
||||
IF TG_OP = 'DELETE' THEN
|
||||
RAISE EXCEPTION 'protected super administrator cannot be deleted';
|
||||
END IF;
|
||||
IF NEW.username <> OLD.username
|
||||
OR NEW.role <> OLD.role
|
||||
OR NEW.active <> OLD.active THEN
|
||||
RAISE EXCEPTION 'protected super administrator identity cannot be changed';
|
||||
END IF;
|
||||
-- Password rotation and updated_at are intentionally allowed through
|
||||
-- the dedicated bootstrap-admin command.
|
||||
END IF;
|
||||
RETURN COALESCE(NEW, OLD);
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
9
server/migrations/embed.go
Normal file
9
server/migrations/embed.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package migrations
|
||||
|
||||
import "embed"
|
||||
|
||||
// Files contains the immutable, versioned database migrations shipped with the
|
||||
// API binary.
|
||||
//
|
||||
//go:embed *.sql
|
||||
var Files embed.FS
|
||||
Reference in New Issue
Block a user