Highly ambitious ATProtocol AppView service and sdks

refactor sync routes to use params instead of query params

Changed files
+116 -62
frontend
src
features
+108 -58
frontend/src/features/slices/sync/handlers.tsx
··· 2 2 import { renderHTML } from "../../../utils/render.tsx"; 3 3 import { requireAuth, withAuth } from "../../../routes/middleware.ts"; 4 4 import { getSliceClient } from "../../../utils/client.ts"; 5 - import { buildSliceUri } from "../../../utils/at-uri.ts"; 6 - import { publicClient } from "../../../config.ts"; 7 5 import { 8 6 requireSliceAccess, 9 7 withSliceAccess, ··· 23 21 req: Request, 24 22 params?: URLPatternResult 25 23 ): Promise<Response> { 26 - const context = await withAuth(req); 27 - const authResponse = requireAuth(context); 24 + const authContext = await withAuth(req); 25 + const authResponse = requireAuth(authContext); 28 26 if (authResponse) return authResponse; 29 27 30 - const sliceId = params?.pathname.groups.id; 31 - 32 - if (!sliceId) { 33 - return renderHTML(<SyncResult success={false} error="Invalid slice ID" />); 28 + const sliceParams = extractSliceParams(params); 29 + if (!sliceParams) { 30 + return renderHTML( 31 + <SyncResult success={false} error="Invalid slice parameters" /> 32 + ); 34 33 } 35 34 35 + const context = await withSliceAccess( 36 + authContext, 37 + sliceParams.handle, 38 + sliceParams.sliceId 39 + ); 40 + const accessError = requireSliceAccess(context); 41 + if (accessError) return accessError; 42 + 36 43 try { 37 44 const formData = await req.formData(); 38 45 const collections = formData.getAll("collections") as string[]; ··· 55 62 ); 56 63 } 57 64 58 - const sliceClient = getSliceClient(context, sliceId); 65 + const sliceClient = getSliceClient( 66 + authContext, 67 + sliceParams.sliceId, 68 + context.sliceContext!.profileDid 69 + ); 59 70 await sliceClient.network.slices.slice.startSync({ 60 - slice: buildSliceUri(context.currentUser.sub!, sliceId), 71 + slice: context.sliceContext!.sliceUri, 61 72 collections: collections.length > 0 ? collections : undefined, 62 73 externalCollections: 63 74 externalCollections.length > 0 ? externalCollections : undefined, 64 75 repos: repos.length > 0 ? repos : undefined, 65 76 }); 66 77 67 - const handle = context.currentUser?.handle; 68 - if (!handle) { 69 - throw new Error("Unable to determine user handle"); 70 - } 71 - 72 - const redirectUrl = buildSliceUrl(handle, sliceId, "sync"); 78 + const redirectUrl = buildSliceUrl( 79 + sliceParams.handle, 80 + sliceParams.sliceId, 81 + "sync" 82 + ); 73 83 return hxRedirect(redirectUrl); 74 84 } catch (error) { 75 85 console.error("Failed to start sync:", error); ··· 82 92 req: Request, 83 93 params?: URLPatternResult 84 94 ): Promise<Response> { 85 - const context = await withAuth(req); 86 - const authResponse = requireAuth(context); 95 + const authContext = await withAuth(req); 96 + const authResponse = requireAuth(authContext); 87 97 if (authResponse) return authResponse; 88 98 89 - const sliceId = params?.pathname.groups.id; 90 - 91 - if (!sliceId) { 99 + const sliceParams = extractSliceParams(params); 100 + if (!sliceParams) { 92 101 return renderHTML( 93 - <div className="p-8 text-center text-red-600">Invalid slice ID</div>, 102 + <div className="p-8 text-center text-red-600"> 103 + Invalid slice parameters 104 + </div>, 94 105 { status: 400 } 95 106 ); 96 107 } 97 108 98 - // Extract handle from query parameters 99 - const url = new URL(req.url); 100 - const handle = url.searchParams.get("handle"); 109 + const context = await withSliceAccess( 110 + authContext, 111 + sliceParams.handle, 112 + sliceParams.sliceId 113 + ); 114 + const accessError = requireSliceAccess(context); 115 + if (accessError) return accessError; 101 116 102 117 try { 103 - const sliceUri = buildSliceUri(context.currentUser.sub!, sliceId); 104 - const sliceClient = getSliceClient(context, sliceId); 118 + const sliceClient = getSliceClient( 119 + authContext, 120 + sliceParams.sliceId, 121 + context.sliceContext!.profileDid 122 + ); 105 123 const jobsResponse = await sliceClient.network.slices.slice.getJobHistory({ 106 - userDid: context.currentUser.sub!, 107 - sliceUri: sliceUri, 124 + userDid: authContext.currentUser.sub!, 125 + sliceUri: context.sliceContext!.sliceUri, 108 126 limit: 10, 109 127 }); 110 128 111 129 return renderHTML( 112 130 <JobHistory 113 131 jobs={jobsResponse || []} 114 - sliceId={sliceId} 115 - handle={handle || undefined} 132 + sliceId={sliceParams.sliceId} 133 + handle={sliceParams.handle} 116 134 /> 117 135 ); 118 136 } catch (error) { ··· 191 209 req: Request, 192 210 params?: URLPatternResult 193 211 ): Promise<Response> { 194 - const context = await withAuth(req); 195 - const authResponse = requireAuth(context); 212 + const authContext = await withAuth(req); 213 + const authResponse = requireAuth(authContext); 196 214 if (authResponse) return authResponse; 197 215 198 - const sliceId = params?.pathname.groups.id; 199 - if (!sliceId) { 200 - return new Response("Invalid slice ID", { status: 400 }); 216 + const sliceParams = extractSliceParams(params); 217 + if (!sliceParams) { 218 + return new Response("Invalid slice parameters", { status: 400 }); 201 219 } 202 220 221 + const context = await withSliceAccess( 222 + authContext, 223 + sliceParams.handle, 224 + sliceParams.sliceId 225 + ); 226 + const accessError = requireSliceAccess(context); 227 + if (accessError) return accessError; 228 + 203 229 try { 204 - const sliceClient = getSliceClient(context, sliceId); 230 + const sliceClient = getSliceClient( 231 + authContext, 232 + sliceParams.sliceId, 233 + context.sliceContext!.profileDid 234 + ); 205 235 const collections: string[] = []; 206 236 const externalCollections: string[] = []; 207 237 208 - // Get slice info for domain comparison 209 - const sliceUri = buildSliceUri(context.currentUser.sub!, sliceId); 210 - const sliceRecord = await publicClient.network.slices.slice.getRecord({ 211 - uri: sliceUri, 212 - }); 213 - const sliceDomain = sliceRecord.value.domain; 238 + // Get slice domain from context 239 + const sliceDomain = context.sliceContext!.slice!.domain; 214 240 215 241 // Get all lexicons and filter by record types 216 242 try { ··· 241 267 242 268 return renderHTML( 243 269 <SyncFormModal 244 - sliceId={sliceId} 270 + sliceId={sliceParams.sliceId} 271 + handle={sliceParams.handle} 245 272 collections={collections} 246 273 externalCollections={externalCollections} 247 274 /> 248 275 ); 249 276 } catch (error) { 250 277 console.error("Error loading sync modal:", error); 251 - return renderHTML(<SyncFormModal sliceId={sliceId} />); 278 + return renderHTML( 279 + <SyncFormModal 280 + sliceId={sliceParams.sliceId} 281 + handle={sliceParams.handle} 282 + /> 283 + ); 252 284 } 253 285 } 254 286 ··· 256 288 req: Request, 257 289 params?: URLPatternResult 258 290 ): Promise<Response> { 259 - const context = await withAuth(req); 260 - const authResponse = requireAuth(context); 291 + const authContext = await withAuth(req); 292 + const authResponse = requireAuth(authContext); 261 293 if (authResponse) return authResponse; 262 294 263 - const sliceId = params?.pathname.groups.id; 264 - if (!sliceId) { 265 - return new Response("Invalid slice ID", { status: 400 }); 295 + const sliceParams = extractSliceParams(params); 296 + if (!sliceParams) { 297 + return new Response("Invalid slice parameters", { status: 400 }); 266 298 } 299 + 300 + const context = await withSliceAccess( 301 + authContext, 302 + sliceParams.handle, 303 + sliceParams.sliceId 304 + ); 305 + const accessError = requireSliceAccess(context); 306 + if (accessError) return accessError; 267 307 268 308 try { 269 309 const formData = await req.formData(); ··· 287 327 ); 288 328 } 289 329 290 - const sliceClient = getSliceClient(context, sliceId); 291 - const sliceUri = buildSliceUri(context.currentUser.sub!, sliceId); 330 + const sliceClient = getSliceClient( 331 + authContext, 332 + sliceParams.sliceId, 333 + context.sliceContext!.profileDid 334 + ); 292 335 293 336 // Call the getSyncSummary endpoint 294 337 const requestParams = { 295 - slice: sliceUri, 338 + slice: context.sliceContext!.sliceUri, 296 339 collections: collections.length > 0 ? collections : undefined, 297 340 externalCollections: 298 341 externalCollections.length > 0 ? externalCollections : undefined, ··· 304 347 305 348 return renderHTML( 306 349 <SyncSummaryModal 307 - sliceId={sliceId} 350 + sliceId={sliceParams.sliceId} 351 + handle={sliceParams.handle} 308 352 summary={summaryResponse} 309 353 collections={collections} 310 354 externalCollections={externalCollections} ··· 326 370 }, 327 371 { 328 372 method: "GET", 329 - pattern: new URLPattern({ pathname: "/api/slices/:id/sync/modal" }), 373 + pattern: new URLPattern({ 374 + pathname: "/profile/:handle/slice/:rkey/sync/modal", 375 + }), 330 376 handler: handleShowSyncModal, 331 377 }, 332 378 { 333 379 method: "POST", 334 - pattern: new URLPattern({ pathname: "/api/slices/:id/sync" }), 380 + pattern: new URLPattern({ pathname: "/profile/:handle/slice/:rkey/sync" }), 335 381 handler: handleSliceSync, 336 382 }, 337 383 { 338 384 method: "POST", 339 - pattern: new URLPattern({ pathname: "/api/slices/:id/sync/summary" }), 385 + pattern: new URLPattern({ 386 + pathname: "/profile/:handle/slice/:rkey/sync/summary", 387 + }), 340 388 handler: handleSyncSummary, 341 389 }, 342 390 { 343 391 method: "GET", 344 - pattern: new URLPattern({ pathname: "/api/slices/:id/job-history" }), 392 + pattern: new URLPattern({ 393 + pathname: "/profile/:handle/slice/:rkey/job-history", 394 + }), 345 395 handler: handleJobHistory, 346 396 }, 347 397 ];
+2 -2
frontend/src/features/slices/sync/templates/SliceSyncPage.tsx
··· 31 31 <div className="flex justify-end mb-4"> 32 32 <Button 33 33 variant="success" 34 - hx-get={`/api/slices/${sliceId}/sync/modal`} 34 + hx-get={`/profile/${slice.creator?.handle}/slice/${sliceId}/sync/modal`} 35 35 hx-target="#modal-container" 36 36 hx-swap="innerHTML" 37 37 > ··· 41 41 <Card> 42 42 <Card.Header title="Recent Sync History" /> 43 43 <Card.Content 44 - hx-get={`/api/slices/${sliceId}/job-history?handle=${slice.creator?.handle}`} 44 + hx-get={`/profile/${slice.creator?.handle}/slice/${sliceId}/job-history`} 45 45 hx-trigger="load, every 10s" 46 46 hx-swap="innerHTML" 47 47 >
+3 -1
frontend/src/features/slices/sync/templates/fragments/SyncFormModal.tsx
··· 5 5 6 6 interface SyncFormModalProps { 7 7 sliceId: string; 8 + handle: string; 8 9 collections?: string[]; 9 10 externalCollections?: string[]; 10 11 } 11 12 12 13 export function SyncFormModal({ 13 14 sliceId, 15 + handle, 14 16 collections = [], 15 17 externalCollections = [], 16 18 }: SyncFormModalProps) { ··· 95 97 type="submit" 96 98 variant="primary" 97 99 className="flex items-center justify-center" 98 - hx-post={`/api/slices/${sliceId}/sync/summary`} 100 + hx-post={`/profile/${handle}/slice/${sliceId}/sync/summary`} 99 101 hx-target="#modal-container" 100 102 hx-swap="innerHTML" 101 103 >
+3 -1
frontend/src/features/slices/sync/templates/fragments/SyncSummaryModal.tsx
··· 5 5 6 6 interface SyncSummaryModalProps { 7 7 sliceId: string; 8 + handle: string; 8 9 summary: NetworkSlicesSliceGetSyncSummaryOutput; 9 10 collections: string[]; 10 11 externalCollections: string[]; ··· 13 14 14 15 export function SyncSummaryModal({ 15 16 sliceId, 17 + handle, 16 18 summary, 17 19 collections, 18 20 externalCollections, ··· 144 146 145 147 {/* Actions */} 146 148 <form 147 - hx-post={`/api/slices/${sliceId}/sync`} 149 + hx-post={`/profile/${handle}/slice/${sliceId}/sync`} 148 150 hx-target="#sync-result" 149 151 hx-swap="innerHTML" 150 152 className="space-y-4"