+185
app/api/change-request/accept/route.ts
+185
app/api/change-request/accept/route.ts
···
1
+
import { NextRequest, NextResponse } from 'next/server'
2
+
import { getSession } from '@/lib/session'
3
+
import { getGlobalOAuthClient } from '@/lib/auth/client'
4
+
import { Agent } from '@atproto/api'
5
+
import { getPdsEndpoint } from '@atproto/common-web'
6
+
import { IdResolver } from '@atproto/identity'
7
+
import { TID } from '@atproto/common'
8
+
9
+
async function getSessionAgent(): Promise<{ agent: Agent; oauthSession: any } | null> {
10
+
try {
11
+
const session = await getSession()
12
+
13
+
if (!session.did) {
14
+
return null
15
+
}
16
+
17
+
const client = await getGlobalOAuthClient()
18
+
const oauthSession = await client.restore(session.did)
19
+
20
+
if (!oauthSession) {
21
+
console.log('OAuth session restoration failed')
22
+
return null
23
+
}
24
+
25
+
const agent = new Agent(oauthSession)
26
+
return { agent, oauthSession }
27
+
} catch (error) {
28
+
console.error('Session restore failed:', error)
29
+
return null
30
+
}
31
+
}
32
+
33
+
// POST - Accept a change request and apply the proposed changes
34
+
export async function POST(request: NextRequest) {
35
+
try {
36
+
console.log('POST /api/change-request/accept - Starting request')
37
+
38
+
const sessionResult = await getSessionAgent()
39
+
if (!sessionResult) {
40
+
console.log('No agent available - authentication required')
41
+
return NextResponse.json({ error: 'Authentication required' }, { status: 401 })
42
+
}
43
+
44
+
const { agent, oauthSession } = sessionResult
45
+
const body = await request.json()
46
+
const { changeRequestUri } = body
47
+
48
+
if (!changeRequestUri) {
49
+
return NextResponse.json({ error: 'changeRequestUri required' }, { status: 400 })
50
+
}
51
+
52
+
// Parse the change request URI to get DID and rkey
53
+
const uriParts = changeRequestUri.replace('at://', '').split('/')
54
+
const requesterDid = uriParts[0]
55
+
const changeRequestRkey = uriParts[2]
56
+
57
+
console.log('Fetching change request:', { requesterDid, changeRequestRkey })
58
+
59
+
// Resolve requester's DID to get their PDS
60
+
const resolver = new IdResolver()
61
+
const requesterDidDoc = await resolver.did.resolve(requesterDid)
62
+
63
+
if (!requesterDidDoc) {
64
+
return NextResponse.json({ error: 'Could not resolve requester DID' }, { status: 404 })
65
+
}
66
+
67
+
const requesterPdsEndpoint = getPdsEndpoint(requesterDidDoc)
68
+
if (!requesterPdsEndpoint) {
69
+
return NextResponse.json({ error: 'No PDS endpoint found for requester' }, { status: 404 })
70
+
}
71
+
72
+
// Create agent for requester's PDS to fetch the change request
73
+
const requesterAgent = new Agent({ service: requesterPdsEndpoint })
74
+
75
+
// Fetch the change request record
76
+
const changeRequestResponse = await requesterAgent.com.atproto.repo.getRecord({
77
+
repo: requesterDid,
78
+
collection: 'org.impactindexer.changeRequest',
79
+
rkey: changeRequestRkey
80
+
})
81
+
82
+
if (!changeRequestResponse.success) {
83
+
return NextResponse.json({ error: 'Change request not found' }, { status: 404 })
84
+
}
85
+
86
+
const changeRequestData = changeRequestResponse.data.value as any
87
+
88
+
// Verify that the current user is the target (project owner)
89
+
if (changeRequestData.targetDid !== agent.assertDid) {
90
+
return NextResponse.json({
91
+
error: 'Unauthorized - you can only accept change requests for your own projects'
92
+
}, { status: 403 })
93
+
}
94
+
95
+
console.log('Fetching proposed record data...')
96
+
97
+
// Fetch the proposed record data
98
+
const proposedUri = changeRequestData.proposedRecord
99
+
const proposedUriParts = proposedUri.replace('at://', '').split('/')
100
+
const proposedRkey = proposedUriParts[2]
101
+
102
+
const proposedResponse = await requesterAgent.com.atproto.repo.getRecord({
103
+
repo: requesterDid,
104
+
collection: 'org.impactindexer.status',
105
+
rkey: proposedRkey
106
+
})
107
+
108
+
if (!proposedResponse.success) {
109
+
return NextResponse.json({ error: 'Proposed record not found' }, { status: 404 })
110
+
}
111
+
112
+
const proposedData = proposedResponse.data.value as any
113
+
114
+
console.log('Applying proposed changes to project owner record...')
115
+
116
+
// Check if the project owner has an existing record
117
+
let existingRecord = null
118
+
let rkey = TID.nextStr()
119
+
120
+
try {
121
+
const listResponse = await agent.com.atproto.repo.listRecords({
122
+
repo: agent.assertDid,
123
+
collection: 'org.impactindexer.status',
124
+
limit: 1
125
+
})
126
+
127
+
if (listResponse.data.records.length > 0) {
128
+
existingRecord = listResponse.data.records[0]
129
+
rkey = existingRecord.uri.split('/').pop() || TID.nextStr()
130
+
}
131
+
} catch (error) {
132
+
console.log('No existing record found, will create new one')
133
+
}
134
+
135
+
// Create the updated record with proposed data, preserving timestamps appropriately
136
+
const now = new Date().toISOString()
137
+
const updatedRecord = {
138
+
$type: 'org.impactindexer.status',
139
+
displayName: proposedData.displayName,
140
+
description: proposedData.description,
141
+
website: proposedData.website,
142
+
fundingReceived: proposedData.fundingReceived,
143
+
fundingGivenOut: proposedData.fundingGivenOut,
144
+
annualBudget: proposedData.annualBudget,
145
+
teamSize: proposedData.teamSize,
146
+
sustainableRevenuePercent: proposedData.sustainableRevenuePercent,
147
+
categories: proposedData.categories,
148
+
impactMetrics: proposedData.impactMetrics,
149
+
geographicDistribution: proposedData.geographicDistribution,
150
+
createdAt: (existingRecord?.value as any)?.createdAt || now,
151
+
updatedAt: now,
152
+
}
153
+
154
+
console.log('Final record to save:', updatedRecord)
155
+
156
+
// Remove undefined fields
157
+
Object.keys(updatedRecord).forEach(key => {
158
+
if ((updatedRecord as any)[key] === undefined) {
159
+
delete (updatedRecord as any)[key]
160
+
}
161
+
})
162
+
163
+
// Save the updated record to the project owner's repository
164
+
const response = await agent.com.atproto.repo.putRecord({
165
+
repo: agent.assertDid,
166
+
collection: 'org.impactindexer.status',
167
+
rkey,
168
+
record: updatedRecord,
169
+
validate: false,
170
+
})
171
+
172
+
console.log('Successfully updated project owner record')
173
+
174
+
return NextResponse.json({
175
+
success: true,
176
+
updatedRecordUri: response.data.uri,
177
+
updatedRecordCid: response.data.cid,
178
+
appliedChanges: updatedRecord
179
+
})
180
+
181
+
} catch (error) {
182
+
console.error('Accept change request failed:', error)
183
+
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
184
+
}
185
+
}
+92
-18
app/change-request/[uri]/page.tsx
+92
-18
app/change-request/[uri]/page.tsx
···
26
26
const [changeRequest, setChangeRequest] = useState<ChangeRequestData | null>(null)
27
27
const [loading, setLoading] = useState(true)
28
28
const [error, setError] = useState<string | null>(null)
29
+
const [accepting, setAccepting] = useState(false)
30
+
const [acceptSuccess, setAcceptSuccess] = useState(false)
31
+
const [acceptError, setAcceptError] = useState<string | null>(null)
32
+
33
+
const acceptChanges = async () => {
34
+
if (!changeRequest || accepting) return
35
+
36
+
setAccepting(true)
37
+
setAcceptError(null)
38
+
39
+
try {
40
+
const response = await fetch('/api/change-request/accept', {
41
+
method: 'POST',
42
+
headers: {
43
+
'Content-Type': 'application/json',
44
+
},
45
+
body: JSON.stringify({
46
+
changeRequestUri: uri
47
+
})
48
+
})
49
+
50
+
if (!response.ok) {
51
+
const errorData = await response.json()
52
+
throw new Error(errorData.error || 'Failed to accept changes')
53
+
}
54
+
55
+
const result = await response.json()
56
+
console.log('Changes accepted successfully:', result)
57
+
setAcceptSuccess(true)
58
+
} catch (error) {
59
+
console.error('Error accepting changes:', error)
60
+
setAcceptError(error instanceof Error ? error.message : 'Failed to accept changes')
61
+
} finally {
62
+
setAccepting(false)
63
+
}
64
+
}
29
65
30
66
useEffect(() => {
31
67
const fetchChangeRequest = async () => {
···
223
259
</div>
224
260
)}
225
261
226
-
{/* Future: Action buttons for project owners */}
262
+
{/* Action buttons for project owners */}
227
263
<div className="bg-surface border-subtle shadow-card p-6">
228
264
<h3 className="font-medium text-primary mb-2">Project Owner Actions</h3>
229
-
<p className="text-sm text-secondary mb-4">
230
-
As the project owner, you can review the proposed changes and decide whether to accept them.
231
-
</p>
232
-
<div className="flex space-x-3">
233
-
<button
234
-
disabled
235
-
className="px-4 py-2 bg-surface-hover text-muted rounded-lg cursor-not-allowed"
236
-
>
237
-
Accept Changes (Coming Soon)
238
-
</button>
239
-
<button
240
-
disabled
241
-
className="px-4 py-2 bg-surface-hover text-muted rounded-lg cursor-not-allowed"
242
-
>
243
-
Decline (Coming Soon)
244
-
</button>
245
-
</div>
265
+
266
+
{acceptSuccess ? (
267
+
<div className="bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-lg p-4 mb-4">
268
+
<h4 className="font-medium text-green-800 dark:text-green-200 mb-1">
269
+
✅ Changes Accepted Successfully!
270
+
</h4>
271
+
<p className="text-green-700 dark:text-green-300 text-sm">
272
+
The proposed changes have been applied to your project. Your project information has been updated.
273
+
</p>
274
+
<Link
275
+
href={`/project/${encodeURIComponent(changeRequest?.targetDid || '')}`}
276
+
className="inline-flex items-center mt-2 text-sm text-green-600 dark:text-green-400 hover:text-green-800 dark:hover:text-green-200 transition-colors"
277
+
>
278
+
View Updated Project
279
+
<ExternalLink className="h-4 w-4 ml-1" />
280
+
</Link>
281
+
</div>
282
+
) : (
283
+
<>
284
+
<p className="text-sm text-secondary mb-4">
285
+
As the project owner, you can review the proposed changes and decide whether to accept them.
286
+
</p>
287
+
288
+
{acceptError && (
289
+
<div className="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4 mb-4">
290
+
<h4 className="font-medium text-red-800 dark:text-red-200 mb-1">
291
+
Error Accepting Changes
292
+
</h4>
293
+
<p className="text-red-700 dark:text-red-300 text-sm">
294
+
{acceptError}
295
+
</p>
296
+
</div>
297
+
)}
298
+
299
+
<div className="flex space-x-3">
300
+
<button
301
+
onClick={acceptChanges}
302
+
disabled={accepting || !changeRequest}
303
+
className={`px-4 py-2 rounded-lg transition-colors ${
304
+
accepting || !changeRequest
305
+
? 'bg-surface-hover text-muted cursor-not-allowed'
306
+
: 'bg-green-600 hover:bg-green-700 text-white'
307
+
}`}
308
+
>
309
+
{accepting ? 'Accepting...' : 'Accept Changes'}
310
+
</button>
311
+
<button
312
+
disabled
313
+
className="px-4 py-2 bg-surface-hover text-muted rounded-lg cursor-not-allowed"
314
+
>
315
+
Decline (Coming Soon)
316
+
</button>
317
+
</div>
318
+
</>
319
+
)}
246
320
</div>
247
321
</div>
248
322
)