diff --git a/frontend/src/hooks/useProfile.js b/frontend/src/hooks/useProfile.js index fc92f47..9778db6 100644 --- a/frontend/src/hooks/useProfile.js +++ b/frontend/src/hooks/useProfile.js @@ -129,7 +129,14 @@ export function useProfile(address) { // ── Save to server + caches ────────────────────────────────────────────── const saveProfile = useCallback(async (data, { authFetch } = {}) => { if (!address) return; - const _fetch = authFetch ?? fetch; + // The POST /api/v1/profile route requires a JWT. Falling back to an + // unauthenticated `fetch` silently 401s and drops the save (see the Setup + // onboarding bug), so require an authenticated fetch up front. + if (typeof authFetch !== 'function') { + console.error('[useProfile] saveProfile called without authFetch — refusing to POST unauthenticated. Pass { authFetch } from useAuth().'); + return { ok: false, error: 'Not authenticated. Reconnect your wallet and try again.' }; + } + const _fetch = authFetch; const payload = { address, diff --git a/frontend/src/pages/app/Setup.jsx b/frontend/src/pages/app/Setup.jsx index c629a64..bf212bd 100644 --- a/frontend/src/pages/app/Setup.jsx +++ b/frontend/src/pages/app/Setup.jsx @@ -2,6 +2,7 @@ import { useState, useEffect, useRef } from 'react'; import { useNavigate } from 'react-router-dom'; import { useAccount } from 'wagmi'; import { useProfile } from '../../hooks/useProfile'; +import { useAuth } from '../../context/AuthContext'; const AGENT_URL = import.meta.env.VITE_AGENT_URL ?? 'http://localhost:3000'; @@ -62,6 +63,7 @@ export default function Setup() { const navigate = useNavigate(); const { address } = useAccount(); const { saveProfile, profile, synced } = useProfile(address); + const { authFetch, isAuthed, signing, signIn, signError } = useAuth(); const [role, setRole] = useState(null); const [loading, setLoading] = useState(false); @@ -87,7 +89,7 @@ export default function Setup() { if (role === 'contractor' && !form.github.trim()) return; setLoading(true); setError(null); - const result = await saveProfile({ role, ...form }); + const result = await saveProfile({ role, ...form }, { authFetch }); setLoading(false); if (!result?.ok) { setError(result?.error ?? 'Something went wrong. Please try again.'); @@ -107,6 +109,42 @@ export default function Setup() { ); } + // Require a wallet signature (SIWE) BEFORE the onboarding form. Saving the + // profile needs an authenticated session; gating here means the user can never + // fill the form only to have the save rejected for lack of a signature. + if (!isAuthed) { + return ( +
+ Sign a message to prove you own this wallet. It is free, off-chain, and uses no gas. + You only do this once to start onboarding. +
+ + {signError && ( +{signError}
+ )} +