Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion frontend/src/hooks/useProfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
40 changes: 39 additions & 1 deletion frontend/src/pages/app/Setup.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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);
Expand All @@ -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.');
Expand All @@ -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 (
<div className="min-h-screen flex items-center justify-center p-4 sm:p-6">
<div className="max-w-md w-full">
<div className="flex items-center gap-2 mb-6">
<div className="w-6 h-6 rounded-md bg-accent/10 border border-accent/20 flex items-center justify-center">
<span className="text-accent text-xs font-mono font-bold">C</span>
</div>
<span className="text-accent font-mono font-semibold text-sm">CronStream</span>
</div>
<div className="card flex flex-col gap-4 text-center py-8">
<h1 className="text-xl font-bold">Verify your wallet</h1>
<p className="text-muted text-sm leading-relaxed max-w-xs mx-auto">
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.
</p>
<button
type="button"
onClick={signIn}
disabled={signing}
className="btn-primary w-full mt-2 disabled:opacity-50"
>
{signing ? 'Check your wallet…' : 'Sign to continue'}
</button>
{signError && (
<p className="text-xs text-red-400 font-mono">{signError}</p>
)}
</div>
</div>
</div>
);
}

return (
<div className="min-h-screen flex items-center justify-center p-4 sm:p-6">
<div className="max-w-lg w-full">
Expand Down
21 changes: 19 additions & 2 deletions ship.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,18 @@
# ship.sh — turn the current working changes into a merged PR in one command.
#
# ./ship.sh "what I shipped"
# ./ship.sh "what I shipped" 0x<stream-id> # route the extension to a specific stream
#
# Flow: branch off main → commit all changes → push → open PR → enable
# auto-merge. CI runs on the PR; once it passes (and any branch-protection
# rules are satisfied) GitHub merges automatically, which fires the CronStream
# webhook and extends the stream.
#
# Pass a stream id as the 2nd argument to embed a
# CronStream-Stream-Id: 0x…
# line in the PR body, which routes the verified extension to that exact stream
# (useful when several streams watch the same repo).
#
# Requirements:
# - git remote 'origin' points at the repo
# - GitHub CLI installed and authenticated: gh auth status
Expand All @@ -17,8 +23,13 @@
set -euo pipefail

MSG="${1:-}"
STREAM_ID="${2:-}"
if [ -z "$MSG" ]; then
echo "Usage: ./ship.sh \"commit message\"" >&2
echo "Usage: ./ship.sh \"commit message\" [0x<stream-id>]" >&2
exit 1
fi
if [ -n "$STREAM_ID" ] && ! [[ "$STREAM_ID" =~ ^0x[a-fA-F0-9]{64}$ ]]; then
echo "✗ Stream id must be 0x followed by 64 hex chars." >&2
exit 1
fi

Expand Down Expand Up @@ -49,7 +60,13 @@ echo "→ Pushing"
git push -u origin "$BRANCH" --quiet

echo "→ Opening PR"
gh pr create --base "$DEFAULT_BRANCH" --head "$BRANCH" --title "$MSG" --fill
if [ -n "$STREAM_ID" ]; then
gh pr create --base "$DEFAULT_BRANCH" --head "$BRANCH" --title "$MSG" \
--body "$MSG"$'\n\n'"CronStream-Stream-Id: $STREAM_ID"
echo " ↳ routing extension to stream $STREAM_ID"
else
gh pr create --base "$DEFAULT_BRANCH" --head "$BRANCH" --title "$MSG" --fill
fi

echo "→ Enabling auto-merge"
# --auto merges as soon as required checks/reviews pass. If auto-merge isn't
Expand Down
Loading