diff --git a/backend/src/app.ts b/backend/src/app.ts index cef7abbd..a6ceec17 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -513,7 +513,25 @@ app.get('/api/bounties/:id/events', (req: Request, res: Response) => { } }); -app.get('/api/bounties/:id', (req: Request, res: Response) => { +app.patch( + '/api/bounties/:id/metadata', + async (req: Request, res: Response) => { + try { + const { id } = req.params; + const { maintainer, newTitle } = req.body; + + if (!maintainer || !newTitle) { + jsonError(res, req, 400, 'Maintainer address and newTitle are required.'); + return; + } + + const updatedBounty = await updateBountyMetadata(id, maintainer, newTitle); + res.json({ data: updatedBounty }); + } catch (error) { + sendError(res, req, error); + } + } +); try { const id = parseId(req.params.id); const bounties = listBounties(); diff --git a/backend/src/routes/bounties.ts b/backend/src/routes/bounties.ts index e69de29b..b85ff66a 100644 --- a/backend/src/routes/bounties.ts +++ b/backend/src/routes/bounties.ts @@ -0,0 +1,20 @@ +import { Request, Response } from 'express'; +import { updateBountyMetadata } from '../services/bountyStore'; +import { sendError } from '../app'; + +export const updateBountyMetadataHandler = async (req: Request, res: Response) => { + try { + const { id } = req.params; + const { maintainer, newTitle } = req.body; + + if (!maintainer || !newTitle) { + res.status(400).json({ error: 'Maintainer address and newTitle are required.' }); + return; + } + + const updatedBounty = await updateBountyMetadata(id, maintainer, newTitle); + res.json({ data: updatedBounty }); + } catch (error) { + sendError(res, req, error); + } +}; diff --git a/backend/src/services/bountyStore.ts b/backend/src/services/bountyStore.ts index 0ff07919..e808182b 100644 --- a/backend/src/services/bountyStore.ts +++ b/backend/src/services/bountyStore.ts @@ -498,6 +498,66 @@ const BOUNTY_LIST_TTL_SECONDS = 5; * @param {CacheAdapter} [cache=getCache()] - The cache adapter to use for caching. * @returns {Promise} A promise that resolves to the sorted and filtered list of bounty records. */ +export function updateBountyMetadata( + id: string, + maintainer: string, + newTitle: string, +): BountyRecord { + return withGlobalLock(async () => { + const records = listBounties(); + const bounty = findBounty(records, id); + + if (bounty.maintainer !== maintainer) { + throw new Error("Only the original maintainer can update bounty metadata."); + } + if ( + bounty.status === "released" || + bounty.status === "refunded" || + bounty.status === "expired" + ) { + throw new Error("Cannot update metadata for finalized bounties."); + } + + const now = nowInSeconds(); + const updated: BountyRecord = { + ...bounty, + title: newTitle, + version: bounty.version + 1, + events: [ + ...bounty.events, + { + type: "created" as any, // Using 'created' as a placeholder for metadata update + timestamp: now, + actor: maintainer, + details: { + oldTitle: bounty.title, + newTitle: newTitle, + event: "BountyMetadataUpdated", + }, + }, + ], + }; + + const persisted = persistUpdated(records, updated); + appendAuditLogs([ + { + bountyId: id, + fromStatus: bounty.status, + toStatus: bounty.status, + transition: "create" as any, // Placeholder transition + actor: maintainer, + timestamp: now, + metadata: { + oldTitle: bounty.title, + newTitle: newTitle, + }, + }, + ]); + invalidateBountyCache(); + return persisted; + }); +} + export async function listBountiesCached( options: ListBountiesOptions = {}, cache: CacheAdapter = getCache(),