Commit 110702d4 authored by 陈曦's avatar 陈曦
Browse files

merge v0.1.119

parent 8cc113dc
Pipeline #82274 passed with stage
in 4 minutes and 19 seconds
ALTER TABLE users
ADD COLUMN IF NOT EXISTS signup_source VARCHAR(20) NOT NULL DEFAULT 'email',
ADD COLUMN IF NOT EXISTS last_login_at TIMESTAMPTZ NULL,
ADD COLUMN IF NOT EXISTS last_active_at TIMESTAMPTZ NULL;
UPDATE users
SET signup_source = 'email'
WHERE signup_source IS NULL OR signup_source = '';
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'users_signup_source_check'
) THEN
ALTER TABLE users
ADD CONSTRAINT users_signup_source_check
CHECK (signup_source IN ('email', 'linuxdo', 'wechat', 'oidc'));
END IF;
END $$;
CREATE TABLE IF NOT EXISTS auth_identities (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
provider_type VARCHAR(20) NOT NULL,
provider_key TEXT NOT NULL,
provider_subject TEXT NOT NULL,
verified_at TIMESTAMPTZ NULL,
issuer TEXT NULL,
metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT auth_identities_provider_type_check
CHECK (provider_type IN ('email', 'linuxdo', 'wechat', 'oidc'))
);
CREATE UNIQUE INDEX IF NOT EXISTS auth_identities_provider_subject_key
ON auth_identities (provider_type, provider_key, provider_subject);
CREATE INDEX IF NOT EXISTS auth_identities_user_id_idx
ON auth_identities (user_id);
CREATE INDEX IF NOT EXISTS auth_identities_user_provider_idx
ON auth_identities (user_id, provider_type);
CREATE TABLE IF NOT EXISTS auth_identity_channels (
id BIGSERIAL PRIMARY KEY,
identity_id BIGINT NOT NULL REFERENCES auth_identities(id) ON DELETE CASCADE,
provider_type VARCHAR(20) NOT NULL,
provider_key TEXT NOT NULL,
channel VARCHAR(20) NOT NULL,
channel_app_id TEXT NOT NULL,
channel_subject TEXT NOT NULL,
metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT auth_identity_channels_provider_type_check
CHECK (provider_type IN ('email', 'linuxdo', 'wechat', 'oidc'))
);
CREATE UNIQUE INDEX IF NOT EXISTS auth_identity_channels_channel_key
ON auth_identity_channels (provider_type, provider_key, channel, channel_app_id, channel_subject);
CREATE INDEX IF NOT EXISTS auth_identity_channels_identity_id_idx
ON auth_identity_channels (identity_id);
CREATE TABLE IF NOT EXISTS pending_auth_sessions (
id BIGSERIAL PRIMARY KEY,
session_token VARCHAR(255) NOT NULL,
intent VARCHAR(40) NOT NULL,
provider_type VARCHAR(20) NOT NULL,
provider_key TEXT NOT NULL,
provider_subject TEXT NOT NULL,
target_user_id BIGINT NULL REFERENCES users(id) ON DELETE SET NULL,
redirect_to TEXT NOT NULL DEFAULT '',
resolved_email TEXT NOT NULL DEFAULT '',
registration_password_hash TEXT NOT NULL DEFAULT '',
upstream_identity_claims JSONB NOT NULL DEFAULT '{}'::jsonb,
local_flow_state JSONB NOT NULL DEFAULT '{}'::jsonb,
browser_session_key TEXT NOT NULL DEFAULT '',
completion_code_hash TEXT NOT NULL DEFAULT '',
completion_code_expires_at TIMESTAMPTZ NULL,
email_verified_at TIMESTAMPTZ NULL,
password_verified_at TIMESTAMPTZ NULL,
totp_verified_at TIMESTAMPTZ NULL,
expires_at TIMESTAMPTZ NOT NULL,
consumed_at TIMESTAMPTZ NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT pending_auth_sessions_intent_check
CHECK (intent IN ('login', 'bind_current_user', 'adopt_existing_user_by_email')),
CONSTRAINT pending_auth_sessions_provider_type_check
CHECK (provider_type IN ('email', 'linuxdo', 'wechat', 'oidc'))
);
CREATE UNIQUE INDEX IF NOT EXISTS pending_auth_sessions_session_token_key
ON pending_auth_sessions (session_token);
CREATE INDEX IF NOT EXISTS pending_auth_sessions_target_user_id_idx
ON pending_auth_sessions (target_user_id);
CREATE INDEX IF NOT EXISTS pending_auth_sessions_expires_at_idx
ON pending_auth_sessions (expires_at);
CREATE INDEX IF NOT EXISTS pending_auth_sessions_provider_idx
ON pending_auth_sessions (provider_type, provider_key, provider_subject);
CREATE INDEX IF NOT EXISTS pending_auth_sessions_completion_code_idx
ON pending_auth_sessions (completion_code_hash);
CREATE TABLE IF NOT EXISTS identity_adoption_decisions (
id BIGSERIAL PRIMARY KEY,
pending_auth_session_id BIGINT NOT NULL REFERENCES pending_auth_sessions(id) ON DELETE CASCADE,
identity_id BIGINT NULL REFERENCES auth_identities(id) ON DELETE SET NULL,
adopt_display_name BOOLEAN NOT NULL DEFAULT FALSE,
adopt_avatar BOOLEAN NOT NULL DEFAULT FALSE,
decided_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE UNIQUE INDEX IF NOT EXISTS identity_adoption_decisions_pending_auth_session_id_key
ON identity_adoption_decisions (pending_auth_session_id);
CREATE INDEX IF NOT EXISTS identity_adoption_decisions_identity_id_idx
ON identity_adoption_decisions (identity_id);
CREATE TABLE IF NOT EXISTS auth_identity_migration_reports (
id BIGSERIAL PRIMARY KEY,
report_type VARCHAR(40) NOT NULL,
report_key TEXT NOT NULL,
details JSONB NOT NULL DEFAULT '{}'::jsonb,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS auth_identity_migration_reports_type_idx
ON auth_identity_migration_reports (report_type);
CREATE UNIQUE INDEX IF NOT EXISTS auth_identity_migration_reports_type_key
ON auth_identity_migration_reports (report_type, report_key);
DO $$
BEGIN
IF EXISTS (
SELECT 1
FROM information_schema.columns
WHERE table_schema = 'public'
AND table_name = 'auth_identity_migration_reports'
AND column_name = 'report_type'
AND COALESCE(character_maximum_length, 0) < 80
) THEN
ALTER TABLE auth_identity_migration_reports
ALTER COLUMN report_type TYPE VARCHAR(80);
END IF;
END $$;
INSERT INTO auth_identities (
user_id,
provider_type,
provider_key,
provider_subject,
verified_at,
metadata
)
SELECT
u.id,
'email',
'email',
LOWER(BTRIM(u.email)),
COALESCE(u.updated_at, u.created_at, NOW()),
jsonb_build_object(
'backfill_source', 'users.email',
'migration', '109_auth_identity_compat_backfill'
)
FROM users AS u
WHERE u.deleted_at IS NULL
AND BTRIM(COALESCE(u.email, '')) <> ''
AND RIGHT(LOWER(BTRIM(u.email)), LENGTH('@linuxdo-connect.invalid')) <> '@linuxdo-connect.invalid'
AND RIGHT(LOWER(BTRIM(u.email)), LENGTH('@oidc-connect.invalid')) <> '@oidc-connect.invalid'
AND RIGHT(LOWER(BTRIM(u.email)), LENGTH('@wechat-connect.invalid')) <> '@wechat-connect.invalid'
ON CONFLICT (provider_type, provider_key, provider_subject) DO NOTHING;
INSERT INTO auth_identities (
user_id,
provider_type,
provider_key,
provider_subject,
verified_at,
metadata
)
SELECT
u.id,
'linuxdo',
'linuxdo',
SUBSTRING(BTRIM(u.email) FROM '(?i)^linuxdo-(.+)@linuxdo-connect\.invalid$'),
COALESCE(u.updated_at, u.created_at, NOW()),
jsonb_build_object(
'backfill_source', 'synthetic_email',
'legacy_email', BTRIM(u.email),
'migration', '109_auth_identity_compat_backfill'
)
FROM users AS u
WHERE u.deleted_at IS NULL
AND LOWER(BTRIM(u.email)) ~ '^linuxdo-.+@linuxdo-connect\.invalid$'
ON CONFLICT (provider_type, provider_key, provider_subject) DO NOTHING;
INSERT INTO auth_identities (
user_id,
provider_type,
provider_key,
provider_subject,
verified_at,
metadata
)
SELECT
u.id,
'wechat',
'wechat',
SUBSTRING(BTRIM(u.email) FROM '(?i)^wechat-(.+)@wechat-connect\.invalid$'),
COALESCE(u.updated_at, u.created_at, NOW()),
jsonb_build_object(
'backfill_source', 'synthetic_email',
'legacy_email', BTRIM(u.email),
'migration', '109_auth_identity_compat_backfill'
)
FROM users AS u
WHERE u.deleted_at IS NULL
AND LOWER(BTRIM(u.email)) ~ '^wechat-.+@wechat-connect\.invalid$'
ON CONFLICT (provider_type, provider_key, provider_subject) DO NOTHING;
UPDATE users
SET signup_source = 'linuxdo'
WHERE deleted_at IS NULL
AND LOWER(BTRIM(COALESCE(email, ''))) ~ '^linuxdo-.+@linuxdo-connect\.invalid$';
UPDATE users
SET signup_source = 'wechat'
WHERE deleted_at IS NULL
AND LOWER(BTRIM(COALESCE(email, ''))) ~ '^wechat-.+@wechat-connect\.invalid$';
UPDATE users
SET signup_source = 'oidc'
WHERE deleted_at IS NULL
AND LOWER(BTRIM(COALESCE(email, ''))) ~ '^oidc-.+@oidc-connect\.invalid$';
INSERT INTO auth_identity_migration_reports (report_type, report_key, details)
SELECT
'oidc_synthetic_email_requires_manual_recovery',
CAST(u.id AS TEXT),
jsonb_build_object(
'user_id', u.id,
'email', LOWER(BTRIM(u.email)),
'reason', 'cannot recover issuer_plus_sub deterministically from synthetic email alone',
'migration', '109_auth_identity_compat_backfill'
)
FROM users AS u
WHERE u.deleted_at IS NULL
AND LOWER(BTRIM(u.email)) ~ '^oidc-.+@oidc-connect\.invalid$'
ON CONFLICT (report_type, report_key) DO NOTHING;
INSERT INTO auth_identity_migration_reports (report_type, report_key, details)
SELECT
'wechat_openid_only_requires_remediation',
CAST(u.id AS TEXT),
jsonb_build_object(
'user_id', u.id,
'email', LOWER(BTRIM(u.email)),
'reason', 'legacy wechat synthetic identity requires explicit unionid remediation if channel-only data exists',
'migration', '109_auth_identity_compat_backfill'
)
FROM users AS u
WHERE u.deleted_at IS NULL
AND LOWER(BTRIM(u.email)) ~ '^wechat-.+@wechat-connect\.invalid$'
AND NOT EXISTS (
SELECT 1
FROM auth_identities ai
WHERE ai.user_id = u.id
AND ai.provider_type = 'wechat'
AND ai.provider_key = 'wechat'
)
ON CONFLICT (report_type, report_key) DO NOTHING;
CREATE TABLE IF NOT EXISTS user_provider_default_grants (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
provider_type VARCHAR(20) NOT NULL,
grant_reason VARCHAR(20) NOT NULL DEFAULT 'first_bind',
granted_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT user_provider_default_grants_provider_type_check
CHECK (provider_type IN ('email', 'linuxdo', 'wechat', 'oidc')),
CONSTRAINT user_provider_default_grants_reason_check
CHECK (grant_reason IN ('signup', 'first_bind'))
);
CREATE UNIQUE INDEX IF NOT EXISTS user_provider_default_grants_user_provider_reason_key
ON user_provider_default_grants (user_id, provider_type, grant_reason);
CREATE INDEX IF NOT EXISTS user_provider_default_grants_user_id_idx
ON user_provider_default_grants (user_id);
CREATE TABLE IF NOT EXISTS user_avatars (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
storage_provider VARCHAR(20) NOT NULL DEFAULT 'database',
storage_key TEXT NOT NULL DEFAULT '',
url TEXT NOT NULL DEFAULT '',
content_type VARCHAR(100) NOT NULL DEFAULT '',
byte_size INT NOT NULL DEFAULT 0,
sha256 VARCHAR(64) NOT NULL DEFAULT '',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE UNIQUE INDEX IF NOT EXISTS user_avatars_user_id_key
ON user_avatars (user_id);
INSERT INTO settings (key, value)
VALUES
('auth_source_default_email_balance', '0'),
('auth_source_default_email_concurrency', '5'),
('auth_source_default_email_subscriptions', '[]'),
('auth_source_default_email_grant_on_signup', 'false'),
('auth_source_default_email_grant_on_first_bind', 'false'),
('auth_source_default_linuxdo_balance', '0'),
('auth_source_default_linuxdo_concurrency', '5'),
('auth_source_default_linuxdo_subscriptions', '[]'),
('auth_source_default_linuxdo_grant_on_signup', 'false'),
('auth_source_default_linuxdo_grant_on_first_bind', 'false'),
('auth_source_default_oidc_balance', '0'),
('auth_source_default_oidc_concurrency', '5'),
('auth_source_default_oidc_subscriptions', '[]'),
('auth_source_default_oidc_grant_on_signup', 'false'),
('auth_source_default_oidc_grant_on_first_bind', 'false'),
('auth_source_default_wechat_balance', '0'),
('auth_source_default_wechat_concurrency', '5'),
('auth_source_default_wechat_subscriptions', '[]'),
('auth_source_default_wechat_grant_on_signup', 'false'),
('auth_source_default_wechat_grant_on_first_bind', 'false'),
('force_email_on_third_party_signup', 'false')
ON CONFLICT (key) DO NOTHING;
INSERT INTO settings (key, value)
VALUES
('payment_visible_method_alipay_source', ''),
('payment_visible_method_wxpay_source', ''),
('payment_visible_method_alipay_enabled', 'false'),
('payment_visible_method_wxpay_enabled', 'false'),
('openai_advanced_scheduler_enabled', 'false')
ON CONFLICT (key) DO NOTHING;
ALTER TABLE payment_orders ADD COLUMN IF NOT EXISTS provider_key VARCHAR(30);
UPDATE payment_orders
SET provider_key = (
SELECT provider_key
FROM payment_provider_instances
WHERE CAST(id AS TEXT) = payment_orders.provider_instance_id
)
WHERE provider_key IS NULL
AND provider_instance_id IS NOT NULL;
UPDATE auth_identities AS ai
SET
provider_key = 'wechat-main',
metadata = COALESCE(ai.metadata, '{}'::jsonb) || jsonb_build_object(
'legacy_provider_key', 'wechat',
'normalized_by_migration', '113_normalize_legacy_wechat_provider_key'
),
updated_at = NOW()
WHERE ai.provider_type = 'wechat'
AND ai.provider_key = 'wechat'
AND NOT EXISTS (
SELECT 1
FROM auth_identities AS canon
WHERE canon.provider_type = 'wechat'
AND canon.provider_key = 'wechat-main'
AND canon.provider_subject = ai.provider_subject
);
UPDATE auth_identity_channels AS channel
SET
provider_key = 'wechat-main',
metadata = COALESCE(channel.metadata, '{}'::jsonb) || jsonb_build_object(
'legacy_provider_key', 'wechat',
'normalized_by_migration', '113_normalize_legacy_wechat_provider_key'
),
updated_at = NOW()
WHERE channel.provider_type = 'wechat'
AND channel.provider_key = 'wechat'
AND NOT EXISTS (
SELECT 1
FROM auth_identity_channels AS canon
WHERE canon.provider_type = 'wechat'
AND canon.provider_key = 'wechat-main'
AND canon.channel = channel.channel
AND canon.channel_app_id = channel.channel_app_id
AND canon.channel_subject = channel.channel_subject
);
INSERT INTO auth_identity_migration_reports (report_type, report_key, details)
SELECT
'wechat_provider_key_conflict',
CAST(ai.id AS TEXT),
jsonb_build_object(
'legacy_identity_id', ai.id,
'legacy_user_id', ai.user_id,
'provider_subject', ai.provider_subject,
'canonical_identity_id', canon.id,
'canonical_user_id', canon.user_id,
'same_user', canon.user_id = ai.user_id,
'migration', '113_normalize_legacy_wechat_provider_key'
)
FROM auth_identities AS ai
JOIN auth_identities AS canon
ON canon.provider_type = 'wechat'
AND canon.provider_key = 'wechat-main'
AND canon.provider_subject = ai.provider_subject
WHERE ai.provider_type = 'wechat'
AND ai.provider_key = 'wechat'
ON CONFLICT (report_type, report_key) DO NOTHING;
INSERT INTO auth_identity_migration_reports (report_type, report_key, details)
SELECT
'wechat_channel_provider_key_conflict',
CAST(channel.id AS TEXT),
jsonb_build_object(
'legacy_channel_id', channel.id,
'legacy_identity_id', channel.identity_id,
'canonical_channel_id', canon.id,
'canonical_identity_id', canon.identity_id,
'channel', channel.channel,
'channel_app_id', channel.channel_app_id,
'channel_subject', channel.channel_subject,
'same_user', COALESCE(legacy_identity.user_id = canonical_identity.user_id, FALSE),
'migration', '113_normalize_legacy_wechat_provider_key'
)
FROM auth_identity_channels AS channel
JOIN auth_identity_channels AS canon
ON canon.provider_type = 'wechat'
AND canon.provider_key = 'wechat-main'
AND canon.channel = channel.channel
AND canon.channel_app_id = channel.channel_app_id
AND canon.channel_subject = channel.channel_subject
LEFT JOIN auth_identities AS legacy_identity
ON legacy_identity.id = channel.identity_id
LEFT JOIN auth_identities AS canonical_identity
ON canonical_identity.id = canon.identity_id
WHERE channel.provider_type = 'wechat'
AND channel.provider_key = 'wechat'
ON CONFLICT (report_type, report_key) DO NOTHING;
ALTER TABLE auth_identity_migration_reports
ADD COLUMN IF NOT EXISTS resolved_at TIMESTAMPTZ NULL;
ALTER TABLE auth_identity_migration_reports
ADD COLUMN IF NOT EXISTS resolved_by_user_id BIGINT NULL;
ALTER TABLE auth_identity_migration_reports
ADD COLUMN IF NOT EXISTS resolution_note TEXT NOT NULL DEFAULT '';
CREATE INDEX IF NOT EXISTS idx_auth_identity_migration_reports_resolved_at
ON auth_identity_migration_reports (resolved_at);
CREATE OR REPLACE FUNCTION public.__migration_115_safe_legacy_metadata_jsonb(input_text TEXT)
RETURNS JSONB
LANGUAGE plpgsql
AS $$
DECLARE
parsed JSONB;
BEGIN
IF input_text IS NULL OR BTRIM(input_text) = '' THEN
RETURN '{}'::jsonb;
END IF;
BEGIN
parsed := input_text::jsonb;
EXCEPTION
WHEN OTHERS THEN
RETURN '{}'::jsonb;
END;
IF jsonb_typeof(parsed) = 'object' THEN
RETURN parsed;
END IF;
RETURN jsonb_build_object('_legacy_metadata_raw_json', parsed);
END;
$$;
DO $$
BEGIN
IF to_regclass('public.user_external_identities') IS NULL THEN
RETURN;
END IF;
EXECUTE $sql$
WITH legacy AS (
SELECT
uei.id,
uei.user_id,
BTRIM(uei.provider_user_id) AS provider_user_id,
BTRIM(uei.provider_username) AS provider_username,
BTRIM(uei.display_name) AS display_name,
public.__migration_115_safe_legacy_metadata_jsonb(uei.metadata) AS metadata_json,
uei.created_at,
uei.updated_at
FROM user_external_identities AS uei
JOIN users AS u ON u.id = uei.user_id
WHERE u.deleted_at IS NULL
AND LOWER(BTRIM(COALESCE(uei.provider, ''))) = 'linuxdo'
AND BTRIM(COALESCE(uei.provider_user_id, '')) <> ''
),
legacy_subjects AS (
SELECT
provider_user_id AS provider_subject,
COUNT(DISTINCT user_id) AS distinct_user_count
FROM legacy
GROUP BY provider_user_id
),
canonical_legacy AS (
SELECT
legacy.*,
ROW_NUMBER() OVER (
PARTITION BY legacy.provider_user_id
ORDER BY COALESCE(legacy.updated_at, legacy.created_at, NOW()) DESC, legacy.id DESC
) AS canonical_row_num
FROM legacy
JOIN legacy_subjects AS subjects
ON subjects.provider_subject = legacy.provider_user_id
AND subjects.distinct_user_count = 1
)
INSERT INTO auth_identities (
user_id,
provider_type,
provider_key,
provider_subject,
verified_at,
metadata
)
SELECT
legacy.user_id,
'linuxdo',
'linuxdo',
legacy.provider_user_id,
COALESCE(legacy.updated_at, legacy.created_at, NOW()),
legacy.metadata_json || jsonb_build_object(
'legacy_identity_id', legacy.id,
'provider_user_id', legacy.provider_user_id,
'provider_username', legacy.provider_username,
'display_name', legacy.display_name,
'migration', '115_auth_identity_legacy_external_backfill'
)
FROM canonical_legacy AS legacy
WHERE legacy.canonical_row_num = 1
ON CONFLICT (provider_type, provider_key, provider_subject) DO NOTHING;
$sql$;
EXECUTE $sql$
WITH legacy AS (
SELECT
uei.id,
uei.user_id,
BTRIM(uei.provider_user_id) AS provider_user_id,
BTRIM(uei.provider_union_id) AS provider_union_id,
BTRIM(uei.provider_username) AS provider_username,
BTRIM(uei.display_name) AS display_name,
public.__migration_115_safe_legacy_metadata_jsonb(uei.metadata) AS metadata_json,
uei.created_at,
uei.updated_at
FROM user_external_identities AS uei
JOIN users AS u ON u.id = uei.user_id
WHERE u.deleted_at IS NULL
AND LOWER(BTRIM(COALESCE(uei.provider, ''))) = 'wechat'
AND BTRIM(COALESCE(uei.provider_union_id, '')) <> ''
),
legacy_subjects AS (
SELECT
provider_union_id AS provider_subject,
COUNT(DISTINCT user_id) AS distinct_user_count
FROM legacy
GROUP BY provider_union_id
),
canonical_legacy AS (
SELECT
legacy.*,
ROW_NUMBER() OVER (
PARTITION BY legacy.provider_union_id
ORDER BY COALESCE(legacy.updated_at, legacy.created_at, NOW()) DESC, legacy.id DESC
) AS canonical_row_num
FROM legacy
JOIN legacy_subjects AS subjects
ON subjects.provider_subject = legacy.provider_union_id
AND subjects.distinct_user_count = 1
)
INSERT INTO auth_identities (
user_id,
provider_type,
provider_key,
provider_subject,
verified_at,
metadata
)
SELECT
legacy.user_id,
'wechat',
'wechat-main',
legacy.provider_union_id,
COALESCE(legacy.updated_at, legacy.created_at, NOW()),
legacy.metadata_json || jsonb_build_object(
'legacy_identity_id', legacy.id,
'openid', legacy.provider_user_id,
'unionid', legacy.provider_union_id,
'provider_user_id', legacy.provider_user_id,
'provider_union_id', legacy.provider_union_id,
'provider_username', legacy.provider_username,
'display_name', legacy.display_name,
'migration', '115_auth_identity_legacy_external_backfill'
)
FROM canonical_legacy AS legacy
WHERE legacy.canonical_row_num = 1
ON CONFLICT (provider_type, provider_key, provider_subject) DO NOTHING;
$sql$;
EXECUTE $sql$
WITH legacy AS (
SELECT
uei.user_id,
BTRIM(uei.provider_user_id) AS provider_user_id,
BTRIM(uei.provider_union_id) AS provider_union_id,
BTRIM(COALESCE(meta.metadata_json ->> 'channel', '')) AS channel,
BTRIM(COALESCE(meta.metadata_json ->> 'channel_app_id', meta.metadata_json ->> 'appid', meta.metadata_json ->> 'app_id', '')) AS channel_app_id,
meta.metadata_json
FROM user_external_identities AS uei
JOIN users AS u ON u.id = uei.user_id
CROSS JOIN LATERAL (
SELECT public.__migration_115_safe_legacy_metadata_jsonb(uei.metadata) AS metadata_json
) AS meta
WHERE u.deleted_at IS NULL
AND LOWER(BTRIM(COALESCE(uei.provider, ''))) = 'wechat'
AND BTRIM(COALESCE(uei.provider_union_id, '')) <> ''
),
legacy_subjects AS (
SELECT
provider_union_id AS provider_subject,
COUNT(DISTINCT user_id) AS distinct_user_count
FROM legacy
GROUP BY provider_union_id
)
INSERT INTO auth_identity_channels (
identity_id,
provider_type,
provider_key,
channel,
channel_app_id,
channel_subject,
metadata
)
SELECT
ai.id,
'wechat',
'wechat-main',
legacy.channel,
legacy.channel_app_id,
legacy.provider_user_id,
legacy.metadata_json || jsonb_build_object(
'openid', legacy.provider_user_id,
'unionid', legacy.provider_union_id,
'migration', '115_auth_identity_legacy_external_backfill'
)
FROM legacy
JOIN legacy_subjects AS subjects
ON subjects.provider_subject = legacy.provider_union_id
AND subjects.distinct_user_count = 1
JOIN auth_identities AS ai
ON ai.user_id = legacy.user_id
AND ai.provider_type = 'wechat'
AND ai.provider_key = 'wechat-main'
AND ai.provider_subject = legacy.provider_union_id
WHERE legacy.channel <> ''
AND legacy.channel_app_id <> ''
AND legacy.provider_user_id <> ''
ON CONFLICT DO NOTHING;
$sql$;
EXECUTE $sql$
INSERT INTO auth_identity_migration_reports (report_type, report_key, details)
SELECT
'wechat_openid_only_requires_remediation',
'legacy_external_identity:' || legacy.id::text,
legacy.metadata_json || jsonb_build_object(
'legacy_identity_id', legacy.id,
'user_id', legacy.user_id,
'openid', legacy.provider_user_id,
'reason', 'legacy user_external_identities row only has openid and cannot be canonicalized offline',
'migration', '115_auth_identity_legacy_external_backfill'
)
FROM (
SELECT
uei.id,
uei.user_id,
BTRIM(uei.provider_user_id) AS provider_user_id,
public.__migration_115_safe_legacy_metadata_jsonb(uei.metadata) AS metadata_json
FROM user_external_identities AS uei
JOIN users AS u ON u.id = uei.user_id
WHERE u.deleted_at IS NULL
AND LOWER(BTRIM(COALESCE(uei.provider, ''))) = 'wechat'
AND BTRIM(COALESCE(uei.provider_user_id, '')) <> ''
AND BTRIM(COALESCE(uei.provider_union_id, '')) = ''
) AS legacy
ON CONFLICT (report_type, report_key) DO NOTHING;
$sql$;
END $$;
INSERT INTO auth_identity_migration_reports (report_type, report_key, details)
SELECT
'wechat_openid_only_requires_remediation',
'synthetic_auth_identity:' || ai.id::text,
COALESCE(ai.metadata, '{}'::jsonb) || jsonb_build_object(
'auth_identity_id', ai.id,
'user_id', ai.user_id,
'provider_subject', ai.provider_subject,
'reason', 'synthetic wechat auth identity still lacks unionid metadata and needs remediation',
'migration', '115_auth_identity_legacy_external_backfill'
)
FROM auth_identities AS ai
WHERE ai.provider_type = 'wechat'
AND COALESCE(ai.metadata ->> 'backfill_source', '') = 'synthetic_email'
AND BTRIM(COALESCE(ai.metadata ->> 'unionid', '')) = ''
ON CONFLICT (report_type, report_key) DO NOTHING;
DROP FUNCTION IF EXISTS public.__migration_115_safe_legacy_metadata_jsonb(TEXT);
ALTER TABLE payment_orders
ADD COLUMN IF NOT EXISTS provider_snapshot JSONB;
INSERT INTO settings (key, value)
VALUES
(
'wechat_connect_open_enabled',
CASE
WHEN NOT EXISTS (SELECT 1 FROM settings WHERE key = 'wechat_connect_enabled') THEN ''
WHEN COALESCE((SELECT value FROM settings WHERE key = 'wechat_connect_enabled'), 'false') <> 'true' THEN 'false'
WHEN LOWER(TRIM(COALESCE((SELECT value FROM settings WHERE key = 'wechat_connect_mode'), 'open'))) = 'mp' THEN 'false'
ELSE 'true'
END
),
(
'wechat_connect_mp_enabled',
CASE
WHEN NOT EXISTS (SELECT 1 FROM settings WHERE key = 'wechat_connect_enabled') THEN ''
WHEN COALESCE((SELECT value FROM settings WHERE key = 'wechat_connect_enabled'), 'false') <> 'true' THEN 'false'
WHEN LOWER(TRIM(COALESCE((SELECT value FROM settings WHERE key = 'wechat_connect_mode'), 'open'))) = 'mp' THEN 'true'
ELSE 'false'
END
),
('auth_source_default_email_grant_on_signup', 'false'),
('auth_source_default_linuxdo_grant_on_signup', 'false'),
('auth_source_default_oidc_grant_on_signup', 'false'),
('auth_source_default_wechat_grant_on_signup', 'false')
ON CONFLICT (key) DO NOTHING;
-- Intentionally left as a no-op.
-- The online index rollout lives in 120_enforce_payment_orders_out_trade_no_unique_notx.sql
DO $$
BEGIN
NULL;
END $$;
-- Build the payment order uniqueness guarantee online.
-- The migration runner performs an explicit duplicate out_trade_no precheck and
-- drops any stale invalid paymentorder_out_trade_no_unique index before retrying.
-- Create the new partial unique index concurrently first so writes keep flowing,
-- then remove the legacy index name once the replacement is ready.
CREATE UNIQUE INDEX CONCURRENTLY IF NOT EXISTS paymentorder_out_trade_no_unique
ON payment_orders (out_trade_no)
WHERE out_trade_no <> '';
DROP INDEX CONCURRENTLY IF EXISTS paymentorder_out_trade_no;
DO $$
BEGIN
IF EXISTS (
SELECT 1
FROM pg_indexes
WHERE schemaname = 'public'
AND tablename = 'payment_orders'
AND indexname = 'paymentorder_out_trade_no_unique'
) THEN
IF EXISTS (
SELECT 1
FROM pg_indexes
WHERE schemaname = 'public'
AND tablename = 'payment_orders'
AND indexname = 'paymentorder_out_trade_no'
) THEN
EXECUTE 'DROP INDEX IF EXISTS paymentorder_out_trade_no';
END IF;
EXECUTE 'ALTER INDEX paymentorder_out_trade_no_unique RENAME TO paymentorder_out_trade_no';
END IF;
END $$;
ALTER TABLE auth_identity_migration_reports
ALTER COLUMN report_type TYPE VARCHAR(80);
UPDATE pending_auth_sessions
SET
local_flow_state = jsonb_set(
local_flow_state,
'{completion_response}',
((local_flow_state -> 'completion_response') - 'access_token' - 'refresh_token' - 'expires_in' - 'token_type'),
true
)
WHERE jsonb_typeof(local_flow_state -> 'completion_response') = 'object'
AND (
(local_flow_state -> 'completion_response') ? 'access_token'
OR (local_flow_state -> 'completion_response') ? 'refresh_token'
OR (local_flow_state -> 'completion_response') ? 'expires_in'
OR (local_flow_state -> 'completion_response') ? 'token_type'
);
-- Auto-backfill untouched migration 110 signup-grant defaults to the corrected false value.
-- Rows still matching the migration-110 default payload and timestamp window are treated as
-- untouched legacy defaults; any remaining legacy true values are reported for manual review.
WITH migration_110 AS (
SELECT applied_at
FROM schema_migrations
WHERE filename = '110_pending_auth_and_provider_default_grants.sql'
),
providers AS (
SELECT provider_type
FROM (
VALUES ('email'), ('linuxdo'), ('oidc'), ('wechat')
) AS providers(provider_type)
),
legacy_provider_defaults AS (
SELECT providers.provider_type
FROM providers
CROSS JOIN migration_110
JOIN settings balance
ON balance.key = 'auth_source_default_' || providers.provider_type || '_balance'
JOIN settings concurrency
ON concurrency.key = 'auth_source_default_' || providers.provider_type || '_concurrency'
JOIN settings subscriptions
ON subscriptions.key = 'auth_source_default_' || providers.provider_type || '_subscriptions'
JOIN settings grant_on_signup
ON grant_on_signup.key = 'auth_source_default_' || providers.provider_type || '_grant_on_signup'
JOIN settings grant_on_first_bind
ON grant_on_first_bind.key = 'auth_source_default_' || providers.provider_type || '_grant_on_first_bind'
WHERE balance.value = '0'
AND concurrency.value = '5'
AND subscriptions.value = '[]'
AND grant_on_signup.value = 'true'
AND grant_on_first_bind.value = 'false'
AND balance.updated_at BETWEEN migration_110.applied_at - INTERVAL '1 minute' AND migration_110.applied_at + INTERVAL '1 minute'
AND concurrency.updated_at BETWEEN migration_110.applied_at - INTERVAL '1 minute' AND migration_110.applied_at + INTERVAL '1 minute'
AND subscriptions.updated_at BETWEEN migration_110.applied_at - INTERVAL '1 minute' AND migration_110.applied_at + INTERVAL '1 minute'
AND grant_on_signup.updated_at BETWEEN migration_110.applied_at - INTERVAL '1 minute' AND migration_110.applied_at + INTERVAL '1 minute'
AND grant_on_first_bind.updated_at BETWEEN migration_110.applied_at - INTERVAL '1 minute' AND migration_110.applied_at + INTERVAL '1 minute'
),
updated_signup_grants AS (
UPDATE settings
SET
value = 'false',
updated_at = NOW()
FROM legacy_provider_defaults
WHERE settings.key = 'auth_source_default_' || legacy_provider_defaults.provider_type || '_grant_on_signup'
AND settings.value = 'true'
RETURNING legacy_provider_defaults.provider_type
)
INSERT INTO auth_identity_migration_reports (report_type, report_key, details)
SELECT
'legacy_auth_source_signup_grant_review',
providers.provider_type,
jsonb_build_object(
'provider_type', providers.provider_type,
'current_value', grant_on_signup.value,
'auto_backfilled', FALSE,
'reason', 'legacy_true_default_not_auto_backfilled'
)
FROM providers
JOIN settings grant_on_signup
ON grant_on_signup.key = 'auth_source_default_' || providers.provider_type || '_grant_on_signup'
LEFT JOIN updated_signup_grants
ON updated_signup_grants.provider_type = providers.provider_type
WHERE grant_on_signup.value = 'true'
AND updated_signup_grants.provider_type IS NULL
ON CONFLICT (report_type, report_key) DO NOTHING;
-- Preserve legacy OIDC behavior for upgraded installs that predate the
-- introduction of secure PKCE/id_token defaults. Fresh installs continue to
-- inherit runtime defaults when these rows are absent.
WITH legacy_oidc_install AS (
SELECT 1
FROM settings
WHERE key IN (
'oidc_connect_enabled',
'oidc_connect_client_id',
'oidc_connect_authorize_url',
'oidc_connect_token_url',
'oidc_connect_issuer_url',
'oidc_connect_userinfo_url',
'oidc_connect_frontend_redirect_url'
)
LIMIT 1
)
INSERT INTO settings (key, value)
SELECT defaults.key, 'false'
FROM legacy_oidc_install
CROSS JOIN (
VALUES
('oidc_connect_use_pkce'),
('oidc_connect_validate_id_token')
) AS defaults(key)
WHERE NOT EXISTS (
SELECT 1
FROM settings existing
WHERE existing.key = defaults.key
)
ON CONFLICT (key) DO NOTHING;
-- Migration: 125_add_channel_monitors
-- 渠道监控 MVP:周期性对外部 provider/endpoint/api_key 做模型心跳测试。
--
-- 表结构说明:
-- - channel_monitors 渠道配置表(一行 = 一个监控对象)
-- - channel_monitor_histories 检测历史明细表(一次检测一个模型 = 一行)
--
-- 设计要点:
-- - api_key_encrypted 列存放 AES-256-GCM 密文(base64),由 service 层加密。
-- - extra_models 用 JSONB 存储字符串数组,便于扩展(后续可加权重等元数据)。
-- - history 表通过 ON DELETE CASCADE 自动清理已删除监控的历史。
-- - (enabled, last_checked_at) 索引服务于调度器扫描“到期需要检测”的监控。
-- - histories 上 (monitor_id, model, checked_at DESC) 服务用户视图聚合查询;
-- 单独的 (checked_at) 索引服务定期清理 30 天前数据的 DELETE。
CREATE TABLE IF NOT EXISTS channel_monitors (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
provider VARCHAR(20) NOT NULL, -- openai / anthropic / gemini
endpoint VARCHAR(500) NOT NULL, -- base origin
api_key_encrypted TEXT NOT NULL, -- AES-256-GCM (base64)
primary_model VARCHAR(200) NOT NULL,
extra_models JSONB NOT NULL DEFAULT '[]'::jsonb,
group_name VARCHAR(100) NOT NULL DEFAULT '',
enabled BOOLEAN NOT NULL DEFAULT TRUE,
interval_seconds INT NOT NULL,
last_checked_at TIMESTAMPTZ,
created_by BIGINT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT channel_monitors_provider_check CHECK (provider IN ('openai', 'anthropic', 'gemini')),
CONSTRAINT channel_monitors_interval_check CHECK (interval_seconds BETWEEN 15 AND 3600)
);
CREATE INDEX IF NOT EXISTS idx_channel_monitors_enabled_last_checked
ON channel_monitors (enabled, last_checked_at);
CREATE INDEX IF NOT EXISTS idx_channel_monitors_provider
ON channel_monitors (provider);
CREATE INDEX IF NOT EXISTS idx_channel_monitors_group_name
ON channel_monitors (group_name);
CREATE TABLE IF NOT EXISTS channel_monitor_histories (
id BIGSERIAL PRIMARY KEY,
monitor_id BIGINT NOT NULL REFERENCES channel_monitors(id) ON DELETE CASCADE,
model VARCHAR(200) NOT NULL,
status VARCHAR(20) NOT NULL,
latency_ms INT,
ping_latency_ms INT,
message VARCHAR(500) NOT NULL DEFAULT '',
checked_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT channel_monitor_histories_status_check
CHECK (status IN ('operational', 'degraded', 'failed', 'error'))
);
CREATE INDEX IF NOT EXISTS idx_channel_monitor_histories_monitor_model_checked
ON channel_monitor_histories (monitor_id, model, checked_at DESC);
CREATE INDEX IF NOT EXISTS idx_channel_monitor_histories_checked_at
ON channel_monitor_histories (checked_at);
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment