diff --git a/Dockerfile.stage b/Dockerfile.stage index 744dee8..29abfba 100644 --- a/Dockerfile.stage +++ b/Dockerfile.stage @@ -39,7 +39,7 @@ COPY dist/main-combined.el ./ RUN python3 bootstrap.py main-combined.el > main.c && \ sed -i \ - 's|#include "el_runtime.h"|#include "el_runtime.h"\nel_val_t http_get_auth(el_val_t url, el_val_t tok);\nel_val_t http_post_auth(el_val_t url, el_val_t tok, el_val_t body);\nel_val_t cwd(void);\nel_val_t color_bold(el_val_t s);\nel_val_t unix_timestamp(void);\nel_val_t gcs_write(el_val_t bucket, el_val_t object_name, el_val_t content);\nel_val_t gcs_read(el_val_t bucket, el_val_t object_name);\nel_val_t supabase_insert(el_val_t project_url, el_val_t service_key, el_val_t table, el_val_t row_json);\nel_val_t supabase_get(el_val_t project_url, el_val_t service_key, el_val_t table_and_query);|' \ + 's|#include "el_runtime.h"|#include "el_runtime.h"\nel_val_t http_get_auth(el_val_t url, el_val_t tok);\nel_val_t http_post_auth(el_val_t url, el_val_t tok, el_val_t body);\nel_val_t cwd(void);\nel_val_t color_bold(el_val_t s);\nel_val_t unix_timestamp(void);\nel_val_t gcs_write(el_val_t bucket, el_val_t object_name, el_val_t content);\nel_val_t gcs_read(el_val_t bucket, el_val_t object_name);\nel_val_t supabase_insert(el_val_t project_url, el_val_t service_key, el_val_t table, el_val_t row_json);\nel_val_t supabase_get(el_val_t project_url, el_val_t service_key, el_val_t table_and_query);\nel_val_t supabase_auth_user(el_val_t project_url, el_val_t anon_key, el_val_t user_jwt);|' \ main.c && \ cc -O2 -rdynamic \ -o neuron-web \ diff --git a/build-local.sh b/build-local.sh index dbcd8cb..a7fd92a 100755 --- a/build-local.sh +++ b/build-local.sh @@ -51,7 +51,7 @@ echo "==> Bootstrap El → C" python3 "${BOOTSTRAP}" dist/main-combined.el > dist/main.c echo "==> Injecting stubs" -sed -i '' 's|#include "el_runtime.h"|#include "el_runtime.h"\nel_val_t http_get_auth(el_val_t url, el_val_t tok);\nel_val_t http_post_auth(el_val_t url, el_val_t tok, el_val_t body);\nel_val_t cwd(void);\nel_val_t color_bold(el_val_t s);\nel_val_t unix_timestamp(void);\nel_val_t gcs_write(el_val_t bucket, el_val_t object_name, el_val_t content);\nel_val_t gcs_read(el_val_t bucket, el_val_t object_name);\nel_val_t supabase_insert(el_val_t project_url, el_val_t service_key, el_val_t table, el_val_t row_json);\nel_val_t supabase_get(el_val_t project_url, el_val_t service_key, el_val_t table_and_query);|' dist/main.c +sed -i '' 's|#include "el_runtime.h"|#include "el_runtime.h"\nel_val_t http_get_auth(el_val_t url, el_val_t tok);\nel_val_t http_post_auth(el_val_t url, el_val_t tok, el_val_t body);\nel_val_t cwd(void);\nel_val_t color_bold(el_val_t s);\nel_val_t unix_timestamp(void);\nel_val_t gcs_write(el_val_t bucket, el_val_t object_name, el_val_t content);\nel_val_t gcs_read(el_val_t bucket, el_val_t object_name);\nel_val_t supabase_insert(el_val_t project_url, el_val_t service_key, el_val_t table, el_val_t row_json);\nel_val_t supabase_get(el_val_t project_url, el_val_t service_key, el_val_t table_and_query);\nel_val_t supabase_auth_user(el_val_t project_url, el_val_t anon_key, el_val_t user_jwt);|' dist/main.c echo "==> Compiling neuron-web" cc -O2 \ diff --git a/src/account.el b/src/account.el index b22627a..68dda18 100644 --- a/src/account.el +++ b/src/account.el @@ -1189,28 +1189,20 @@ fn account_page(supabase_url: String, supabase_anon_key: String) -> String { async function loadWaitlistData(email) { try { - var result = await sb - .from('waitlist') - .select('plan, member_number, source, created_at') - .eq('email', email) - .order('created_at', { ascending: false }) - .limit(1); + var sess = await sb.auth.getSession(); + var token = sess.data && sess.data.session ? sess.data.session.access_token : ''; + if (!token) { showNoPlan(); return; } - if (result.error) { - console.error('Waitlist query error:', result.error.message); - showNoPlan(); - return; - } + var r = await fetch('/api/my-plan', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ access_token: token }) + }); + var row = await r.json(); - var row = result.data && result.data.length > 0 ? result.data[0] : null; - if (!row) { - // No plan — redirect to pricing so they can choose - showNoPlan(); - return; - } + if (!row || !row.plan) { showNoPlan(); return; } renderPlanCard(row); } catch (e) { - console.error('Failed to load waitlist data:', e); showNoPlan(); } } diff --git a/src/checkout.el b/src/checkout.el index bb75081..76ac9fc 100644 --- a/src/checkout.el +++ b/src/checkout.el @@ -703,7 +703,6 @@ fn checkout_page(plan: String, pub_key: String) -> String { return; } - // Capture the PI id so we can attach a Customer at submit time window._neuronPiId = data.id || (data.client_secret ? data.client_secret.split('_secret_')[0] : ''); waitForStripe(function() { @@ -818,10 +817,6 @@ fn checkout_page(plan: String, pub_key: String) -> String { setLoading(true); document.getElementById('payment-message').style.display = 'none'; - // Link a Stripe Customer to this PaymentIntent and to the Supabase waitlist row - // (so the buyer shows up as a named customer, not Guest, and the account page - // can find their plan via stripe_customer_id). Non-blocking - if it fails, the - // webhook still links them server-side after payment_intent.succeeded fires. if (window._neuronPiId) { try { await fetch('/api/link-customer', { diff --git a/src/main.el b/src/main.el index 442ffbb..9a4b3a2 100644 --- a/src/main.el +++ b/src/main.el @@ -498,6 +498,40 @@ fn handle_request(method: String, path: String, body: String) -> String { return "{\"status\":\"ok\",\"service\":\"neuron-web\"}" } + // ── My plan: server-side waitlist read with JWT verification ───────────── + // POST { "access_token": "" }. We verify the JWT via Supabase + // /auth/v1/user, extract the email, then read the waitlist row with the + // SERVICE key (bypasses RLS). Canonical plan source for /account. + if str_eq(path, "/api/my-plan") { + let mp_jwt: String = json_get_string(body, "access_token") + if str_eq(mp_jwt, "") { + return "{\"__status__\":401,\"error\":\"missing_jwt\"}" + } + let mp_sb_url: String = state_get("__supabase_project_url__") + let mp_anon: String = state_get("__supabase_anon_key__") + let mp_service: String = state_get("__supabase_service_key__") + if str_eq(mp_sb_url, "") || str_eq(mp_anon, "") || str_eq(mp_service, "") { + return "{\"__status__\":503,\"error\":\"supabase_not_configured\"}" + } + let mp_user: String = supabase_auth_user(mp_sb_url, mp_anon, mp_jwt) + let mp_email: String = json_get(mp_user, "email") + if str_eq(mp_email, "") { + return "{\"__status__\":401,\"error\":\"invalid_jwt\"}" + } + let mp_email_safe: String = str_replace(str_replace(mp_email, "@", "%40"), "+", "%2B") + let mp_query: String = "waitlist?select=plan,member_number,source,created_at,stripe_customer_id,name&email=eq." + mp_email_safe + "&order=created_at.desc&limit=1" + let mp_resp: String = supabase_get(mp_sb_url, mp_service, mp_query) + if str_eq(mp_resp, "") || str_eq(mp_resp, "[]") { + return "{\"plan\":null,\"email\":\"" + mp_email + "\"}" + } + // Strip outer array brackets to return a plain object + if str_starts_with(mp_resp, "[") { + let mp_inner: String = str_slice(mp_resp, 1, str_len(mp_resp) - 1) + return mp_inner + } + return mp_resp + } + // ── Founding count ──────────────────────────────────────────────────────── if str_eq(path, "/api/founding-count") {