diff --git a/backend/internal/repository/auth_identity_legacy_migration_integration_test.go b/backend/internal/repository/auth_identity_legacy_migration_integration_test.go index 6a6312d4..7f2f363f 100644 --- a/backend/internal/repository/auth_identity_legacy_migration_integration_test.go +++ b/backend/internal/repository/auth_identity_legacy_migration_integration_test.go @@ -200,6 +200,252 @@ FROM auth_identity_migration_reports var afterCount int require.NoError(t, tx.QueryRowContext(ctx, ` SELECT COUNT(*) +FROM auth_identity_migration_reports + `).Scan(&afterCount)) + require.Equal(t, beforeCount, afterCount) +} + +func TestAuthIdentityLegacyExternalSafetyMigration_ReportsConflictsAndDowngradesInvalidJSON(t *testing.T) { + tx := testTx(t) + ctx := context.Background() + + migrationPath := filepath.Join("..", "..", "migrations", "116_auth_identity_legacy_external_safety_reports.sql") + migrationSQL, err := os.ReadFile(migrationPath) + require.NoError(t, err) + + _, err = tx.ExecContext(ctx, ` +CREATE TABLE IF NOT EXISTS user_external_identities ( + id BIGSERIAL PRIMARY KEY, + user_id BIGINT NOT NULL, + provider TEXT NOT NULL, + provider_user_id TEXT NOT NULL, + provider_union_id TEXT NULL, + provider_username TEXT NOT NULL DEFAULT '', + display_name TEXT NOT NULL DEFAULT '', + profile_url TEXT NOT NULL DEFAULT '', + avatar_url TEXT NOT NULL DEFAULT '', + metadata TEXT NOT NULL DEFAULT '{}', + created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP +); + + TRUNCATE TABLE + auth_identity_channels, + auth_identities, + auth_identity_migration_reports, + user_external_identities, + users + RESTART IDENTITY; +`) + require.NoError(t, err) + + userIDs := make([]int64, 0, 8) + for _, email := range []string{ + "linuxdo-conflict-legacy@example.com", + "linuxdo-conflict-owner@example.com", + "wechat-conflict-legacy@example.com", + "wechat-conflict-owner@example.com", + "wechat-channel-legacy@example.com", + "wechat-channel-owner@example.com", + "linuxdo-invalid-json@example.com", + "wechat-openid-invalid-json@example.com", + } { + var userID int64 + require.NoError(t, tx.QueryRowContext(ctx, ` +INSERT INTO users (email, password_hash, role, status, balance, concurrency) +VALUES ($1, 'hash', 'user', 'active', 0, 1) +RETURNING id`, email).Scan(&userID)) + userIDs = append(userIDs, userID) + } + + linuxdoConflictLegacyUserID := userIDs[0] + linuxdoConflictOwnerUserID := userIDs[1] + wechatConflictLegacyUserID := userIDs[2] + wechatConflictOwnerUserID := userIDs[3] + wechatChannelLegacyUserID := userIDs[4] + wechatChannelOwnerUserID := userIDs[5] + linuxdoInvalidJSONUserID := userIDs[6] + wechatInvalidOpenIDUserID := userIDs[7] + + require.NoError(t, tx.QueryRowContext(ctx, ` +INSERT INTO auth_identities (user_id, provider_type, provider_key, provider_subject, metadata) +VALUES ($1, 'linuxdo', 'linuxdo', 'linuxdo-conflict', '{}'::jsonb) +RETURNING id`, linuxdoConflictOwnerUserID).Scan(new(int64))) + + require.NoError(t, tx.QueryRowContext(ctx, ` +INSERT INTO auth_identities (user_id, provider_type, provider_key, provider_subject, metadata) +VALUES ($1, 'wechat', 'wechat-main', 'union-conflict', '{}'::jsonb) +RETURNING id`, wechatConflictOwnerUserID).Scan(new(int64))) + + var wechatChannelOwnerIdentityID int64 + require.NoError(t, tx.QueryRowContext(ctx, ` +INSERT INTO auth_identities (user_id, provider_type, provider_key, provider_subject, metadata) +VALUES ($1, 'wechat', 'wechat-main', 'union-channel-owner', '{}'::jsonb) +RETURNING id`, wechatChannelOwnerUserID).Scan(&wechatChannelOwnerIdentityID)) + + require.NoError(t, tx.QueryRowContext(ctx, ` +INSERT INTO auth_identity_channels ( + identity_id, + provider_type, + provider_key, + channel, + channel_app_id, + channel_subject, + metadata +) +VALUES ($1, 'wechat', 'wechat-main', 'oa', 'wx-app-conflict', 'openid-channel-conflict', '{}'::jsonb) +RETURNING id`, wechatChannelOwnerIdentityID).Scan(new(int64))) + + var linuxdoConflictLegacyID int64 + require.NoError(t, tx.QueryRowContext(ctx, ` +INSERT INTO user_external_identities ( + user_id, + provider, + provider_user_id, + provider_union_id, + provider_username, + display_name, + metadata +) VALUES ($1, 'linuxdo', 'linuxdo-conflict', NULL, 'legacy-linuxdo', 'Legacy LinuxDo Conflict', '{"source":"legacy"}') +RETURNING id +`, linuxdoConflictLegacyUserID).Scan(&linuxdoConflictLegacyID)) + + var wechatConflictLegacyID int64 + require.NoError(t, tx.QueryRowContext(ctx, ` +INSERT INTO user_external_identities ( + user_id, + provider, + provider_user_id, + provider_union_id, + provider_username, + display_name, + metadata +) VALUES ($1, 'wechat', 'openid-union-conflict', 'union-conflict', 'legacy-wechat', 'Legacy WeChat Conflict', '{"channel":"oa","appid":"wx-app-conflict-canon"}') +RETURNING id +`, wechatConflictLegacyUserID).Scan(&wechatConflictLegacyID)) + + var wechatChannelConflictLegacyID int64 + require.NoError(t, tx.QueryRowContext(ctx, ` +INSERT INTO user_external_identities ( + user_id, + provider, + provider_user_id, + provider_union_id, + provider_username, + display_name, + metadata +) VALUES ($1, 'wechat', 'openid-channel-conflict', 'union-channel-legacy', 'legacy-wechat-channel', 'Legacy WeChat Channel Conflict', '{"channel":"oa","appid":"wx-app-conflict"}') +RETURNING id +`, wechatChannelLegacyUserID).Scan(&wechatChannelConflictLegacyID)) + + var linuxdoInvalidJSONLegacyID int64 + require.NoError(t, tx.QueryRowContext(ctx, ` +INSERT INTO user_external_identities ( + user_id, + provider, + provider_user_id, + provider_union_id, + provider_username, + display_name, + metadata +) VALUES ($1, 'linuxdo', 'linuxdo-invalid-json', NULL, 'legacy-linuxdo-invalid', 'Legacy LinuxDo Invalid JSON', '{invalid') +RETURNING id +`, linuxdoInvalidJSONUserID).Scan(&linuxdoInvalidJSONLegacyID)) + + var wechatInvalidOpenIDLegacyID int64 + require.NoError(t, tx.QueryRowContext(ctx, ` +INSERT INTO user_external_identities ( + user_id, + provider, + provider_user_id, + provider_union_id, + provider_username, + display_name, + metadata +) VALUES ($1, 'wechat', 'openid-invalid-json-only', NULL, 'legacy-wechat-invalid', 'Legacy WeChat Invalid JSON', '{still-invalid') +RETURNING id +`, wechatInvalidOpenIDUserID).Scan(&wechatInvalidOpenIDLegacyID)) + + _, err = tx.ExecContext(ctx, string(migrationSQL)) + require.NoError(t, err) + + var linuxdoConflictReportCount int + require.NoError(t, tx.QueryRowContext(ctx, ` +SELECT COUNT(*) +FROM auth_identity_migration_reports +WHERE report_type = 'legacy_external_identity_conflict' + AND report_key = $1 +`, "legacy_external_identity:"+strconv.FormatInt(linuxdoConflictLegacyID, 10)).Scan(&linuxdoConflictReportCount)) + require.Equal(t, 1, linuxdoConflictReportCount) + + var wechatConflictReportCount int + require.NoError(t, tx.QueryRowContext(ctx, ` +SELECT COUNT(*) +FROM auth_identity_migration_reports +WHERE report_type = 'legacy_external_identity_conflict' + AND report_key = $1 +`, "legacy_external_identity:"+strconv.FormatInt(wechatConflictLegacyID, 10)).Scan(&wechatConflictReportCount)) + require.Equal(t, 1, wechatConflictReportCount) + + var channelConflictReportCount int + require.NoError(t, tx.QueryRowContext(ctx, ` +SELECT COUNT(*) +FROM auth_identity_migration_reports +WHERE report_type = 'legacy_external_channel_conflict' + AND report_key = $1 +`, "legacy_external_identity:"+strconv.FormatInt(wechatChannelConflictLegacyID, 10)).Scan(&channelConflictReportCount)) + require.Equal(t, 1, channelConflictReportCount) + + var invalidJSONReportCount int + require.NoError(t, tx.QueryRowContext(ctx, ` +SELECT COUNT(*) +FROM auth_identity_migration_reports +WHERE report_type = 'legacy_external_identity_invalid_metadata_json' + AND report_key IN ($1, $2) +`, "legacy_external_identity:"+strconv.FormatInt(linuxdoInvalidJSONLegacyID, 10), "legacy_external_identity:"+strconv.FormatInt(wechatInvalidOpenIDLegacyID, 10)).Scan(&invalidJSONReportCount)) + require.Equal(t, 2, invalidJSONReportCount) + + var linuxdoInvalidIdentityCount int + require.NoError(t, tx.QueryRowContext(ctx, ` +SELECT COUNT(*) +FROM auth_identities +WHERE user_id = $1 + AND provider_type = 'linuxdo' + AND provider_key = 'linuxdo' + AND provider_subject = 'linuxdo-invalid-json' +`, linuxdoInvalidJSONUserID).Scan(&linuxdoInvalidIdentityCount)) + require.Equal(t, 1, linuxdoInvalidIdentityCount) + + var wechatOpenIDOnlyReportCount int + require.NoError(t, tx.QueryRowContext(ctx, ` +SELECT COUNT(*) +FROM auth_identity_migration_reports +WHERE report_type = 'wechat_openid_only_requires_remediation' + AND report_key = $1 +`, "legacy_external_identity:"+strconv.FormatInt(wechatInvalidOpenIDLegacyID, 10)).Scan(&wechatOpenIDOnlyReportCount)) + require.Equal(t, 1, wechatOpenIDOnlyReportCount) +} + +func TestAuthIdentityLegacyExternalSafetyMigration_IsSafeWhenLegacyTableMissing(t *testing.T) { + tx := testTx(t) + ctx := context.Background() + + migrationPath := filepath.Join("..", "..", "migrations", "116_auth_identity_legacy_external_safety_reports.sql") + migrationSQL, err := os.ReadFile(migrationPath) + require.NoError(t, err) + + var beforeCount int + require.NoError(t, tx.QueryRowContext(ctx, ` +SELECT COUNT(*) +FROM auth_identity_migration_reports +`).Scan(&beforeCount)) + + _, err = tx.ExecContext(ctx, string(migrationSQL)) + require.NoError(t, err) + + var afterCount int + require.NoError(t, tx.QueryRowContext(ctx, ` +SELECT COUNT(*) FROM auth_identity_migration_reports `).Scan(&afterCount)) require.Equal(t, beforeCount, afterCount) diff --git a/backend/migrations/116_auth_identity_legacy_external_safety_reports.sql b/backend/migrations/116_auth_identity_legacy_external_safety_reports.sql new file mode 100644 index 00000000..994f3f37 --- /dev/null +++ b/backend/migrations/116_auth_identity_legacy_external_safety_reports.sql @@ -0,0 +1,336 @@ +CREATE OR REPLACE FUNCTION public.__migration_116_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; +$$; + +CREATE OR REPLACE FUNCTION public.__migration_116_is_valid_legacy_metadata_jsonb(input_text TEXT) +RETURNS BOOLEAN +LANGUAGE plpgsql +AS $$ +DECLARE + parsed JSONB; +BEGIN + IF input_text IS NULL OR BTRIM(input_text) = '' THEN + RETURN TRUE; + END IF; + + parsed := input_text::jsonb; + RETURN TRUE; +EXCEPTION + WHEN OTHERS THEN + RETURN FALSE; +END; +$$; + +DO $$ +BEGIN + IF to_regclass('public.user_external_identities') IS NULL THEN + RETURN; + END IF; + + EXECUTE $sql$ +INSERT INTO auth_identity_migration_reports (report_type, report_key, details) +SELECT + 'legacy_external_identity_invalid_metadata_json', + 'legacy_external_identity:' || uei.id::text, + jsonb_build_object( + 'legacy_identity_id', uei.id, + 'user_id', uei.user_id, + 'provider', LOWER(BTRIM(COALESCE(uei.provider, ''))), + 'provider_user_id', BTRIM(COALESCE(uei.provider_user_id, '')), + 'provider_union_id', BTRIM(COALESCE(uei.provider_union_id, '')), + 'reason', 'legacy metadata is not valid JSON; migration downgraded metadata to empty object', + 'raw_metadata', LEFT(BTRIM(COALESCE(uei.metadata, '')), 1000), + 'migration', '116_auth_identity_legacy_external_safety_reports' + ) +FROM user_external_identities AS uei +JOIN users AS u ON u.id = uei.user_id +WHERE u.deleted_at IS NULL + AND BTRIM(COALESCE(uei.metadata, '')) <> '' + AND NOT public.__migration_116_is_valid_legacy_metadata_jsonb(uei.metadata) +ON CONFLICT (report_type, report_key) DO NOTHING; +$sql$; + + EXECUTE $sql$ +INSERT INTO auth_identity_migration_reports (report_type, report_key, details) +SELECT + 'legacy_external_identity_conflict', + 'legacy_external_identity:' || legacy.id::text, + legacy.metadata_json || jsonb_build_object( + 'legacy_identity_id', legacy.id, + 'legacy_user_id', legacy.user_id, + 'existing_identity_id', ai.id, + 'existing_user_id', ai.user_id, + 'provider_type', legacy.provider_type, + 'provider_key', legacy.provider_key, + 'provider_subject', legacy.provider_subject, + 'reason', 'legacy canonical identity subject already belongs to another user', + 'migration', '116_auth_identity_legacy_external_safety_reports' + ) +FROM ( + SELECT + uei.id, + uei.user_id, + LOWER(BTRIM(COALESCE(uei.provider, ''))) AS provider_type, + CASE + WHEN LOWER(BTRIM(COALESCE(uei.provider, ''))) = 'wechat' THEN 'wechat-main' + ELSE 'linuxdo' + END AS provider_key, + CASE + WHEN LOWER(BTRIM(COALESCE(uei.provider, ''))) = 'wechat' THEN BTRIM(COALESCE(uei.provider_union_id, '')) + ELSE BTRIM(COALESCE(uei.provider_user_id, '')) + END AS provider_subject, + BTRIM(COALESCE(uei.provider_user_id, '')) AS provider_user_id, + BTRIM(COALESCE(uei.provider_union_id, '')) AS provider_union_id, + BTRIM(COALESCE(uei.provider_username, '')) AS provider_username, + BTRIM(COALESCE(uei.display_name, '')) AS display_name, + public.__migration_116_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, ''))) IN ('linuxdo', 'wechat') + AND ( + (LOWER(BTRIM(COALESCE(uei.provider, ''))) = 'linuxdo' AND BTRIM(COALESCE(uei.provider_user_id, '')) <> '') + OR + (LOWER(BTRIM(COALESCE(uei.provider, ''))) = 'wechat' AND BTRIM(COALESCE(uei.provider_union_id, '')) <> '') + ) +) AS legacy +JOIN auth_identities AS ai + ON ai.provider_type = legacy.provider_type + AND ai.provider_key = legacy.provider_key + AND ai.provider_subject = legacy.provider_subject +WHERE ai.user_id <> legacy.user_id +ON CONFLICT (report_type, report_key) DO NOTHING; +$sql$; + + EXECUTE $sql$ +INSERT INTO auth_identities ( + user_id, + provider_type, + provider_key, + provider_subject, + verified_at, + metadata +) +SELECT + legacy.user_id, + legacy.provider_type, + legacy.provider_key, + legacy.provider_subject, + legacy.verified_at, + legacy.metadata_json || jsonb_build_object( + 'legacy_identity_id', legacy.id, + 'provider_user_id', legacy.provider_user_id, + 'provider_union_id', NULLIF(legacy.provider_union_id, ''), + 'provider_username', legacy.provider_username, + 'display_name', legacy.display_name, + 'migration', '116_auth_identity_legacy_external_safety_reports' + ) +FROM ( + SELECT + uei.id, + uei.user_id, + LOWER(BTRIM(COALESCE(uei.provider, ''))) AS provider_type, + CASE + WHEN LOWER(BTRIM(COALESCE(uei.provider, ''))) = 'wechat' THEN 'wechat-main' + ELSE 'linuxdo' + END AS provider_key, + CASE + WHEN LOWER(BTRIM(COALESCE(uei.provider, ''))) = 'wechat' THEN BTRIM(COALESCE(uei.provider_union_id, '')) + ELSE BTRIM(COALESCE(uei.provider_user_id, '')) + END AS provider_subject, + BTRIM(COALESCE(uei.provider_user_id, '')) AS provider_user_id, + BTRIM(COALESCE(uei.provider_union_id, '')) AS provider_union_id, + BTRIM(COALESCE(uei.provider_username, '')) AS provider_username, + BTRIM(COALESCE(uei.display_name, '')) AS display_name, + public.__migration_116_safe_legacy_metadata_jsonb(uei.metadata) AS metadata_json, + COALESCE(uei.updated_at, uei.created_at, NOW()) AS verified_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, ''))) IN ('linuxdo', 'wechat') + AND ( + (LOWER(BTRIM(COALESCE(uei.provider, ''))) = 'linuxdo' AND BTRIM(COALESCE(uei.provider_user_id, '')) <> '') + OR + (LOWER(BTRIM(COALESCE(uei.provider, ''))) = 'wechat' AND BTRIM(COALESCE(uei.provider_union_id, '')) <> '') + ) +) AS legacy +LEFT JOIN auth_identities AS ai + ON ai.provider_type = legacy.provider_type + AND ai.provider_key = legacy.provider_key + AND ai.provider_subject = legacy.provider_subject +WHERE ai.id IS NULL +ON CONFLICT (provider_type, provider_key, provider_subject) DO NOTHING; +$sql$; + + EXECUTE $sql$ +INSERT INTO auth_identity_migration_reports (report_type, report_key, details) +SELECT + 'legacy_external_channel_conflict', + 'legacy_external_identity:' || legacy.id::text, + legacy.metadata_json || jsonb_build_object( + 'legacy_identity_id', legacy.id, + 'legacy_user_id', legacy.user_id, + 'existing_channel_id', channel.id, + 'existing_identity_id', existing_ai.id, + 'existing_user_id', existing_ai.user_id, + 'provider_type', 'wechat', + 'provider_key', 'wechat-main', + 'provider_subject', legacy.provider_union_id, + 'channel', legacy.channel, + 'channel_app_id', legacy.channel_app_id, + 'channel_subject', legacy.provider_user_id, + 'reason', 'legacy channel subject already belongs to another user', + 'migration', '116_auth_identity_legacy_external_safety_reports' + ) +FROM ( + SELECT + uei.id, + uei.user_id, + BTRIM(COALESCE(uei.provider_user_id, '')) AS provider_user_id, + BTRIM(COALESCE(uei.provider_union_id, '')) AS provider_union_id, + public.__migration_116_safe_legacy_metadata_jsonb(uei.metadata) AS metadata_json, + BTRIM(COALESCE(public.__migration_116_safe_legacy_metadata_jsonb(uei.metadata) ->> 'channel', '')) AS channel, + BTRIM(COALESCE( + public.__migration_116_safe_legacy_metadata_jsonb(uei.metadata) ->> 'channel_app_id', + public.__migration_116_safe_legacy_metadata_jsonb(uei.metadata) ->> 'appid', + public.__migration_116_safe_legacy_metadata_jsonb(uei.metadata) ->> 'app_id', + '' + )) AS channel_app_id + 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, '')) <> '' + AND BTRIM(COALESCE(uei.provider_user_id, '')) <> '' +) AS legacy +JOIN auth_identities AS legacy_ai + ON legacy_ai.user_id = legacy.user_id + AND legacy_ai.provider_type = 'wechat' + AND legacy_ai.provider_key = 'wechat-main' + AND legacy_ai.provider_subject = legacy.provider_union_id +JOIN auth_identity_channels AS channel + ON channel.provider_type = 'wechat' + AND channel.provider_key = 'wechat-main' + AND channel.channel = legacy.channel + AND channel.channel_app_id = legacy.channel_app_id + AND channel.channel_subject = legacy.provider_user_id +JOIN auth_identities AS existing_ai + ON existing_ai.id = channel.identity_id +WHERE legacy.channel <> '' + AND legacy.channel_app_id <> '' + AND existing_ai.user_id <> legacy.user_id +ON CONFLICT (report_type, report_key) DO NOTHING; +$sql$; + + EXECUTE $sql$ +INSERT INTO auth_identity_channels ( + identity_id, + provider_type, + provider_key, + channel, + channel_app_id, + channel_subject, + metadata +) +SELECT + legacy_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', '116_auth_identity_legacy_external_safety_reports' + ) +FROM ( + SELECT + uei.user_id, + BTRIM(COALESCE(uei.provider_user_id, '')) AS provider_user_id, + BTRIM(COALESCE(uei.provider_union_id, '')) AS provider_union_id, + public.__migration_116_safe_legacy_metadata_jsonb(uei.metadata) AS metadata_json, + BTRIM(COALESCE(public.__migration_116_safe_legacy_metadata_jsonb(uei.metadata) ->> 'channel', '')) AS channel, + BTRIM(COALESCE( + public.__migration_116_safe_legacy_metadata_jsonb(uei.metadata) ->> 'channel_app_id', + public.__migration_116_safe_legacy_metadata_jsonb(uei.metadata) ->> 'appid', + public.__migration_116_safe_legacy_metadata_jsonb(uei.metadata) ->> 'app_id', + '' + )) AS channel_app_id + 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, '')) <> '' + AND BTRIM(COALESCE(uei.provider_user_id, '')) <> '' +) AS legacy +JOIN auth_identities AS legacy_ai + ON legacy_ai.user_id = legacy.user_id + AND legacy_ai.provider_type = 'wechat' + AND legacy_ai.provider_key = 'wechat-main' + AND legacy_ai.provider_subject = legacy.provider_union_id +LEFT JOIN auth_identity_channels AS channel + ON channel.provider_type = 'wechat' + AND channel.provider_key = 'wechat-main' + AND channel.channel = legacy.channel + AND channel.channel_app_id = legacy.channel_app_id + AND channel.channel_subject = legacy.provider_user_id +WHERE legacy.channel <> '' + AND legacy.channel_app_id <> '' + AND channel.id IS NULL +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', '116_auth_identity_legacy_external_safety_reports' + ) +FROM ( + SELECT + uei.id, + uei.user_id, + BTRIM(COALESCE(uei.provider_user_id, '')) AS provider_user_id, + public.__migration_116_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 $$; + +DROP FUNCTION IF EXISTS public.__migration_116_is_valid_legacy_metadata_jsonb(TEXT); +DROP FUNCTION IF EXISTS public.__migration_116_safe_legacy_metadata_jsonb(TEXT);