fix link to proposed record #3

merged
opened by adamspiers.org targeting main from adamspiers.org/eii-frontend: change-requests
Changed files
+199 -1
app
api
change-request
accept
change-request
[uri]
+185
app/api/change-request/accept/route.ts
···
+
import { NextRequest, NextResponse } from 'next/server'
+
import { getSession } from '@/lib/session'
+
import { getGlobalOAuthClient } from '@/lib/auth/client'
+
import { Agent } from '@atproto/api'
+
import { getPdsEndpoint } from '@atproto/common-web'
+
import { IdResolver } from '@atproto/identity'
+
import { TID } from '@atproto/common'
+
+
async function getSessionAgent(): Promise<{ agent: Agent; oauthSession: any } | null> {
+
try {
+
const session = await getSession()
+
+
if (!session.did) {
+
return null
+
}
+
+
const client = await getGlobalOAuthClient()
+
const oauthSession = await client.restore(session.did)
+
+
if (!oauthSession) {
+
console.log('OAuth session restoration failed')
+
return null
+
}
+
+
const agent = new Agent(oauthSession)
+
return { agent, oauthSession }
+
} catch (error) {
+
console.error('Session restore failed:', error)
+
return null
+
}
+
}
+
+
// POST - Accept a change request and apply the proposed changes
+
export async function POST(request: NextRequest) {
+
try {
+
console.log('POST /api/change-request/accept - Starting request')
+
+
const sessionResult = await getSessionAgent()
+
if (!sessionResult) {
+
console.log('No agent available - authentication required')
+
return NextResponse.json({ error: 'Authentication required' }, { status: 401 })
+
}
+
+
const { agent, oauthSession } = sessionResult
+
const body = await request.json()
+
const { changeRequestUri } = body
+
+
if (!changeRequestUri) {
+
return NextResponse.json({ error: 'changeRequestUri required' }, { status: 400 })
+
}
+
+
// Parse the change request URI to get DID and rkey
+
const uriParts = changeRequestUri.replace('at://', '').split('/')
+
const requesterDid = uriParts[0]
+
const changeRequestRkey = uriParts[2]
+
+
console.log('Fetching change request:', { requesterDid, changeRequestRkey })
+
+
// Resolve requester's DID to get their PDS
+
const resolver = new IdResolver()
+
const requesterDidDoc = await resolver.did.resolve(requesterDid)
+
+
if (!requesterDidDoc) {
+
return NextResponse.json({ error: 'Could not resolve requester DID' }, { status: 404 })
+
}
+
+
const requesterPdsEndpoint = getPdsEndpoint(requesterDidDoc)
+
if (!requesterPdsEndpoint) {
+
return NextResponse.json({ error: 'No PDS endpoint found for requester' }, { status: 404 })
+
}
+
+
// Create agent for requester's PDS to fetch the change request
+
const requesterAgent = new Agent({ service: requesterPdsEndpoint })
+
+
// Fetch the change request record
+
const changeRequestResponse = await requesterAgent.com.atproto.repo.getRecord({
+
repo: requesterDid,
+
collection: 'org.impactindexer.changeRequest',
+
rkey: changeRequestRkey
+
})
+
+
if (!changeRequestResponse.success) {
+
return NextResponse.json({ error: 'Change request not found' }, { status: 404 })
+
}
+
+
const changeRequestData = changeRequestResponse.data.value as any
+
+
// Verify that the current user is the target (project owner)
+
if (changeRequestData.targetDid !== agent.assertDid) {
+
return NextResponse.json({
+
error: 'Unauthorized - you can only accept change requests for your own projects'
+
}, { status: 403 })
+
}
+
+
console.log('Fetching proposed record data...')
+
+
// Fetch the proposed record data
+
const proposedUri = changeRequestData.proposedRecord
+
const proposedUriParts = proposedUri.replace('at://', '').split('/')
+
const proposedRkey = proposedUriParts[2]
+
+
const proposedResponse = await requesterAgent.com.atproto.repo.getRecord({
+
repo: requesterDid,
+
collection: 'org.impactindexer.status',
+
rkey: proposedRkey
+
})
+
+
if (!proposedResponse.success) {
+
return NextResponse.json({ error: 'Proposed record not found' }, { status: 404 })
+
}
+
+
const proposedData = proposedResponse.data.value as any
+
+
console.log('Applying proposed changes to project owner record...')
+
+
// Check if the project owner has an existing record
+
let existingRecord = null
+
let rkey = TID.nextStr()
+
+
try {
+
const listResponse = await agent.com.atproto.repo.listRecords({
+
repo: agent.assertDid,
+
collection: 'org.impactindexer.status',
+
limit: 1
+
})
+
+
if (listResponse.data.records.length > 0) {
+
existingRecord = listResponse.data.records[0]
+
rkey = existingRecord.uri.split('/').pop() || TID.nextStr()
+
}
+
} catch (error) {
+
console.log('No existing record found, will create new one')
+
}
+
+
// Create the updated record with proposed data, preserving timestamps appropriately
+
const now = new Date().toISOString()
+
const updatedRecord = {
+
$type: 'org.impactindexer.status',
+
displayName: proposedData.displayName,
+
description: proposedData.description,
+
website: proposedData.website,
+
fundingReceived: proposedData.fundingReceived,
+
fundingGivenOut: proposedData.fundingGivenOut,
+
annualBudget: proposedData.annualBudget,
+
teamSize: proposedData.teamSize,
+
sustainableRevenuePercent: proposedData.sustainableRevenuePercent,
+
categories: proposedData.categories,
+
impactMetrics: proposedData.impactMetrics,
+
geographicDistribution: proposedData.geographicDistribution,
+
createdAt: (existingRecord?.value as any)?.createdAt || now,
+
updatedAt: now,
+
}
+
+
console.log('Final record to save:', updatedRecord)
+
+
// Remove undefined fields
+
Object.keys(updatedRecord).forEach(key => {
+
if ((updatedRecord as any)[key] === undefined) {
+
delete (updatedRecord as any)[key]
+
}
+
})
+
+
// Save the updated record to the project owner's repository
+
const response = await agent.com.atproto.repo.putRecord({
+
repo: agent.assertDid,
+
collection: 'org.impactindexer.status',
+
rkey,
+
record: updatedRecord,
+
validate: false,
+
})
+
+
console.log('Successfully updated project owner record')
+
+
return NextResponse.json({
+
success: true,
+
updatedRecordUri: response.data.uri,
+
updatedRecordCid: response.data.cid,
+
appliedChanges: updatedRecord
+
})
+
+
} catch (error) {
+
console.error('Accept change request failed:', error)
+
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
+
}
+
}
+14 -1
app/change-request/[uri]/page.tsx
···
const originalValue = changeRequest.originalData?.[key]
const proposedValue = changeRequest.proposedData[key]
-
if (originalValue === proposedValue) return null
+
// Deep comparison for arrays and objects
+
const valuesAreEqual = () => {
+
if (originalValue === proposedValue) return true
+
if (Array.isArray(originalValue) && Array.isArray(proposedValue)) {
+
return originalValue.length === proposedValue.length &&
+
originalValue.every((item, index) => item === proposedValue[index])
+
}
+
if (typeof originalValue === 'object' && typeof proposedValue === 'object') {
+
return JSON.stringify(originalValue) === JSON.stringify(proposedValue)
+
}
+
return false
+
}
+
+
if (valuesAreEqual()) return null
return (
<div key={key} className="border-l-2 border-accent pl-4">