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
26 changes: 12 additions & 14 deletions client/src/components/admin/ProgramJudgingSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ const prizeText = (amount?: number | null, currency?: string | null) =>
export function ProgramJudgingSection({
programSlug,
getAuth,
signWinnerAction,
signPublishAction,
canSelectWinners = false,
prizeTiers,
resultsPublishedAt = null,
Expand All @@ -48,13 +48,12 @@ export function ProgramJudgingSection({
programSlug: string;
getAuth: () => Promise<AdminAuthArg>;
/**
* Fresh-signature source for the high-stakes winner/payout actions
* (award prize, publish results, mark paid). When provided, these actions
* pop a wallet signature every time instead of riding the cached admin
* session — a deliberate human confirmation. Reads still use `getAuth`.
* Only wired on the wallet-admin path (`canSelectWinners`).
* Fresh-signature source for PUBLISHING results only — the one irreversible,
* public action we still gate behind a deliberate wallet signature. Everything
* else (scoring, award, mark paid) rides the cached admin session opened at
* sign-in. When omitted, publish falls back to the cached session too.
*/
signWinnerAction?: (action: "mark-paid" | "award-prize" | "publish-results") => Promise<string>;
signPublishAction?: () => Promise<string>;
canSelectWinners?: boolean;
prizeTiers?: ApiPrizeTier[] | null;
resultsPublishedAt?: string | null;
Expand Down Expand Up @@ -216,17 +215,16 @@ export function ProgramJudgingSection({
}
};

// Auth for the high-stakes winner/payout actions: a fresh signature each
// time when wired (wallet admins), else the cached admin session.
const winnerAuth = (action: "mark-paid" | "award-prize" | "publish-results") =>
signWinnerAction ? signWinnerAction(action) : getAuth();
// Publish re-signs fresh (the one action still gated). Everything else rides
// the cached admin session opened at sign-in.
const publishAuth = () => (signPublishAction ? signPublishAction() : getAuth());

// Platform admin: assign a prize tier (winner) to a submission, or clear it.
const award = async (submissionId: string, amountRaw: string) => {
const tier = amountRaw === "" ? null : tiers.find((t) => t.amount === Number(amountRaw)) ?? null;
setAwardingId(submissionId);
try {
const auth = await winnerAuth("award-prize");
const auth = await getAuth();
await api.awardPrize(programSlug, submissionId, tier, auth);
setBoard((prev) =>
prev && !prev.locked
Expand Down Expand Up @@ -256,7 +254,7 @@ export function ProgramJudgingSection({
const markPaid = async (submissionId: string, paid: boolean) => {
setPaidId(submissionId);
try {
const auth = await winnerAuth("mark-paid");
const auth = await getAuth();
await api.setSubmissionPaid(programSlug, submissionId, paid, auth);
setBoard((prev) =>
prev && !prev.locked
Expand All @@ -278,7 +276,7 @@ export function ProgramJudgingSection({
const togglePublish = async () => {
setPublishing(true);
try {
const auth = await winnerAuth("publish-results");
const auth = await publishAuth();
const res = await api.publishResults(programSlug, !publishedAt, auth);
setPublishedAt(res.data.resultsPublishedAt);
onPublishedChange?.(res.data.resultsPublishedAt);
Expand Down
11 changes: 2 additions & 9 deletions client/src/components/admin/WinnersTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -93,17 +93,11 @@ interface WinnersTableProps {
* write below to share the cache.
*/
signAdminAction: () => Promise<import("@/lib/api").AdminAuthArg>;
/**
* Auth for the payout action specifically. Pops a FRESH signature every
* time (never the cached session) so confirming a payout is a deliberate,
* separately-signed step. Falls back to signAdminAction if not provided.
*/
signPaymentAction?: () => Promise<import("@/lib/api").AdminAuthArg>;
}

type WinnerFilter = "all" | "main-track" | "bounty";

export function WinnersTable({ projects, onRefresh, connectedAddress, signAdminAction, signPaymentAction }: WinnersTableProps) {
export function WinnersTable({ projects, onRefresh, connectedAddress, signAdminAction }: WinnersTableProps) {
const { toast } = useToast();
const [saving, setSaving] = useState<string | null>(null);
const [winnerFilter, setWinnerFilter] = useState<WinnerFilter>("all");
Expand Down Expand Up @@ -372,8 +366,7 @@ export function WinnersTable({ projects, onRefresh, connectedAddress, signAdminA
setSaving("payment");

try {
// Confirming a payout re-signs fresh every time (not the cached session).
const authHeader = await (signPaymentAction ?? signAdminAction)();
const authHeader = await signAdminAction();

// Validate
if (!manageModal.paymentAmount || manageModal.paymentAmount <= 0) {
Expand Down
12 changes: 3 additions & 9 deletions client/src/lib/siwsUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
*/

export interface SiwsContext {
action: 'update-team' | 'submit-deliverable' | 'update-project' | 'register-address' | 'admin-action' | 'create-project' | 'delete-project' | 'review-project' | 'approve-project' | 'reject-project' | 'post-update' | 'update-funding-signal' | 'apply-to-program' | 'create-program' | 'update-program' | 'review-application' | 'update-program-sponsors' | 'import-program-signups' | 'submit-continuation' | 'update-notifications' | 'mark-paid' | 'award-prize' | 'publish-results' | 'confirm-payment';
action: 'update-team' | 'submit-deliverable' | 'update-project' | 'register-address' | 'admin-action' | 'create-project' | 'delete-project' | 'review-project' | 'approve-project' | 'reject-project' | 'post-update' | 'update-funding-signal' | 'apply-to-program' | 'create-program' | 'update-program' | 'review-application' | 'update-program-sponsors' | 'import-program-signups' | 'submit-continuation' | 'update-notifications' | 'publish-results';
projectId?: string;
projectTitle?: string;
programTitle?: string;
Expand Down Expand Up @@ -77,16 +77,10 @@ export function generateSiwsStatement(context: SiwsContext): string {
case 'update-notifications':
return `Update notification preferences for wallet on Stadium`;

// High-stakes payout/results actions: each re-signs fresh (never the
// cached admin session) so the human deliberately confirms it.
case 'mark-paid':
return `Mark a payout as sent on ${baseDomain}`;
case 'award-prize':
return `Select a winner on ${baseDomain}`;
// Publishing results is the one judging action still gated by a fresh
// signature (irreversible + public). Everything else rides the session.
case 'publish-results':
return `Publish results on ${baseDomain}`;
case 'confirm-payment':
return `Confirm a milestone payout on ${baseDomain}`;

default:
return `Sign in to ${baseDomain}`;
Expand Down
9 changes: 2 additions & 7 deletions client/src/pages/AdminPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -109,9 +109,7 @@ const AdminPage = () => {
const handleConfirmM1Payout = async (data: any) => {
if (!selectedProject) return;
try {
// Payout confirmation moves real funds — re-sign fresh every time
// (never the cached admin session) so the human deliberately approves it.
const authHeaders = { "x-siws-auth": await auth.signAction("confirm-payment") };
const authHeaders = await auth.getAdminBearerHeaders();
const response = await fetch(`/api/m2-program/${selectedProject.id}/confirm-payment`, {
method: "POST",
headers: { "Content-Type": "application/json", ...authHeaders },
Expand Down Expand Up @@ -148,9 +146,7 @@ const AdminPage = () => {
const handleConfirmPayment = async (data: any) => {
if (!selectedProject) return;
try {
// Payout confirmation moves real funds — re-sign fresh every time
// (never the cached admin session) so the human deliberately approves it.
const authHeaders = { "x-siws-auth": await auth.signAction("confirm-payment") };
const authHeaders = await auth.getAdminBearerHeaders();
const response = await fetch(`/api/m2-program/${selectedProject.id}/confirm-payment`, {
method: "POST",
headers: { "Content-Type": "application/json", ...authHeaders },
Expand Down Expand Up @@ -503,7 +499,6 @@ const AdminPage = () => {
onRefresh={loadData}
connectedAddress={BYPASS_ADMIN_CHECK ? ADMIN_ADDRESSES[0]?.address : auth.account?.address}
signAdminAction={auth.getAdminBearerHeaders}
signPaymentAction={() => auth.signAction("confirm-payment")}
/>
</section>

Expand Down
2 changes: 1 addition & 1 deletion client/src/pages/AdminProgramPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -357,7 +357,7 @@ const AdminProgramPage = () => {
<ProgramJudgingSection
programSlug={program.slug}
getAuth={getAdminAuth}
signWinnerAction={(a) => auth.signAction(a)}
signPublishAction={() => auth.signAction("publish-results")}
canSelectWinners={isGlobalAdmin}
prizeTiers={program.prizeTiers}
resultsPublishedAt={program.resultsPublishedAt}
Expand Down
5 changes: 5 additions & 0 deletions server/api/auth/__tests__/statements.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ describe('validateStatement', () => {
}
});

it('accepts the publish-results statement the client signs (client↔server contract)', () => {
// generateSiwsStatement('publish-results') in client/src/lib/siwsUtils.ts.
expect(validateStatement('Publish results on Stadium')).toBe(true);
});

it('accepts project-specific statements via pattern match', () => {
expect(validateStatement('Update team members for Acme Rocket on Stadium')).toBe(true);
expect(validateStatement('Approve project Acme Rocket on Stadium')).toBe(true);
Expand Down
2 changes: 2 additions & 0 deletions server/api/auth/statements.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ export const VALID_STATEMENTS = [
'Submit project continuation on Stadium',
// Phase 2 revamp: wallet contacts (#67)
'Update notification preferences for wallet on Stadium',
// Publishing results is the one judging action still gated by a fresh signature.
'Publish results on Stadium',
];

// Patterns for project-specific statements that interpolate a project/program name.
Expand Down