Highly ambitious ATProtocol AppView service and sdks
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix auth redirects

Chad Miller 3a2ce6e2 65f2673b

+65 -27
+1 -1
frontend/deno.json
··· 18 18 "jsxImportSource": "preact" 19 19 }, 20 20 "imports": { 21 - "@slices/oauth": "jsr:@slices/oauth@^0.1.1", 21 + "@slices/oauth": "jsr:@slices/oauth@^0.3.0", 22 22 "@std/assert": "jsr:@std/assert@^1.0.14", 23 23 "preact": "npm:preact@^10.27.1", 24 24 "preact-render-to-string": "npm:preact-render-to-string@^6.5.13",
+4 -4
frontend/deno.lock
··· 1 1 { 2 2 "version": "5", 3 3 "specifiers": { 4 - "jsr:@slices/oauth@~0.1.1": "0.1.1", 4 + "jsr:@slices/oauth@0.3": "0.3.0", 5 5 "jsr:@std/assert@^1.0.14": "1.0.14", 6 6 "jsr:@std/cli@^1.0.21": "1.0.21", 7 7 "jsr:@std/encoding@^1.0.10": "1.0.10", ··· 19 19 "npm:typed-htmx@~0.3.1": "0.3.1" 20 20 }, 21 21 "jsr": { 22 - "@slices/oauth@0.1.1": { 23 - "integrity": "86911a01f6637e979b03030aeac337fe3084b20e5efd60ac18df14cbcd1ad8da" 22 + "@slices/oauth@0.3.0": { 23 + "integrity": "a6f3296e701291f14b4c8491a7f7a86bd3c8d5caf006eb1d371627558439e3b5" 24 24 }, 25 25 "@std/assert@1.0.14": { 26 26 "integrity": "68d0d4a43b365abc927f45a9b85c639ea18a9fab96ad92281e493e4ed84abaa4", ··· 98 98 }, 99 99 "workspace": { 100 100 "dependencies": [ 101 - "jsr:@slices/oauth@~0.1.1", 101 + "jsr:@slices/oauth@0.3", 102 102 "jsr:@std/assert@^1.0.14", 103 103 "jsr:@std/http@^1.0.20", 104 104 "npm:preact-render-to-string@^6.5.13",
+7
frontend/src/client.ts
··· 273 273 httpMethod !== "GET" 274 274 ) { 275 275 try { 276 + console.log("🔄 Got 401, invalidating token and attempting refresh..."); 277 + // Mark current token as invalid to force refresh 278 + this.oauthClient.invalidateCurrentToken(); 276 279 // Force token refresh by calling ensureValidToken again 277 280 await this.oauthClient.ensureValidToken(); 281 + console.log("✅ Token refresh successful, retrying request..."); 278 282 // Retry the request once with refreshed tokens 279 283 return this.makeRequestWithRetry(endpoint, method, params, true); 280 284 } catch (_refreshError) { 285 + console.error("❌ Token refresh failed:", _refreshError); 281 286 throw new Error( 282 287 `Authentication required: OAuth tokens are invalid or expired. Please log in again.` 283 288 ); ··· 634 639 // Handle 401 Unauthorized - attempt token refresh and retry once 635 640 if (response.status === 401 && !isRetry && this.oauthClient) { 636 641 try { 642 + // Mark current token as invalid to force refresh 643 + this.oauthClient.invalidateCurrentToken(); 637 644 // Force token refresh by calling ensureValidToken again 638 645 await this.oauthClient.ensureValidToken(); 639 646 // Retry the request once with refreshed tokens
+3 -2
frontend/src/main.ts
··· 12 12 { 13 13 port: parseInt(Deno.env.get("PORT") || "8080"), 14 14 hostname: "0.0.0.0", 15 - onListen: ({ port, hostname }) => console.log(`Frontend server running on http://${hostname}:${port}`), 15 + onListen: ({ port, hostname }) => 16 + console.log(`Frontend server running on http://${hostname}:${port}`), 16 17 }, 17 18 handler 18 - ); 19 + );
+12
frontend/src/routes/middleware.ts
··· 27 27 } 28 28 return null; 29 29 } 30 + 31 + export function requireApiAuth( 32 + context: RouteContext 33 + ): Response | null { 34 + if (!context.currentUser.isAuthenticated) { 35 + return new Response(JSON.stringify({ error: "Authentication required" }), { 36 + status: 401, 37 + headers: { "Content-Type": "application/json" } 38 + }); 39 + } 40 + return null; 41 + }
+31 -20
frontend/src/routes/slices.tsx
··· 1 1 import type { Route } from "@std/http/unstable-route"; 2 2 import { render } from "preact-render-to-string"; 3 - import { withAuth, requireAuth } from "./middleware.ts"; 3 + import { withAuth, requireAuth, requireApiAuth } from "./middleware.ts"; 4 4 import { atprotoClient } from "../config.ts"; 5 5 import { getSliceClient } from "../utils/client.ts"; 6 + import { buildSliceUri } from "../utils/at-uri.ts"; 6 7 import { CreateSliceDialog } from "../components/CreateSliceDialog.tsx"; 7 8 import { UpdateResult } from "../components/UpdateResult.tsx"; 8 9 import { EmptyLexiconState } from "../components/EmptyLexiconState.tsx"; ··· 90 91 params?: URLPatternResult 91 92 ): Promise<Response> { 92 93 const context = await withAuth(req); 93 - const authResponse = requireAuth(context, req); 94 + const authResponse = requireApiAuth(context); 94 95 if (authResponse) return authResponse; 95 96 96 97 const sliceId = params?.pathname.groups.id; ··· 113 114 } 114 115 115 116 // Construct the URI for this slice 116 - const sliceUri = `at://${context.currentUser.sub}/social.slices.slice/${sliceId}`; 117 + const sliceUri = buildSliceUri(context.currentUser.sub!, sliceId); 117 118 118 119 // Get the current record first 119 120 const currentRecord = await atprotoClient.social.slices.slice.getRecord({ ··· 161 162 params?: URLPatternResult 162 163 ): Promise<Response> { 163 164 const context = await withAuth(req); 164 - const authResponse = requireAuth(context, req); 165 + const authResponse = requireApiAuth(context); 165 166 if (authResponse) return authResponse; 166 167 167 168 const sliceId = params?.pathname.groups.id; ··· 185 186 } 186 187 } 187 188 188 - async function handleListLexicons(req: Request, params?: URLPatternResult): Promise<Response> { 189 + async function handleListLexicons( 190 + req: Request, 191 + params?: URLPatternResult 192 + ): Promise<Response> { 189 193 const context = await withAuth(req); 190 - const authResponse = requireAuth(context, req); 194 + const authResponse = requireApiAuth(context); 191 195 if (authResponse) return authResponse; 192 196 193 197 const sliceId = params?.pathname.groups.id; ··· 198 202 try { 199 203 // Get slice-specific client and fetch lexicons 200 204 const sliceClient = getSliceClient(context, sliceId); 201 - const lexiconRecords = await sliceClient.social.slices.lexicon.listRecords(); 205 + const lexiconRecords = 206 + await sliceClient.social.slices.lexicon.listRecords(); 202 207 203 208 if (lexiconRecords.records.length === 0) { 204 209 const html = render(<EmptyLexiconState />); ··· 242 247 243 248 async function handleCreateLexicon(req: Request): Promise<Response> { 244 249 const context = await withAuth(req); 245 - const authResponse = requireAuth(context, req); 250 + const authResponse = requireApiAuth(context); 246 251 if (authResponse) return authResponse; 247 252 248 253 try { ··· 291 296 sliceId = pathParts[3]; 292 297 } 293 298 294 - const sliceUri = `at://${context.currentUser.sub}/social.slices.slice/${sliceId}`; 299 + const sliceUri = buildSliceUri(context.currentUser.sub!, sliceId); 295 300 296 301 const lexiconRecord = { 297 302 nsid: lexiconData.id, ··· 305 310 ); 306 311 307 312 const html = render( 308 - <LexiconSuccessMessage nsid={lexiconRecord.nsid} uri={result.uri} sliceId={sliceId} /> 313 + <LexiconSuccessMessage 314 + nsid={lexiconRecord.nsid} 315 + uri={result.uri} 316 + sliceId={sliceId} 317 + /> 309 318 ); 310 319 return new Response(html, { 311 320 status: 200, ··· 338 347 params?: URLPatternResult 339 348 ): Promise<Response> { 340 349 const context = await withAuth(req); 341 - const authResponse = requireAuth(context, req); 350 + const authResponse = requireApiAuth(context); 342 351 if (authResponse) return authResponse; 343 352 344 353 const sliceId = params?.pathname.groups.id; ··· 353 362 354 363 // Check if there are any remaining lexicons using slice-specific client 355 364 const sliceClient = getSliceClient(context, sliceId); 356 - const remainingLexicons = await sliceClient.social.slices.lexicon.listRecords(); 365 + const remainingLexicons = 366 + await sliceClient.social.slices.lexicon.listRecords(); 357 367 358 368 if (remainingLexicons.records.length === 0) { 359 369 // If no lexicons remain, return the empty state and target the parent list ··· 392 402 params?: URLPatternResult 393 403 ): Promise<Response> { 394 404 const context = await withAuth(req); 395 - const authResponse = requireAuth(context, req); 405 + const authResponse = requireApiAuth(context); 396 406 if (authResponse) return authResponse; 397 407 398 408 const sliceId = params?.pathname.groups.id; ··· 454 464 455 465 async function handleUpdateProfile(req: Request): Promise<Response> { 456 466 const context = await withAuth(req); 457 - const authResponse = requireAuth(context, req); 467 + const authResponse = requireApiAuth(context); 458 468 if (authResponse) return authResponse; 459 469 460 470 try { ··· 523 533 <SettingsResult 524 534 type="success" 525 535 message="Profile updated successfully!" 526 - showRefresh={true} 536 + showRefresh 527 537 /> 528 538 ); 529 539 return new Response(html, { ··· 532 542 }); 533 543 } catch (profileError) { 534 544 console.error("Profile update error:", profileError); 535 - const errorMessage = profileError instanceof Error 536 - ? profileError.message 537 - : String(profileError); 538 - 545 + const errorMessage = 546 + profileError instanceof Error 547 + ? profileError.message 548 + : String(profileError); 549 + 539 550 const html = render( 540 551 <SettingsResult 541 552 type="error" ··· 564 575 params?: URLPatternResult 565 576 ): Promise<Response> { 566 577 const context = await withAuth(req); 567 - const authResponse = requireAuth(context, req); 578 + const authResponse = requireApiAuth(context); 568 579 if (authResponse) return authResponse; 569 580 570 581 const sliceId = params?.pathname.groups.id;
+7
frontend/src/utils/at-uri.ts
··· 74 74 */ 75 75 export function getCollectionFromUri(uri: string): string { 76 76 return parseAtUri(uri).collection; 77 + } 78 + 79 + /** 80 + * Build a slice URI from DID and slice ID 81 + */ 82 + export function buildSliceUri(did: string, sliceId: string): string { 83 + return buildAtUri({ did, collection: "social.slices.slice", rkey: sliceId }); 77 84 }