Files
Jyotisha/deploy/reset-staging-account.sh
Jesse_Chen f3946eaa6f
Staging Backend Quality Gate / validate (push) Failing after 9m57s
Staging Backend Quality Gate / publish (push) Has been skipped
chore: retire legacy rectification runtime
2026-08-07 12:55:32 +08:00

260 lines
13 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
set +x
required=(DEPLOY_PATH EXPECTED_DEPLOY_SHA RESET_EMAIL RESET_CONFIRMATION)
for key in "${required[@]}"; do
if [ -z "${!key:-}" ]; then
echo "required staging account-reset input is missing: $key" >&2
exit 1
fi
done
[[ "$EXPECTED_DEPLOY_SHA" =~ ^[0-9a-f]{40}$ ]] || {
echo "invalid expected deployment SHA" >&2
exit 1
}
[[ "$RESET_EMAIL" =~ ^[[:alnum:]._%+-]+@[[:alnum:].-]+\.[[:alpha:]]{2,63}$ ]] || {
echo "invalid reset email" >&2
exit 1
}
[ "$RESET_CONFIRMATION" = "RESET $RESET_EMAIL" ] || {
echo "account reset confirmation does not match" >&2
exit 1
}
[ "$DEPLOY_PATH" = "/opt/jyotisha-staging" ] || {
echo "refusing non-staging deployment path" >&2
exit 1
}
state_directory="$DEPLOY_PATH/.state"
[ -f "$state_directory/deployed-revision" ] || {
echo "staging deployed revision is unavailable" >&2
exit 1
}
[ "$(<"$state_directory/deployed-revision")" = "$EXPECTED_DEPLOY_SHA" ] || {
echo "deployed staging revision does not match the approved reset SHA" >&2
exit 1
}
install -d -m 700 "$state_directory"
exec 9>"$state_directory/mutation.lock"
flock -n 9 || {
echo "another staging mutation holds the host lock" >&2
exit 75
}
cd "$DEPLOY_PATH"
compose=(docker compose -p jyotisha-staging -f deploy/docker-compose.postgres.yml)
"${compose[@]}" ps --status running postgres --quiet | grep -q . || {
echo "staging postgres container is not running" >&2
exit 1
}
run_psql() {
"${compose[@]}" exec -T -e RESET_EMAIL="$RESET_EMAIL" postgres sh -ceu '
exec psql -X -v ON_ERROR_STOP=1 -v target_email="$RESET_EMAIL" \
-U "$POSTGRES_USER" -d "$POSTGRES_DB"
'
}
run_psql <<'SQL'
begin;
create temporary table reset_snapshot on commit drop as
select
identity_user.id,
identity_user.email,
profile.email as profile_email,
profile.credits,
(select count(*) from identity.accounts value where value.user_id = identity_user.id) as identity_accounts,
(select count(*) from identity.sessions value where value.user_id = identity_user.id) as identity_sessions,
(select count(*) from public.credit_transactions value where value.user_id = identity_user.id) as credit_transactions,
(select count(*) from public.credit_request_cancellations value where value.user_id = identity_user.id) as credit_cancellations,
(select count(*) from public.consultation_requests value where value.user_id = identity_user.id) as consultation_requests,
(select count(*) from public.birth_time_rectification_billing value where value.user_id = identity_user.id) as rectification_billing,
(select count(*) from public.birth_time_rectification_action_receipts value where value.user_id = identity_user.id) as action_receipts,
(select count(*) from public.redemption_codes value where value.redeemed_by = identity_user.id) as redeemed_codes,
(select count(*) from audit.admin_audit_logs value where value.actor_user_id = identity_user.id) as admin_audit_logs
from identity.users identity_user
join auth.users auth_user on auth_user.id = identity_user.id
join public.profiles profile on profile.id = identity_user.id
where lower(btrim(identity_user.email)) = lower(btrim(:'target_email'))
and lower(btrim(auth_user.email)) = lower(btrim(:'target_email'))
for update of identity_user, auth_user, profile;
do $$
begin
if (select count(*) from reset_snapshot) <> 1 then
raise exception 'account_not_found_or_identity_bridge_mismatch';
end if;
end $$;
select jsonb_build_object(
'stage', 'preflight',
'email', snapshot.email,
'credits', snapshot.credits,
'identityAccounts', snapshot.identity_accounts,
'identitySessions', snapshot.identity_sessions,
'creditTransactions', snapshot.credit_transactions,
'creditCancellations', snapshot.credit_cancellations,
'consultationRequests', snapshot.consultation_requests,
'rectificationBilling', snapshot.rectification_billing,
'actionReceipts', snapshot.action_receipts,
'redeemedCodes', snapshot.redeemed_codes,
'adminAuditLogs', snapshot.admin_audit_logs,
'chatSessions', (select count(*) from public.chat_sessions value where value.user_id = snapshot.id),
'chartProfiles', (select count(*) from public.chart_profiles value where value.user_id = snapshot.id),
'synastryReports', (select count(*) from public.synastry_reports value where value.user_id = snapshot.id)
)
from reset_snapshot snapshot;
update public.profiles profile
set name = null,
birth_date = null,
birth_time = null,
country_code = null,
province_code = null,
city_code = null,
district_code = null,
onboarding_payload = null,
onboarding_version = null,
onboarding_generated_at = null,
latitude = null,
longitude = null,
timezone_offset = null,
reported_birth_time = null,
active_birth_time = null,
birth_time_source = null,
birth_time_period = null,
birth_time_clue = null,
uncertainty_before_minutes = null,
uncertainty_after_minutes = null,
birth_time_status = null,
rectification_confidence = null,
rectification_case_id = null,
birth_place_label = null,
birth_place_type = null,
birth_place_provider = null,
birth_place_provider_id = null,
timezone_id = null,
timezone_source = null,
updated_at = pg_catalog.now()
from reset_snapshot snapshot
where profile.id = snapshot.id;
delete from public.chat_sessions value using reset_snapshot snapshot where value.user_id = snapshot.id;
delete from public.chart_profiles value using reset_snapshot snapshot where value.user_id = snapshot.id;
delete from public.synastry_reports value using reset_snapshot snapshot where value.user_id = snapshot.id;
do $$
begin
if exists (
select 1
from reset_snapshot snapshot
join identity.users identity_user on identity_user.id = snapshot.id
join auth.users auth_user on auth_user.id = snapshot.id
join public.profiles profile on profile.id = snapshot.id
where identity_user.email is distinct from snapshot.email
or auth_user.email is distinct from snapshot.email
or profile.email is distinct from snapshot.profile_email
or profile.credits is distinct from snapshot.credits
or (select count(*) from identity.accounts value where value.user_id = snapshot.id) <> snapshot.identity_accounts
or (select count(*) from identity.sessions value where value.user_id = snapshot.id) <> snapshot.identity_sessions
or (select count(*) from public.credit_transactions value where value.user_id = snapshot.id) <> snapshot.credit_transactions
or (select count(*) from public.credit_request_cancellations value where value.user_id = snapshot.id) <> snapshot.credit_cancellations
or (select count(*) from public.consultation_requests value where value.user_id = snapshot.id) <> snapshot.consultation_requests
or (select count(*) from public.birth_time_rectification_billing value where value.user_id = snapshot.id) <> snapshot.rectification_billing
or (select count(*) from public.birth_time_rectification_action_receipts value where value.user_id = snapshot.id) <> snapshot.action_receipts
or (select count(*) from public.redemption_codes value where value.redeemed_by = snapshot.id) <> snapshot.redeemed_codes
or (select count(*) from audit.admin_audit_logs value where value.actor_user_id = snapshot.id) <> snapshot.admin_audit_logs
) then
raise exception 'preserved_state_changed';
end if;
if exists (select 1 from public.chat_sessions value join reset_snapshot snapshot on value.user_id = snapshot.id)
or exists (select 1 from public.chart_profiles value join reset_snapshot snapshot on value.user_id = snapshot.id)
or exists (select 1 from public.synastry_reports value join reset_snapshot snapshot on value.user_id = snapshot.id)
or exists (
select 1 from public.profiles profile join reset_snapshot snapshot on profile.id = snapshot.id
where profile.name is not null or profile.birth_date is not null or profile.birth_time is not null
or profile.country_code is not null or profile.province_code is not null or profile.city_code is not null or profile.district_code is not null
or profile.onboarding_payload is not null or profile.onboarding_version is not null or profile.onboarding_generated_at is not null
or profile.latitude is not null or profile.longitude is not null or profile.timezone_offset is not null
or profile.reported_birth_time is not null or profile.active_birth_time is not null or profile.birth_time_source is not null
or profile.birth_time_period is not null or profile.birth_time_clue is not null
or profile.uncertainty_before_minutes is not null or profile.uncertainty_after_minutes is not null
or profile.birth_time_status is not null or profile.rectification_confidence is not null or profile.rectification_case_id is not null
or profile.birth_place_label is not null or profile.birth_place_type is not null or profile.birth_place_provider is not null
or profile.birth_place_provider_id is not null or profile.timezone_id is not null or profile.timezone_source is not null
) then
raise exception 'reset_state_not_empty';
end if;
end $$;
commit;
SQL
run_psql <<'SQL'
begin;
create temporary table postflight_target on commit drop as
select identity_user.id, identity_user.email, profile.credits,
not (
profile.name is null and profile.birth_date is null and profile.birth_time is null
and profile.country_code is null and profile.province_code is null and profile.city_code is null and profile.district_code is null
and profile.onboarding_payload is null and profile.onboarding_version is null and profile.onboarding_generated_at is null
and profile.latitude is null and profile.longitude is null and profile.timezone_offset is null
and profile.reported_birth_time is null and profile.active_birth_time is null and profile.birth_time_source is null
and profile.birth_time_period is null and profile.birth_time_clue is null
and profile.uncertainty_before_minutes is null and profile.uncertainty_after_minutes is null
and profile.birth_time_status is null and profile.rectification_confidence is null and profile.rectification_case_id is null
and profile.birth_place_label is null and profile.birth_place_type is null and profile.birth_place_provider is null
and profile.birth_place_provider_id is null and profile.timezone_id is null and profile.timezone_source is null
) as profile_not_reset,
lower(btrim(profile.email)) = lower(btrim(identity_user.email)) as profile_email_matches
from identity.users identity_user
join auth.users auth_user on auth_user.id = identity_user.id
and lower(btrim(auth_user.email)) = lower(btrim(identity_user.email))
join public.profiles profile on profile.id = identity_user.id
where lower(btrim(identity_user.email)) = lower(btrim(:'target_email'));
do $$
declare
target_id uuid;
begin
if (select count(*) from postflight_target) <> 1 then
raise exception 'postflight_account_not_found_or_identity_bridge_mismatch';
end if;
select id into target_id from postflight_target;
if exists (select 1 from public.chat_sessions value where value.user_id = target_id)
or exists (select 1 from public.chart_profiles value where value.user_id = target_id)
or exists (select 1 from public.synastry_reports value where value.user_id = target_id)
or exists (select 1 from postflight_target where profile_not_reset) then
raise exception 'postflight_reset_state_not_empty';
end if;
end $$;
select jsonb_build_object(
'stage', 'postflight',
'matchedAccounts', (select count(*) from postflight_target),
'email', (select email from postflight_target),
'credits', (select credits from postflight_target),
'profileEmailMatches', (select profile_email_matches from postflight_target),
'profileNotReset', (select profile_not_reset from postflight_target),
'chatSessions', (select count(*) from public.chat_sessions value where value.user_id = (select id from postflight_target)),
'chartProfiles', (select count(*) from public.chart_profiles value where value.user_id = (select id from postflight_target)),
'synastryReports', (select count(*) from public.synastry_reports value where value.user_id = (select id from postflight_target)),
'identityAccounts', (select count(*) from identity.accounts value where value.user_id = (select id from postflight_target)),
'identitySessions', (select count(*) from identity.sessions value where value.user_id = (select id from postflight_target)),
'creditTransactions', (select count(*) from public.credit_transactions value where value.user_id = (select id from postflight_target)),
'creditCancellations', (select count(*) from public.credit_request_cancellations value where value.user_id = (select id from postflight_target)),
'consultationRequests', (select count(*) from public.consultation_requests value where value.user_id = (select id from postflight_target)),
'rectificationBilling', (select count(*) from public.birth_time_rectification_billing value where value.user_id = (select id from postflight_target)),
'actionReceipts', (select count(*) from public.birth_time_rectification_action_receipts value where value.user_id = (select id from postflight_target))
);
commit;
SQL