Fix webhook failing to update plan for pre-existing Supabase users
Dev — Build & local smoke test / build-smoke (pull_request) Successful in 1m17s
Dev — Build & local smoke test / build-smoke (pull_request) Successful in 1m17s
supabase_admin_invite re-sends a magic link for users who already have
an account (e.g. signed up via attestation before paying) but does not
touch their user_metadata — leaving plan as "free" after purchase.
Fix: add supabase_admin_update_user (PUT /auth/v1/admin/users/{id})
and call it after every invite so user_metadata is always stamped with
the correct plan, name, and stripe_customer_id. Idempotent for new and
returning users.
Also fix waitlist_upsert to use on_conflict=email,plan so the upsert
works for users who already have a waitlist row from attestation,
rather than silently failing on duplicate key.
This commit is contained in:
Vendored
+49
@@ -237,6 +237,55 @@ el_val_t supabase_admin_invite(el_val_t project_url, el_val_t service_key, el_va
|
||||
return http_post_with_headers(EL_STR(url), body_json, headers);
|
||||
}
|
||||
|
||||
/*
|
||||
* supabase_admin_update_user — PUT {project_url}/auth/v1/admin/users/{user_id}
|
||||
* with the service-role key to overwrite a user's user_metadata (and any other
|
||||
* top-level fields in body_json). Unlike /auth/v1/invite, this always writes
|
||||
* the supplied data even when the user already exists.
|
||||
*
|
||||
* body_json example:
|
||||
* {"user_metadata":{"plan":"founding","stripe_customer_id":"cus_xxx","name":"..."}}
|
||||
*
|
||||
* Returns the raw JSON response from Supabase (includes the updated user object).
|
||||
* Returns "" on transport error.
|
||||
*
|
||||
* Used by the Stripe webhook after supabase_admin_invite to guarantee the
|
||||
* plan is stamped correctly regardless of whether the account was created
|
||||
* before or after payment.
|
||||
*/
|
||||
el_val_t supabase_admin_update_user(el_val_t project_url, el_val_t service_key,
|
||||
el_val_t user_id, el_val_t body_json) {
|
||||
CURL *c = curl_easy_init();
|
||||
if (!c) return EL_STR("");
|
||||
char url[1024];
|
||||
snprintf(url, sizeof(url), "%s/auth/v1/admin/users/%s",
|
||||
EL_CSTR(project_url), EL_CSTR(user_id));
|
||||
char auth_hdr[2048];
|
||||
snprintf(auth_hdr, sizeof(auth_hdr), "Authorization: Bearer %s", EL_CSTR(service_key));
|
||||
char api_hdr[2048];
|
||||
snprintf(api_hdr, sizeof(api_hdr), "apikey: %s", EL_CSTR(service_key));
|
||||
struct curl_slist *hdrs = NULL;
|
||||
hdrs = curl_slist_append(hdrs, auth_hdr);
|
||||
hdrs = curl_slist_append(hdrs, api_hdr);
|
||||
hdrs = curl_slist_append(hdrs, "Content-Type: application/json");
|
||||
hdrs = curl_slist_append(hdrs, "Accept: application/json");
|
||||
_stub_resp_t r = {0};
|
||||
curl_easy_setopt(c, CURLOPT_URL, url);
|
||||
curl_easy_setopt(c, CURLOPT_CUSTOMREQUEST, "PUT");
|
||||
curl_easy_setopt(c, CURLOPT_POSTFIELDS, EL_CSTR(body_json));
|
||||
curl_easy_setopt(c, CURLOPT_HTTPHEADER, hdrs);
|
||||
curl_easy_setopt(c, CURLOPT_FOLLOWLOCATION, 1L);
|
||||
curl_easy_setopt(c, CURLOPT_TIMEOUT, 60L);
|
||||
curl_easy_setopt(c, CURLOPT_WRITEFUNCTION, _stub_write);
|
||||
curl_easy_setopt(c, CURLOPT_WRITEDATA, &r);
|
||||
CURLcode rc = curl_easy_perform(c);
|
||||
curl_easy_cleanup(c);
|
||||
curl_slist_free_all(hdrs);
|
||||
if (rc != CURLE_OK) { free(r.buf); return EL_STR(""); }
|
||||
if (!r.buf) return EL_STR("");
|
||||
return EL_STR(r.buf);
|
||||
}
|
||||
|
||||
/*
|
||||
* gcs_get_token — fetch an OAuth2 bearer token.
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user