account: server-side plan lookup via /api/my-plan, scrub internal comments from JS
The /account "Loading..." spinner stayed on forever because the browser-side waitlist read went through the anon key and didn't reach the row. Replaced it with a POST /api/my-plan: the server verifies the user's access_token via Supabase /auth/v1/user, then reads the waitlist row with the service key. Bypasses RLS without exposing the service key to the browser. Stripped implementation comments from the served JS so the browser doesn't broadcast how internals are shaped. Build pipeline: declared the supabase_auth_user stub for both build-local.sh and Dockerfile.stage so the bootstrap-injected forward declarations match what's actually linked.
This commit is contained in:
+1
-1
@@ -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 \
|
||||
|
||||
+1
-1
@@ -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 \
|
||||
|
||||
+10
-18
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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', {
|
||||
|
||||
+34
@@ -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": "<user_jwt>" }. 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") {
|
||||
|
||||
Reference in New Issue
Block a user