mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-09 10:57:03 +08:00
feat(security): harden gateway boundaries and usage policies
Consolidate subscription usage policy enforcement, privacy-safe persistence, and gateway security hardening into one reviewable change. Includes bounded HTTP and execution envelopes, header and protocol guards, DNS and relay validation, authentication and secret projection hardening, secure backup/install paths, and regression coverage.
This commit is contained in:
@@ -578,7 +578,7 @@ CREATE TABLE IF NOT EXISTS proxy_nodes (
|
||||
is_manual TINYINT(1) NOT NULL DEFAULT 0,
|
||||
proxy_url VARCHAR(500),
|
||||
proxy_username VARCHAR(255),
|
||||
proxy_password VARCHAR(500),
|
||||
proxy_password TEXT,
|
||||
created_at BIGINT NOT NULL,
|
||||
updated_at BIGINT NOT NULL,
|
||||
remote_config TEXT,
|
||||
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
CREATE TABLE IF NOT EXISTS usage_cost_reservations (
|
||||
`request_id` VARCHAR(128) NOT NULL,
|
||||
`subject_id` VARCHAR(128) NOT NULL,
|
||||
`reservation_token` VARCHAR(128) NOT NULL,
|
||||
`admitted_at` BIGINT NOT NULL,
|
||||
`reserved_cost_units` BIGINT NOT NULL,
|
||||
`actual_cost_units` BIGINT,
|
||||
`state` VARCHAR(20) NOT NULL,
|
||||
`reservation_expires_at` BIGINT NOT NULL,
|
||||
`retain_until` BIGINT NOT NULL,
|
||||
`finalized_at` BIGINT,
|
||||
`created_at` BIGINT NOT NULL,
|
||||
`updated_at` BIGINT NOT NULL,
|
||||
PRIMARY KEY (`reservation_token`),
|
||||
CONSTRAINT usage_cost_reservations_state_check
|
||||
CHECK (`state` IN ('reserved', 'finalized', 'released')),
|
||||
CONSTRAINT usage_cost_reservations_reserved_cost_units_check
|
||||
CHECK (`reserved_cost_units` >= 0),
|
||||
CONSTRAINT usage_cost_reservations_actual_cost_units_check
|
||||
CHECK (`actual_cost_units` IS NULL OR `actual_cost_units` >= 0),
|
||||
CONSTRAINT usage_cost_reservations_expiry_check
|
||||
CHECK (`reservation_expires_at` > `admitted_at`),
|
||||
CONSTRAINT usage_cost_reservations_retention_check
|
||||
CHECK (`retain_until` >= `reservation_expires_at`),
|
||||
CONSTRAINT usage_cost_reservations_lifecycle_check CHECK (
|
||||
(`state` = 'reserved' AND `actual_cost_units` IS NULL AND `finalized_at` IS NULL)
|
||||
OR (`state` = 'finalized' AND `actual_cost_units` IS NOT NULL AND `finalized_at` IS NOT NULL)
|
||||
OR (`state` = 'released' AND `actual_cost_units` IS NOT NULL
|
||||
AND `actual_cost_units` = 0 AND `finalized_at` IS NOT NULL)
|
||||
),
|
||||
KEY usage_cost_reservations_request_id_idx (`request_id`),
|
||||
KEY usage_cost_reservations_subject_admitted_at_idx (`subject_id`, `admitted_at`),
|
||||
KEY usage_cost_reservations_reservation_expires_at_idx (`reservation_expires_at`),
|
||||
KEY usage_cost_reservations_retain_until_token_idx (`retain_until`, `reservation_token`)
|
||||
);
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
CREATE TABLE IF NOT EXISTS usage_request_admissions (
|
||||
`request_id` VARCHAR(128) NOT NULL,
|
||||
`subject_id` VARCHAR(128) NOT NULL,
|
||||
`event_token` VARCHAR(128) NOT NULL,
|
||||
`admitted_at` BIGINT NOT NULL,
|
||||
`retain_until` BIGINT NOT NULL,
|
||||
`state` VARCHAR(20) NOT NULL,
|
||||
`released_at` BIGINT,
|
||||
`created_at` BIGINT NOT NULL,
|
||||
PRIMARY KEY (`event_token`),
|
||||
CONSTRAINT usage_request_admissions_retention_check
|
||||
CHECK (`retain_until` > `admitted_at`),
|
||||
CONSTRAINT usage_request_admissions_state_check
|
||||
CHECK (`state` IN ('active', 'released')),
|
||||
CONSTRAINT usage_request_admissions_lifecycle_check CHECK (
|
||||
(`state` = 'active' AND `released_at` IS NULL)
|
||||
OR (`state` = 'released' AND `released_at` IS NOT NULL
|
||||
AND `released_at` >= `admitted_at`)
|
||||
),
|
||||
KEY usage_request_admissions_subject_admitted_at_idx (`subject_id`, `admitted_at`),
|
||||
KEY usage_request_admissions_retain_until_token_idx (`retain_until`, `event_token`)
|
||||
);
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
-- Preserve the already-published cost-ledger migration checksum. This
|
||||
-- follow-up also upgrades databases which ran the feature branch before user
|
||||
-- ownership was enforced. Keep one ALTER TABLE per migration because MySQL
|
||||
-- DDL implicitly commits.
|
||||
DELETE reservation
|
||||
FROM usage_cost_reservations AS reservation
|
||||
LEFT JOIN users AS app_user ON app_user.id = reservation.subject_id
|
||||
WHERE app_user.id IS NULL;
|
||||
|
||||
ALTER TABLE usage_cost_reservations
|
||||
ADD CONSTRAINT usage_cost_reservations_subject_id_fkey
|
||||
FOREIGN KEY (`subject_id`) REFERENCES users (`id`) ON DELETE CASCADE;
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
-- Split from the cost-ledger foreign key so a MySQL implicit DDL commit cannot
|
||||
-- leave two table changes behind one dirty migration record.
|
||||
DELETE admission
|
||||
FROM usage_request_admissions AS admission
|
||||
LEFT JOIN users AS app_user ON app_user.id = admission.subject_id
|
||||
WHERE app_user.id IS NULL;
|
||||
|
||||
ALTER TABLE usage_request_admissions
|
||||
ADD CONSTRAINT usage_request_admissions_subject_id_fkey
|
||||
FOREIGN KEY (`subject_id`) REFERENCES users (`id`) ON DELETE CASCADE;
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
-- A gateway transaction identifier may repeat across payment methods, but
|
||||
-- must never identify two orders in the same normalized method. MySQL commits
|
||||
-- persistent DDL implicitly, so reject historical conflicts before any
|
||||
-- persistent UPDATE or ALTER TABLE. CREATE/DROP TEMPORARY TABLE do not cause an
|
||||
-- implicit commit, and the leading DROP also makes a same-session retry safe.
|
||||
-- Diagnose with:
|
||||
-- SELECT LOWER(TRIM(payment_method)),
|
||||
-- CONVERT(gateway_order_id USING utf8mb4) COLLATE utf8mb4_0900_bin,
|
||||
-- COUNT(*)
|
||||
-- FROM payment_orders
|
||||
-- WHERE gateway_order_id IS NOT NULL
|
||||
-- GROUP BY LOWER(TRIM(payment_method)),
|
||||
-- CONVERT(gateway_order_id USING utf8mb4) COLLATE utf8mb4_0900_bin
|
||||
-- HAVING COUNT(*) > 1;
|
||||
DROP TEMPORARY TABLE IF EXISTS aether_payment_gateway_order_uniqueness_preflight;
|
||||
|
||||
CREATE TEMPORARY TABLE aether_payment_gateway_order_uniqueness_preflight (
|
||||
conflict_marker TINYINT NOT NULL PRIMARY KEY
|
||||
);
|
||||
|
||||
INSERT INTO aether_payment_gateway_order_uniqueness_preflight (conflict_marker)
|
||||
VALUES (1);
|
||||
|
||||
-- Inserting the same marker fails on the first conflicting group. The grouping
|
||||
-- mirrors the values and collations used by the normalization and final index:
|
||||
-- payment methods use their existing column collation after LOWER/TRIM, while
|
||||
-- opaque gateway identifiers use MySQL 8's case-sensitive binary collation.
|
||||
INSERT INTO aether_payment_gateway_order_uniqueness_preflight (conflict_marker)
|
||||
SELECT 1
|
||||
FROM payment_orders
|
||||
WHERE gateway_order_id IS NOT NULL
|
||||
GROUP BY
|
||||
LOWER(TRIM(payment_method)),
|
||||
CONVERT(gateway_order_id USING utf8mb4) COLLATE utf8mb4_0900_bin
|
||||
HAVING COUNT(*) > 1
|
||||
LIMIT 1;
|
||||
|
||||
DROP TEMPORARY TABLE aether_payment_gateway_order_uniqueness_preflight;
|
||||
|
||||
UPDATE payment_orders
|
||||
SET payment_method = LOWER(TRIM(payment_method))
|
||||
WHERE BINARY payment_method <> BINARY LOWER(TRIM(payment_method));
|
||||
|
||||
UPDATE payment_callbacks
|
||||
SET payment_method = LOWER(TRIM(payment_method))
|
||||
WHERE BINARY payment_method <> BINARY LOWER(TRIM(payment_method));
|
||||
|
||||
-- Gateway identifiers are opaque and case-sensitive. Changing the column
|
||||
-- collation and adding the unique index in one ALTER avoids a persistent
|
||||
-- intermediate schema if either operation fails.
|
||||
ALTER TABLE payment_orders
|
||||
MODIFY COLUMN gateway_order_id VARCHAR(128)
|
||||
CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_bin NULL,
|
||||
ADD UNIQUE INDEX uq_payment_orders_payment_method_gateway_order_id
|
||||
(payment_method, gateway_order_id);
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
ALTER TABLE users
|
||||
ADD COLUMN security_version BIGINT NOT NULL DEFAULT 0;
|
||||
|
||||
ALTER TABLE user_sessions
|
||||
ADD COLUMN security_version BIGINT NOT NULL DEFAULT 0;
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
UPDATE request_candidates
|
||||
SET
|
||||
username = NULL,
|
||||
api_key_name = NULL,
|
||||
extra_data = NULL,
|
||||
required_capabilities = NULL,
|
||||
error_message = NULL,
|
||||
error_type = NULL,
|
||||
skip_reason = NULL
|
||||
WHERE username IS NOT NULL
|
||||
OR api_key_name IS NOT NULL
|
||||
OR extra_data IS NOT NULL
|
||||
OR required_capabilities IS NOT NULL
|
||||
OR error_message IS NOT NULL
|
||||
OR error_type IS NOT NULL
|
||||
OR skip_reason IS NOT NULL;
|
||||
+124
@@ -0,0 +1,124 @@
|
||||
-- Historical HTTP captures predate the deny-by-default persistence policy and
|
||||
-- may contain credentials or request content. Keep the usage and projected
|
||||
-- billing facts, but remove every legacy copy of the raw exchange.
|
||||
UPDATE `usage` AS usage_rows
|
||||
LEFT JOIN usage_cost_reservations AS reservation
|
||||
ON reservation.state = 'reserved'
|
||||
AND REGEXP_LIKE(
|
||||
reservation.reservation_token,
|
||||
'^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$',
|
||||
'c'
|
||||
)
|
||||
AND reservation.request_id = usage_rows.request_id
|
||||
AND reservation.subject_id = usage_rows.user_id
|
||||
AND reservation.reservation_token = JSON_UNQUOTE(JSON_EXTRACT(
|
||||
CASE
|
||||
WHEN JSON_VALID(usage_rows.request_metadata) = 1
|
||||
THEN usage_rows.request_metadata
|
||||
ELSE JSON_OBJECT()
|
||||
END,
|
||||
'$.plan_usage_reservation_token'
|
||||
))
|
||||
SET usage_rows.error_message = NULL,
|
||||
usage_rows.error_category = CASE
|
||||
WHEN usage_rows.error_category IS NULL
|
||||
OR TRIM(usage_rows.error_category) = '' THEN NULL
|
||||
WHEN LOWER(TRIM(usage_rows.error_category)) IN (
|
||||
'auth', 'cancelled', 'client_error', 'http_error',
|
||||
'non_success_status', 'provider_error', 'rate_limit', 'redirect',
|
||||
'server_error', 'stream_missing_terminal_event',
|
||||
'stream_terminal_error', 'upstream_error'
|
||||
) THEN LOWER(TRIM(usage_rows.error_category))
|
||||
ELSE 'other_error'
|
||||
END,
|
||||
usage_rows.request_headers = NULL,
|
||||
usage_rows.request_body = NULL,
|
||||
usage_rows.provider_request_headers = NULL,
|
||||
usage_rows.provider_request_body = NULL,
|
||||
usage_rows.response_headers = NULL,
|
||||
usage_rows.response_body = NULL,
|
||||
usage_rows.client_response_headers = NULL,
|
||||
usage_rows.client_response_body = NULL,
|
||||
usage_rows.request_body_compressed = NULL,
|
||||
usage_rows.provider_request_body_compressed = NULL,
|
||||
usage_rows.response_body_compressed = NULL,
|
||||
usage_rows.client_response_body_compressed = NULL,
|
||||
usage_rows.request_metadata = CASE
|
||||
WHEN reservation.reservation_token IS NULL THEN NULL
|
||||
WHEN JSON_TYPE(JSON_EXTRACT(
|
||||
CASE
|
||||
WHEN JSON_VALID(usage_rows.request_metadata) = 1
|
||||
THEN usage_rows.request_metadata
|
||||
ELSE JSON_OBJECT()
|
||||
END,
|
||||
'$.plan_usage_reservation_deferred'
|
||||
)) = 'BOOLEAN'
|
||||
AND JSON_UNQUOTE(JSON_EXTRACT(
|
||||
CASE
|
||||
WHEN JSON_VALID(usage_rows.request_metadata) = 1
|
||||
THEN usage_rows.request_metadata
|
||||
ELSE JSON_OBJECT()
|
||||
END,
|
||||
'$.plan_usage_reservation_deferred'
|
||||
)) = 'true'
|
||||
THEN JSON_SET(
|
||||
JSON_OBJECT(
|
||||
'plan_usage_reservation_token', reservation.reservation_token
|
||||
),
|
||||
'$.plan_usage_reservation_deferred',
|
||||
JSON_EXTRACT('true', '$')
|
||||
)
|
||||
ELSE JSON_OBJECT(
|
||||
'plan_usage_reservation_token', reservation.reservation_token
|
||||
)
|
||||
END
|
||||
WHERE usage_rows.error_message IS NOT NULL
|
||||
OR usage_rows.error_category IS NOT NULL
|
||||
OR usage_rows.request_headers IS NOT NULL
|
||||
OR usage_rows.request_body IS NOT NULL
|
||||
OR usage_rows.provider_request_headers IS NOT NULL
|
||||
OR usage_rows.provider_request_body IS NOT NULL
|
||||
OR usage_rows.response_headers IS NOT NULL
|
||||
OR usage_rows.response_body IS NOT NULL
|
||||
OR usage_rows.client_response_headers IS NOT NULL
|
||||
OR usage_rows.client_response_body IS NOT NULL
|
||||
OR usage_rows.request_body_compressed IS NOT NULL
|
||||
OR usage_rows.provider_request_body_compressed IS NOT NULL
|
||||
OR usage_rows.response_body_compressed IS NOT NULL
|
||||
OR usage_rows.client_response_body_compressed IS NOT NULL
|
||||
OR usage_rows.request_metadata IS NOT NULL;
|
||||
|
||||
DELETE FROM usage_http_audits;
|
||||
|
||||
DELETE FROM usage_body_blobs;
|
||||
|
||||
-- All fields needed by billing reads are projected into typed columns on this
|
||||
-- table. The historical JSON documents include rule expressions, catalog
|
||||
-- snapshots, and arbitrary dimensions, so they are not retained.
|
||||
UPDATE usage_settlement_snapshots
|
||||
SET settlement_snapshot = NULL,
|
||||
billing_dimensions = NULL
|
||||
WHERE settlement_snapshot IS NOT NULL
|
||||
OR billing_dimensions IS NOT NULL;
|
||||
|
||||
UPDATE video_tasks
|
||||
SET original_request_body = NULL,
|
||||
converted_request_body = NULL,
|
||||
progress_message = NULL,
|
||||
error_message = NULL,
|
||||
request_metadata = NULL,
|
||||
video_url = NULL,
|
||||
video_urls = NULL,
|
||||
thumbnail_url = NULL,
|
||||
stored_video_path = NULL,
|
||||
webhook_url = NULL
|
||||
WHERE original_request_body IS NOT NULL
|
||||
OR converted_request_body IS NOT NULL
|
||||
OR progress_message IS NOT NULL
|
||||
OR error_message IS NOT NULL
|
||||
OR request_metadata IS NOT NULL
|
||||
OR video_url IS NOT NULL
|
||||
OR video_urls IS NOT NULL
|
||||
OR thumbnail_url IS NOT NULL
|
||||
OR stored_video_path IS NOT NULL
|
||||
OR webhook_url IS NOT NULL;
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
UPDATE background_task_runs
|
||||
SET owner_instance = NULL,
|
||||
created_by = CASE
|
||||
WHEN LOWER(TRIM(created_by)) IN ('admin', 'scheduler', 'system')
|
||||
THEN LOWER(TRIM(created_by))
|
||||
ELSE NULL
|
||||
END,
|
||||
progress_message = NULL,
|
||||
payload_json = NULL,
|
||||
result_json = NULL,
|
||||
error_message = CASE
|
||||
WHEN status = 'failed' THEN 'background_task_failed'
|
||||
ELSE NULL
|
||||
END
|
||||
WHERE owner_instance IS NOT NULL
|
||||
OR created_by IS NOT NULL
|
||||
OR progress_message IS NOT NULL
|
||||
OR payload_json IS NOT NULL
|
||||
OR result_json IS NOT NULL
|
||||
OR error_message IS NOT NULL;
|
||||
|
||||
UPDATE background_task_events
|
||||
SET event_type = CASE
|
||||
WHEN event_type IN (
|
||||
'cancel_requested', 'failed', 'queued', 'running',
|
||||
'skipped', 'succeeded', 'worker_boot'
|
||||
) THEN event_type
|
||||
ELSE 'unclassified_event'
|
||||
END,
|
||||
message = CASE
|
||||
WHEN event_type IN (
|
||||
'cancel_requested', 'failed', 'queued', 'running',
|
||||
'skipped', 'succeeded', 'worker_boot'
|
||||
) THEN event_type
|
||||
ELSE 'unclassified_event'
|
||||
END,
|
||||
payload_json = NULL
|
||||
WHERE message <> event_type
|
||||
OR payload_json IS NOT NULL
|
||||
OR event_type NOT IN (
|
||||
'cancel_requested', 'failed', 'queued', 'running',
|
||||
'skipped', 'succeeded', 'worker_boot'
|
||||
);
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
-- Stripe PaymentIntent client secrets are payment capabilities. New writes
|
||||
-- store only encrypted values; legacy plaintext cannot be migrated safely
|
||||
-- without the application encryption key, so remove it fail-closed.
|
||||
UPDATE payment_orders
|
||||
SET status = CASE
|
||||
WHEN LOWER(TRIM(payment_method)) = 'stripe' AND status = 'pending'
|
||||
THEN 'expired'
|
||||
ELSE status
|
||||
END,
|
||||
gateway_response = JSON_REMOVE(gateway_response, '$.client_secret')
|
||||
WHERE gateway_response IS NOT NULL
|
||||
AND JSON_VALID(gateway_response) = 1
|
||||
AND JSON_CONTAINS_PATH(gateway_response, 'one', '$.client_secret') = 1;
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
-- Callback idempotency uses payload_hash and does not require the raw body.
|
||||
-- Older versions stored provider-controlled payloads that may contain payment
|
||||
-- capabilities or customer PII, so remove those legacy copies fail-closed.
|
||||
UPDATE payment_callbacks
|
||||
SET payload = NULL
|
||||
WHERE payload IS NOT NULL;
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
-- Video prompts are user content and are not required for polling or billing.
|
||||
-- Remove historical copies now that new writes discard them before persistence.
|
||||
UPDATE video_tasks
|
||||
SET prompt = NULL
|
||||
WHERE prompt IS NOT NULL;
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
-- Identity claims required by the application are stored in dedicated columns.
|
||||
-- Remove legacy provider-controlled userinfo JSON, which may contain unrelated
|
||||
-- PII or credentials and is not needed for authentication or account binding.
|
||||
UPDATE user_oauth_links
|
||||
SET extra_data = NULL
|
||||
WHERE extra_data IS NOT NULL;
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
-- Purpose-bound Fernet envelopes can exceed the former 500-character plaintext limit.
|
||||
ALTER TABLE proxy_nodes
|
||||
MODIFY COLUMN proxy_password TEXT NULL;
|
||||
+377
@@ -0,0 +1,377 @@
|
||||
SET @aether_drop_fact_user_fk_sql := IF(
|
||||
EXISTS (
|
||||
SELECT 1 FROM information_schema.TABLE_CONSTRAINTS
|
||||
WHERE CONSTRAINT_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'user_plan_entitlements'
|
||||
AND CONSTRAINT_NAME = 'user_plan_entitlements_user_id_fkey'
|
||||
AND CONSTRAINT_TYPE = 'FOREIGN KEY'
|
||||
),
|
||||
'ALTER TABLE user_plan_entitlements DROP FOREIGN KEY user_plan_entitlements_user_id_fkey',
|
||||
'DO 0'
|
||||
);
|
||||
PREPARE aether_drop_fact_user_fk_stmt FROM @aether_drop_fact_user_fk_sql;
|
||||
EXECUTE aether_drop_fact_user_fk_stmt;
|
||||
DEALLOCATE PREPARE aether_drop_fact_user_fk_stmt;
|
||||
|
||||
SET @aether_drop_fact_user_fk_sql := IF(
|
||||
EXISTS (
|
||||
SELECT 1 FROM information_schema.TABLE_CONSTRAINTS
|
||||
WHERE CONSTRAINT_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'entitlement_usage_ledgers'
|
||||
AND CONSTRAINT_NAME = 'entitlement_usage_ledgers_user_id_fkey'
|
||||
AND CONSTRAINT_TYPE = 'FOREIGN KEY'
|
||||
),
|
||||
'ALTER TABLE entitlement_usage_ledgers DROP FOREIGN KEY entitlement_usage_ledgers_user_id_fkey',
|
||||
'DO 0'
|
||||
);
|
||||
PREPARE aether_drop_fact_user_fk_stmt FROM @aether_drop_fact_user_fk_sql;
|
||||
EXECUTE aether_drop_fact_user_fk_stmt;
|
||||
DEALLOCATE PREPARE aether_drop_fact_user_fk_stmt;
|
||||
|
||||
SET @aether_drop_fact_user_fk_sql := IF(
|
||||
EXISTS (
|
||||
SELECT 1 FROM information_schema.TABLE_CONSTRAINTS
|
||||
WHERE CONSTRAINT_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'user_referrals'
|
||||
AND CONSTRAINT_NAME = 'user_referrals_inviter_user_id_fkey'
|
||||
AND CONSTRAINT_TYPE = 'FOREIGN KEY'
|
||||
),
|
||||
'ALTER TABLE user_referrals DROP FOREIGN KEY user_referrals_inviter_user_id_fkey',
|
||||
'DO 0'
|
||||
);
|
||||
PREPARE aether_drop_fact_user_fk_stmt FROM @aether_drop_fact_user_fk_sql;
|
||||
EXECUTE aether_drop_fact_user_fk_stmt;
|
||||
DEALLOCATE PREPARE aether_drop_fact_user_fk_stmt;
|
||||
|
||||
SET @aether_drop_fact_user_fk_sql := IF(
|
||||
EXISTS (
|
||||
SELECT 1 FROM information_schema.TABLE_CONSTRAINTS
|
||||
WHERE CONSTRAINT_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'user_referrals'
|
||||
AND CONSTRAINT_NAME = 'user_referrals_invitee_user_id_fkey'
|
||||
AND CONSTRAINT_TYPE = 'FOREIGN KEY'
|
||||
),
|
||||
'ALTER TABLE user_referrals DROP FOREIGN KEY user_referrals_invitee_user_id_fkey',
|
||||
'DO 0'
|
||||
);
|
||||
PREPARE aether_drop_fact_user_fk_stmt FROM @aether_drop_fact_user_fk_sql;
|
||||
EXECUTE aether_drop_fact_user_fk_stmt;
|
||||
DEALLOCATE PREPARE aether_drop_fact_user_fk_stmt;
|
||||
|
||||
SET @aether_drop_fact_user_fk_sql := IF(
|
||||
EXISTS (
|
||||
SELECT 1 FROM information_schema.TABLE_CONSTRAINTS
|
||||
WHERE CONSTRAINT_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'referral_rewards'
|
||||
AND CONSTRAINT_NAME = 'referral_rewards_inviter_user_id_fkey'
|
||||
AND CONSTRAINT_TYPE = 'FOREIGN KEY'
|
||||
),
|
||||
'ALTER TABLE referral_rewards DROP FOREIGN KEY referral_rewards_inviter_user_id_fkey',
|
||||
'DO 0'
|
||||
);
|
||||
PREPARE aether_drop_fact_user_fk_stmt FROM @aether_drop_fact_user_fk_sql;
|
||||
EXECUTE aether_drop_fact_user_fk_stmt;
|
||||
DEALLOCATE PREPARE aether_drop_fact_user_fk_stmt;
|
||||
|
||||
SET @aether_drop_fact_user_fk_sql := IF(
|
||||
EXISTS (
|
||||
SELECT 1 FROM information_schema.TABLE_CONSTRAINTS
|
||||
WHERE CONSTRAINT_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'referral_rewards'
|
||||
AND CONSTRAINT_NAME = 'referral_rewards_invitee_user_id_fkey'
|
||||
AND CONSTRAINT_TYPE = 'FOREIGN KEY'
|
||||
),
|
||||
'ALTER TABLE referral_rewards DROP FOREIGN KEY referral_rewards_invitee_user_id_fkey',
|
||||
'DO 0'
|
||||
);
|
||||
PREPARE aether_drop_fact_user_fk_stmt FROM @aether_drop_fact_user_fk_sql;
|
||||
EXECUTE aether_drop_fact_user_fk_stmt;
|
||||
DEALLOCATE PREPARE aether_drop_fact_user_fk_stmt;
|
||||
|
||||
UPDATE request_candidates
|
||||
SET username = NULL, api_key_name = NULL
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM users WHERE users.id = request_candidates.user_id
|
||||
)
|
||||
AND (username IS NOT NULL OR api_key_name IS NOT NULL);
|
||||
|
||||
UPDATE request_candidates
|
||||
SET api_key_name = NULL
|
||||
WHERE api_key_name IS NOT NULL
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM api_keys WHERE api_keys.id = request_candidates.api_key_id
|
||||
);
|
||||
|
||||
UPDATE video_tasks
|
||||
SET username = NULL, api_key_name = NULL
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM users WHERE users.id = video_tasks.user_id
|
||||
)
|
||||
AND (username IS NOT NULL OR api_key_name IS NOT NULL);
|
||||
|
||||
UPDATE video_tasks
|
||||
SET api_key_name = NULL
|
||||
WHERE api_key_name IS NOT NULL
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM api_keys WHERE api_keys.id = video_tasks.api_key_id
|
||||
);
|
||||
|
||||
UPDATE `usage`
|
||||
SET username = NULL, api_key_name = NULL
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM users WHERE users.id = `usage`.user_id
|
||||
)
|
||||
AND (username IS NOT NULL OR api_key_name IS NOT NULL);
|
||||
|
||||
UPDATE `usage`
|
||||
SET api_key_name = NULL
|
||||
WHERE api_key_name IS NOT NULL
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM api_keys WHERE api_keys.id = `usage`.api_key_id
|
||||
);
|
||||
|
||||
UPDATE stats_user_daily
|
||||
SET username = NULL
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM users WHERE users.id = stats_user_daily.user_id
|
||||
)
|
||||
AND username IS NOT NULL;
|
||||
|
||||
UPDATE stats_user_summary
|
||||
SET username = NULL
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM users WHERE users.id = stats_user_summary.user_id
|
||||
)
|
||||
AND username IS NOT NULL;
|
||||
|
||||
UPDATE stats_user_daily_model
|
||||
SET username = NULL
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM users WHERE users.id = stats_user_daily_model.user_id
|
||||
)
|
||||
AND username IS NOT NULL;
|
||||
|
||||
UPDATE stats_user_daily_provider
|
||||
SET username = NULL
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM users WHERE users.id = stats_user_daily_provider.user_id
|
||||
)
|
||||
AND username IS NOT NULL;
|
||||
|
||||
UPDATE stats_user_daily_api_format
|
||||
SET username = NULL
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM users WHERE users.id = stats_user_daily_api_format.user_id
|
||||
)
|
||||
AND username IS NOT NULL;
|
||||
|
||||
UPDATE stats_user_daily_model_provider
|
||||
SET username = NULL
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM users WHERE users.id = stats_user_daily_model_provider.user_id
|
||||
)
|
||||
AND username IS NOT NULL;
|
||||
|
||||
UPDATE stats_user_daily_cost_savings
|
||||
SET username = NULL
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM users WHERE users.id = stats_user_daily_cost_savings.user_id
|
||||
)
|
||||
AND username IS NOT NULL;
|
||||
|
||||
UPDATE stats_user_daily_cost_savings_provider
|
||||
SET username = NULL
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM users WHERE users.id = stats_user_daily_cost_savings_provider.user_id
|
||||
)
|
||||
AND username IS NOT NULL;
|
||||
|
||||
UPDATE stats_user_daily_cost_savings_model
|
||||
SET username = NULL
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM users WHERE users.id = stats_user_daily_cost_savings_model.user_id
|
||||
)
|
||||
AND username IS NOT NULL;
|
||||
|
||||
UPDATE stats_user_daily_cost_savings_model_provider
|
||||
SET username = NULL
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM users WHERE users.id = stats_user_daily_cost_savings_model_provider.user_id
|
||||
)
|
||||
AND username IS NOT NULL;
|
||||
|
||||
UPDATE stats_daily_api_key
|
||||
SET api_key_name = NULL
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM api_keys WHERE api_keys.id = stats_daily_api_key.api_key_id
|
||||
)
|
||||
AND api_key_name IS NOT NULL;
|
||||
|
||||
UPDATE user_plan_entitlements AS entitlement
|
||||
SET status = CASE WHEN status = 'active' THEN 'revoked' ELSE status END,
|
||||
expires_at = LEAST(expires_at, UNIX_TIMESTAMP()),
|
||||
updated_at = UNIX_TIMESTAMP()
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM users WHERE users.id = entitlement.user_id
|
||||
);
|
||||
|
||||
UPDATE wallets AS wallet
|
||||
SET status = 'disabled', updated_at = UNIX_TIMESTAMP()
|
||||
WHERE (wallet.user_id IS NOT NULL AND NOT EXISTS (
|
||||
SELECT 1 FROM users WHERE users.id = wallet.user_id
|
||||
))
|
||||
OR (wallet.api_key_id IS NOT NULL AND NOT EXISTS (
|
||||
SELECT 1 FROM api_keys WHERE api_keys.id = wallet.api_key_id
|
||||
));
|
||||
|
||||
UPDATE user_referrals AS referral
|
||||
SET invite_code_snapshot = 'deleted-user',
|
||||
source_json = NULL,
|
||||
updated_at = UNIX_TIMESTAMP()
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM users WHERE users.id = referral.inviter_user_id
|
||||
)
|
||||
OR NOT EXISTS (
|
||||
SELECT 1 FROM users WHERE users.id = referral.invitee_user_id
|
||||
);
|
||||
|
||||
UPDATE referral_rewards AS reward
|
||||
SET status = CASE
|
||||
WHEN status IN ('pending', 'failed', 'applying') THEN 'voided'
|
||||
ELSE status
|
||||
END,
|
||||
failure_reason = NULL,
|
||||
admin_note = NULL,
|
||||
updated_at = UNIX_TIMESTAMP()
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM users WHERE users.id = reward.inviter_user_id
|
||||
)
|
||||
OR NOT EXISTS (
|
||||
SELECT 1 FROM users WHERE users.id = reward.invitee_user_id
|
||||
);
|
||||
|
||||
UPDATE audit_logs AS history
|
||||
SET description = 'deleted user event',
|
||||
ip_address = NULL,
|
||||
user_agent = NULL,
|
||||
event_metadata = NULL,
|
||||
error_message = NULL
|
||||
WHERE (history.user_id IS NOT NULL AND NOT EXISTS (
|
||||
SELECT 1 FROM users WHERE users.id = history.user_id
|
||||
))
|
||||
OR (history.api_key_id IS NOT NULL AND NOT EXISTS (
|
||||
SELECT 1 FROM api_keys WHERE api_keys.id = history.api_key_id
|
||||
));
|
||||
|
||||
UPDATE wallet_transactions AS history
|
||||
SET description = NULL
|
||||
WHERE EXISTS (
|
||||
SELECT 1
|
||||
FROM wallets AS wallet
|
||||
WHERE wallet.id = history.wallet_id
|
||||
AND (
|
||||
(wallet.user_id IS NOT NULL AND NOT EXISTS (
|
||||
SELECT 1 FROM users WHERE users.id = wallet.user_id
|
||||
))
|
||||
OR (wallet.api_key_id IS NOT NULL AND NOT EXISTS (
|
||||
SELECT 1 FROM api_keys WHERE api_keys.id = wallet.api_key_id
|
||||
))
|
||||
)
|
||||
);
|
||||
|
||||
UPDATE wallet_transactions AS history
|
||||
SET description = NULL
|
||||
WHERE history.operator_id IS NOT NULL
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM users WHERE users.id = history.operator_id
|
||||
);
|
||||
|
||||
UPDATE payment_orders AS history
|
||||
SET gateway_response = NULL
|
||||
WHERE (history.user_id IS NOT NULL AND NOT EXISTS (
|
||||
SELECT 1 FROM users WHERE users.id = history.user_id
|
||||
))
|
||||
OR EXISTS (
|
||||
SELECT 1
|
||||
FROM wallets AS wallet
|
||||
WHERE wallet.id = history.wallet_id
|
||||
AND (
|
||||
(wallet.user_id IS NOT NULL AND NOT EXISTS (
|
||||
SELECT 1 FROM users WHERE users.id = wallet.user_id
|
||||
))
|
||||
OR (wallet.api_key_id IS NOT NULL AND NOT EXISTS (
|
||||
SELECT 1 FROM api_keys WHERE api_keys.id = wallet.api_key_id
|
||||
))
|
||||
)
|
||||
);
|
||||
|
||||
UPDATE payment_callbacks AS history
|
||||
SET payload = NULL,
|
||||
error_message = NULL
|
||||
WHERE EXISTS (
|
||||
SELECT 1
|
||||
FROM payment_orders AS payment_order
|
||||
LEFT JOIN wallets AS wallet ON wallet.id = payment_order.wallet_id
|
||||
WHERE (
|
||||
payment_order.id = history.payment_order_id
|
||||
OR (history.order_no IS NOT NULL AND payment_order.order_no = history.order_no)
|
||||
)
|
||||
AND (
|
||||
(payment_order.user_id IS NOT NULL AND NOT EXISTS (
|
||||
SELECT 1 FROM users WHERE users.id = payment_order.user_id
|
||||
))
|
||||
OR (wallet.user_id IS NOT NULL AND NOT EXISTS (
|
||||
SELECT 1 FROM users WHERE users.id = wallet.user_id
|
||||
))
|
||||
OR (wallet.api_key_id IS NOT NULL AND NOT EXISTS (
|
||||
SELECT 1 FROM api_keys WHERE api_keys.id = wallet.api_key_id
|
||||
))
|
||||
)
|
||||
);
|
||||
|
||||
UPDATE refund_requests AS history
|
||||
SET reason = NULL,
|
||||
payout_reference = NULL,
|
||||
payout_proof = NULL,
|
||||
failure_reason = NULL
|
||||
WHERE (history.user_id IS NOT NULL AND NOT EXISTS (
|
||||
SELECT 1 FROM users WHERE users.id = history.user_id
|
||||
))
|
||||
OR EXISTS (
|
||||
SELECT 1
|
||||
FROM wallets AS wallet
|
||||
WHERE wallet.id = history.wallet_id
|
||||
AND (
|
||||
(wallet.user_id IS NOT NULL AND NOT EXISTS (
|
||||
SELECT 1 FROM users WHERE users.id = wallet.user_id
|
||||
))
|
||||
OR (wallet.api_key_id IS NOT NULL AND NOT EXISTS (
|
||||
SELECT 1 FROM api_keys WHERE api_keys.id = wallet.api_key_id
|
||||
))
|
||||
)
|
||||
)
|
||||
OR (history.requested_by IS NOT NULL AND NOT EXISTS (
|
||||
SELECT 1 FROM users WHERE users.id = history.requested_by
|
||||
))
|
||||
OR (history.approved_by IS NOT NULL AND NOT EXISTS (
|
||||
SELECT 1 FROM users WHERE users.id = history.approved_by
|
||||
))
|
||||
OR (history.processed_by IS NOT NULL AND NOT EXISTS (
|
||||
SELECT 1 FROM users WHERE users.id = history.processed_by
|
||||
));
|
||||
|
||||
UPDATE referral_rewards AS reward
|
||||
SET failure_reason = NULL,
|
||||
admin_note = NULL,
|
||||
updated_at = UNIX_TIMESTAMP()
|
||||
WHERE reward.admin_operator_id IS NOT NULL
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM users WHERE users.id = reward.admin_operator_id
|
||||
);
|
||||
|
||||
UPDATE redeem_code_batches AS history
|
||||
SET description = NULL
|
||||
WHERE history.created_by IS NOT NULL
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM users WHERE users.id = history.created_by
|
||||
);
|
||||
+451
@@ -0,0 +1,451 @@
|
||||
-- Keep nullable/system-owned history rows intact while clearing values tied to
|
||||
-- deleted users or API keys. Raw payment callback payloads are always removed
|
||||
-- because they are not required for idempotency and may contain PII.
|
||||
UPDATE request_candidates
|
||||
SET username = NULL, api_key_name = NULL
|
||||
WHERE user_id IS NOT NULL
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM users
|
||||
WHERE users.id = request_candidates.user_id AND users.is_deleted = 0
|
||||
)
|
||||
AND (username IS NOT NULL OR api_key_name IS NOT NULL);
|
||||
|
||||
UPDATE request_candidates
|
||||
SET api_key_name = NULL
|
||||
WHERE api_key_id IS NOT NULL
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM api_keys
|
||||
JOIN users AS api_key_owner
|
||||
ON api_key_owner.id = api_keys.user_id AND api_key_owner.is_deleted = 0
|
||||
WHERE api_keys.id = request_candidates.api_key_id
|
||||
)
|
||||
AND api_key_name IS NOT NULL;
|
||||
|
||||
UPDATE video_tasks
|
||||
SET username = NULL, api_key_name = NULL
|
||||
WHERE user_id IS NOT NULL
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM users
|
||||
WHERE users.id = video_tasks.user_id AND users.is_deleted = 0
|
||||
)
|
||||
AND (username IS NOT NULL OR api_key_name IS NOT NULL);
|
||||
|
||||
UPDATE video_tasks
|
||||
SET api_key_name = NULL
|
||||
WHERE api_key_id IS NOT NULL
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM api_keys
|
||||
JOIN users AS api_key_owner
|
||||
ON api_key_owner.id = api_keys.user_id AND api_key_owner.is_deleted = 0
|
||||
WHERE api_keys.id = video_tasks.api_key_id
|
||||
)
|
||||
AND api_key_name IS NOT NULL;
|
||||
|
||||
UPDATE `usage`
|
||||
SET username = NULL, api_key_name = NULL
|
||||
WHERE user_id IS NOT NULL
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM users
|
||||
WHERE users.id = `usage`.user_id AND users.is_deleted = 0
|
||||
)
|
||||
AND (username IS NOT NULL OR api_key_name IS NOT NULL);
|
||||
|
||||
UPDATE `usage`
|
||||
SET api_key_name = NULL
|
||||
WHERE api_key_id IS NOT NULL
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM api_keys
|
||||
JOIN users AS api_key_owner
|
||||
ON api_key_owner.id = api_keys.user_id AND api_key_owner.is_deleted = 0
|
||||
WHERE api_keys.id = `usage`.api_key_id
|
||||
)
|
||||
AND api_key_name IS NOT NULL;
|
||||
|
||||
UPDATE stats_user_daily
|
||||
SET username = NULL
|
||||
WHERE user_id IS NOT NULL
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM users
|
||||
WHERE users.id = stats_user_daily.user_id AND users.is_deleted = 0
|
||||
)
|
||||
AND username IS NOT NULL;
|
||||
|
||||
UPDATE stats_user_summary
|
||||
SET username = NULL
|
||||
WHERE user_id IS NOT NULL
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM users
|
||||
WHERE users.id = stats_user_summary.user_id AND users.is_deleted = 0
|
||||
)
|
||||
AND username IS NOT NULL;
|
||||
|
||||
UPDATE stats_user_daily_model
|
||||
SET username = NULL
|
||||
WHERE user_id IS NOT NULL
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM users
|
||||
WHERE users.id = stats_user_daily_model.user_id AND users.is_deleted = 0
|
||||
)
|
||||
AND username IS NOT NULL;
|
||||
|
||||
UPDATE stats_user_daily_provider
|
||||
SET username = NULL
|
||||
WHERE user_id IS NOT NULL
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM users
|
||||
WHERE users.id = stats_user_daily_provider.user_id AND users.is_deleted = 0
|
||||
)
|
||||
AND username IS NOT NULL;
|
||||
|
||||
UPDATE stats_user_daily_api_format
|
||||
SET username = NULL
|
||||
WHERE user_id IS NOT NULL
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM users
|
||||
WHERE users.id = stats_user_daily_api_format.user_id AND users.is_deleted = 0
|
||||
)
|
||||
AND username IS NOT NULL;
|
||||
|
||||
UPDATE stats_user_daily_model_provider
|
||||
SET username = NULL
|
||||
WHERE user_id IS NOT NULL
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM users
|
||||
WHERE users.id = stats_user_daily_model_provider.user_id AND users.is_deleted = 0
|
||||
)
|
||||
AND username IS NOT NULL;
|
||||
|
||||
UPDATE stats_user_daily_cost_savings
|
||||
SET username = NULL
|
||||
WHERE user_id IS NOT NULL
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM users
|
||||
WHERE users.id = stats_user_daily_cost_savings.user_id AND users.is_deleted = 0
|
||||
)
|
||||
AND username IS NOT NULL;
|
||||
|
||||
UPDATE stats_user_daily_cost_savings_provider
|
||||
SET username = NULL
|
||||
WHERE user_id IS NOT NULL
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM users
|
||||
WHERE users.id = stats_user_daily_cost_savings_provider.user_id
|
||||
AND users.is_deleted = 0
|
||||
)
|
||||
AND username IS NOT NULL;
|
||||
|
||||
UPDATE stats_user_daily_cost_savings_model
|
||||
SET username = NULL
|
||||
WHERE user_id IS NOT NULL
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM users
|
||||
WHERE users.id = stats_user_daily_cost_savings_model.user_id
|
||||
AND users.is_deleted = 0
|
||||
)
|
||||
AND username IS NOT NULL;
|
||||
|
||||
UPDATE stats_user_daily_cost_savings_model_provider
|
||||
SET username = NULL
|
||||
WHERE user_id IS NOT NULL
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM users
|
||||
WHERE users.id = stats_user_daily_cost_savings_model_provider.user_id
|
||||
AND users.is_deleted = 0
|
||||
)
|
||||
AND username IS NOT NULL;
|
||||
|
||||
UPDATE stats_daily_api_key
|
||||
SET api_key_name = NULL
|
||||
WHERE api_key_id IS NOT NULL
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM api_keys
|
||||
JOIN users AS api_key_owner
|
||||
ON api_key_owner.id = api_keys.user_id AND api_key_owner.is_deleted = 0
|
||||
WHERE api_keys.id = stats_daily_api_key.api_key_id
|
||||
)
|
||||
AND api_key_name IS NOT NULL;
|
||||
|
||||
UPDATE user_plan_entitlements AS entitlement
|
||||
SET status = CASE WHEN status = 'active' THEN 'revoked' ELSE status END,
|
||||
expires_at = LEAST(expires_at, UNIX_TIMESTAMP()),
|
||||
updated_at = UNIX_TIMESTAMP()
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM users
|
||||
WHERE users.id = entitlement.user_id AND users.is_deleted = 0
|
||||
);
|
||||
|
||||
UPDATE user_referrals AS referral
|
||||
SET invite_code_snapshot = 'deleted-user',
|
||||
source_json = NULL,
|
||||
updated_at = UNIX_TIMESTAMP()
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM users
|
||||
WHERE users.id = referral.inviter_user_id AND users.is_deleted = 0
|
||||
)
|
||||
OR NOT EXISTS (
|
||||
SELECT 1 FROM users
|
||||
WHERE users.id = referral.invitee_user_id AND users.is_deleted = 0
|
||||
);
|
||||
|
||||
UPDATE referral_rewards AS reward
|
||||
SET status = CASE
|
||||
WHEN status IN ('pending', 'failed', 'applying') THEN 'voided'
|
||||
ELSE status
|
||||
END,
|
||||
failure_reason = NULL,
|
||||
admin_note = NULL,
|
||||
updated_at = UNIX_TIMESTAMP()
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM users
|
||||
WHERE users.id = reward.inviter_user_id AND users.is_deleted = 0
|
||||
)
|
||||
OR NOT EXISTS (
|
||||
SELECT 1 FROM users
|
||||
WHERE users.id = reward.invitee_user_id AND users.is_deleted = 0
|
||||
);
|
||||
|
||||
UPDATE referral_rewards AS reward
|
||||
SET failure_reason = NULL,
|
||||
admin_note = NULL,
|
||||
updated_at = UNIX_TIMESTAMP()
|
||||
WHERE reward.admin_operator_id IS NOT NULL
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM users
|
||||
WHERE users.id = reward.admin_operator_id AND users.is_deleted = 0
|
||||
);
|
||||
|
||||
UPDATE wallets AS wallet
|
||||
SET status = 'disabled', updated_at = UNIX_TIMESTAMP()
|
||||
WHERE (wallet.user_id IS NULL AND wallet.api_key_id IS NULL)
|
||||
OR (wallet.user_id IS NOT NULL AND wallet.api_key_id IS NOT NULL)
|
||||
OR (wallet.user_id IS NOT NULL
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM users
|
||||
WHERE users.id = wallet.user_id AND users.is_deleted = 0
|
||||
))
|
||||
OR (wallet.api_key_id IS NOT NULL
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM api_keys
|
||||
JOIN users AS api_key_owner
|
||||
ON api_key_owner.id = api_keys.user_id AND api_key_owner.is_deleted = 0
|
||||
WHERE api_keys.id = wallet.api_key_id
|
||||
));
|
||||
|
||||
UPDATE audit_logs AS history
|
||||
SET description = 'deleted user event',
|
||||
ip_address = NULL,
|
||||
user_agent = NULL,
|
||||
event_metadata = NULL,
|
||||
error_message = NULL
|
||||
WHERE (history.user_id IS NOT NULL
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM users
|
||||
WHERE users.id = history.user_id AND users.is_deleted = 0
|
||||
))
|
||||
OR (history.api_key_id IS NOT NULL
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM api_keys
|
||||
JOIN users AS api_key_owner
|
||||
ON api_key_owner.id = api_keys.user_id AND api_key_owner.is_deleted = 0
|
||||
WHERE api_keys.id = history.api_key_id
|
||||
));
|
||||
|
||||
UPDATE wallet_transactions AS history
|
||||
SET description = NULL
|
||||
WHERE EXISTS (
|
||||
SELECT 1
|
||||
FROM wallets AS wallet
|
||||
WHERE wallet.id = history.wallet_id
|
||||
AND ((wallet.user_id IS NULL AND wallet.api_key_id IS NULL)
|
||||
OR (wallet.user_id IS NOT NULL AND wallet.api_key_id IS NOT NULL)
|
||||
OR (wallet.user_id IS NOT NULL
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM users
|
||||
WHERE users.id = wallet.user_id AND users.is_deleted = 0
|
||||
))
|
||||
OR (wallet.api_key_id IS NOT NULL
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM api_keys
|
||||
JOIN users AS api_key_owner
|
||||
ON api_key_owner.id = api_keys.user_id
|
||||
AND api_key_owner.is_deleted = 0
|
||||
WHERE api_keys.id = wallet.api_key_id
|
||||
)))
|
||||
)
|
||||
OR NOT EXISTS (
|
||||
SELECT 1 FROM wallets AS wallet WHERE wallet.id = history.wallet_id
|
||||
)
|
||||
OR (history.operator_id IS NOT NULL
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM users
|
||||
WHERE users.id = history.operator_id AND users.is_deleted = 0
|
||||
));
|
||||
|
||||
UPDATE payment_orders AS history
|
||||
SET gateway_response = NULL
|
||||
WHERE (history.user_id IS NOT NULL
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM users
|
||||
WHERE users.id = history.user_id AND users.is_deleted = 0
|
||||
))
|
||||
OR EXISTS (
|
||||
SELECT 1
|
||||
FROM wallets AS wallet
|
||||
WHERE wallet.id = history.wallet_id
|
||||
AND ((wallet.user_id IS NULL AND wallet.api_key_id IS NULL)
|
||||
OR (wallet.user_id IS NOT NULL AND wallet.api_key_id IS NOT NULL)
|
||||
OR (wallet.user_id IS NOT NULL
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM users
|
||||
WHERE users.id = wallet.user_id AND users.is_deleted = 0
|
||||
))
|
||||
OR (wallet.api_key_id IS NOT NULL
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM api_keys
|
||||
JOIN users AS api_key_owner
|
||||
ON api_key_owner.id = api_keys.user_id
|
||||
AND api_key_owner.is_deleted = 0
|
||||
WHERE api_keys.id = wallet.api_key_id
|
||||
)))
|
||||
)
|
||||
OR NOT EXISTS (
|
||||
SELECT 1 FROM wallets AS wallet WHERE wallet.id = history.wallet_id
|
||||
);
|
||||
|
||||
-- Raw provider payloads are never needed for idempotency and must be purged
|
||||
-- even when an old callback cannot be linked back to an order.
|
||||
UPDATE payment_callbacks
|
||||
SET payload = NULL
|
||||
WHERE payload IS NOT NULL;
|
||||
|
||||
UPDATE payment_callbacks AS history
|
||||
SET error_message = NULL
|
||||
WHERE history.error_message IS NOT NULL
|
||||
AND (
|
||||
NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM payment_orders AS payment_order
|
||||
WHERE payment_order.id = history.payment_order_id
|
||||
OR (history.order_no IS NOT NULL AND payment_order.order_no = history.order_no)
|
||||
)
|
||||
OR EXISTS (
|
||||
SELECT 1
|
||||
FROM payment_orders AS payment_order
|
||||
LEFT JOIN wallets AS wallet ON wallet.id = payment_order.wallet_id
|
||||
WHERE (payment_order.id = history.payment_order_id
|
||||
OR (history.order_no IS NOT NULL AND payment_order.order_no = history.order_no))
|
||||
AND ((payment_order.user_id IS NOT NULL
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM users
|
||||
WHERE users.id = payment_order.user_id AND users.is_deleted = 0
|
||||
))
|
||||
OR wallet.id IS NULL
|
||||
OR (wallet.user_id IS NULL AND wallet.api_key_id IS NULL)
|
||||
OR (wallet.user_id IS NOT NULL AND wallet.api_key_id IS NOT NULL)
|
||||
OR (wallet.user_id IS NOT NULL
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM users
|
||||
WHERE users.id = wallet.user_id AND users.is_deleted = 0
|
||||
))
|
||||
OR (wallet.api_key_id IS NOT NULL
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM api_keys
|
||||
JOIN users AS api_key_owner
|
||||
ON api_key_owner.id = api_keys.user_id
|
||||
AND api_key_owner.is_deleted = 0
|
||||
WHERE api_keys.id = wallet.api_key_id
|
||||
)))
|
||||
)
|
||||
);
|
||||
|
||||
UPDATE refund_requests AS history
|
||||
SET reason = NULL,
|
||||
payout_reference = NULL,
|
||||
payout_proof = NULL,
|
||||
failure_reason = NULL
|
||||
WHERE (history.user_id IS NOT NULL
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM users
|
||||
WHERE users.id = history.user_id AND users.is_deleted = 0
|
||||
))
|
||||
OR EXISTS (
|
||||
SELECT 1
|
||||
FROM wallets AS wallet
|
||||
WHERE wallet.id = history.wallet_id
|
||||
AND ((wallet.user_id IS NULL AND wallet.api_key_id IS NULL)
|
||||
OR (wallet.user_id IS NOT NULL AND wallet.api_key_id IS NOT NULL)
|
||||
OR (wallet.user_id IS NOT NULL
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM users
|
||||
WHERE users.id = wallet.user_id AND users.is_deleted = 0
|
||||
))
|
||||
OR (wallet.api_key_id IS NOT NULL
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM api_keys
|
||||
JOIN users AS api_key_owner
|
||||
ON api_key_owner.id = api_keys.user_id
|
||||
AND api_key_owner.is_deleted = 0
|
||||
WHERE api_keys.id = wallet.api_key_id
|
||||
)))
|
||||
)
|
||||
OR NOT EXISTS (
|
||||
SELECT 1 FROM wallets AS wallet WHERE wallet.id = history.wallet_id
|
||||
)
|
||||
OR (history.requested_by IS NOT NULL
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM users
|
||||
WHERE users.id = history.requested_by AND users.is_deleted = 0
|
||||
))
|
||||
OR (history.approved_by IS NOT NULL
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM users
|
||||
WHERE users.id = history.approved_by AND users.is_deleted = 0
|
||||
))
|
||||
OR (history.processed_by IS NOT NULL
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM users
|
||||
WHERE users.id = history.processed_by AND users.is_deleted = 0
|
||||
));
|
||||
|
||||
UPDATE redeem_code_batches AS history
|
||||
SET description = NULL
|
||||
WHERE history.created_by IS NOT NULL
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM users
|
||||
WHERE users.id = history.created_by AND users.is_deleted = 0
|
||||
);
|
||||
|
||||
UPDATE refund_requests
|
||||
SET requested_by = NULL
|
||||
WHERE requested_by IS NOT NULL
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM users
|
||||
WHERE users.id = refund_requests.requested_by AND users.is_deleted = 0
|
||||
);
|
||||
|
||||
UPDATE refund_requests
|
||||
SET approved_by = NULL
|
||||
WHERE approved_by IS NOT NULL
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM users
|
||||
WHERE users.id = refund_requests.approved_by AND users.is_deleted = 0
|
||||
);
|
||||
|
||||
UPDATE refund_requests
|
||||
SET processed_by = NULL
|
||||
WHERE processed_by IS NOT NULL
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM users
|
||||
WHERE users.id = refund_requests.processed_by AND users.is_deleted = 0
|
||||
);
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
-- LDAP configuration is a database-wide singleton. Preserve the row selected by the legacy
|
||||
-- reader (the smallest id), remove historical duplicates, and let the database arbitrate
|
||||
-- concurrent first creation.
|
||||
DELETE FROM ldap_configs
|
||||
WHERE id <> (
|
||||
SELECT keep_id
|
||||
FROM (SELECT MIN(id) AS keep_id FROM ldap_configs) AS ldap_singleton_keeper
|
||||
);
|
||||
|
||||
ALTER TABLE ldap_configs
|
||||
ADD COLUMN singleton_key INT NOT NULL DEFAULT 1,
|
||||
ADD CONSTRAINT ldap_configs_singleton_key_check CHECK (singleton_key = 1),
|
||||
ADD UNIQUE KEY ldap_configs_singleton_key_key (singleton_key);
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
ALTER TABLE proxy_nodes
|
||||
ADD COLUMN tunnel_generation VARCHAR(64) NULL AFTER id;
|
||||
|
||||
UPDATE proxy_nodes
|
||||
SET tunnel_generation = UUID()
|
||||
WHERE tunnel_generation IS NULL OR TRIM(tunnel_generation) = '';
|
||||
|
||||
ALTER TABLE proxy_nodes
|
||||
MODIFY COLUMN tunnel_generation VARCHAR(64) NOT NULL;
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
-- A proxy endpoint has one stable node identity across manual and tunnel
|
||||
-- registrations. MySQL 8 performs ALTER TABLE atomically; if historical
|
||||
-- duplicates exist this migration fails without choosing or deleting a row.
|
||||
-- Diagnose with:
|
||||
-- SELECT ip, port, COUNT(*)
|
||||
-- FROM proxy_nodes
|
||||
-- GROUP BY ip, port
|
||||
-- HAVING COUNT(*) > 1;
|
||||
ALTER TABLE proxy_nodes
|
||||
ADD UNIQUE INDEX uq_proxy_node_ip_port (ip, port);
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE usage_counter_deltas
|
||||
ADD COLUMN target_tunnel_generation VARCHAR(64) NULL;
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
-- Older gateway releases marked every OAuth email as verified without
|
||||
-- retaining verification provenance. Reset those claims conservatively; a
|
||||
-- later trusted OAuth assertion for the same normalized email can upgrade it.
|
||||
UPDATE users
|
||||
SET email_verified = 0,
|
||||
updated_at = UNIX_TIMESTAMP()
|
||||
WHERE LOWER(TRIM(auth_source)) = 'oauth'
|
||||
AND email_verified = 1;
|
||||
Reference in New Issue
Block a user