See the best posts from any Bluesky account
0
fork

Configure Feed

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

Fix OAuth callback, extract auth buttons partial, dev tunnel support

- Fix callback param parsing (use request.qs() instead of completeUrl())
- Resolve handle via identity resolver instead of authenticated getProfile
(we don't have getProfile scope)
- Extract auth buttons into a partial with auth.check() call
- Add auth buttons to landing page header
- Allow all hosts in Vite dev server for tunnel usage
- Make APP_URL configurable in docker-compose.yml

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

+38 -33
+2 -3
app/controllers/oauth_controller.ts
··· 42 42 43 43 try { 44 44 const authUrl = await this.oauthService.authorize(handle) 45 - return response.redirect(authUrl) 45 + return response.redirect().withQs(false).toPath(authUrl) 46 46 } catch (error) { 47 47 logger.error({ error, handle }, 'OAuth authorize failed') 48 48 session.flash('error', 'Could not start sign-in. Check the handle and try again.') ··· 56 56 */ 57 57 async callback({ request, response, session, auth }: HttpContext) { 58 58 try { 59 - const url = new URL(request.completeUrl()) 60 - const params = url.searchParams 59 + const params = new URLSearchParams(request.qs() as Record<string, string>) 61 60 62 61 const { did, handle } = await this.oauthService.callback(params) 63 62
+4 -4
app/services/atproto_oauth.ts
··· 131 131 const { session } = await this.client.callback(params) 132 132 const did = session.sub 133 133 134 - // Resolve handle from the session's DID 135 - const agent = new Agent(session) 136 - const profile = await agent.getProfile({ actor: did }) 137 - const handle = profile.data.handle 134 + // Resolve handle from DID using the identity resolver (no auth needed) 135 + const identityResolver = this.client.identityResolver 136 + const resolved = await identityResolver.resolve(did) 137 + const handle = resolved.handle ?? did 138 138 139 139 // Update the account's handle 140 140 const account = await Account.find(did)
+1 -1
docker-compose.yml
··· 7 7 CLICKHOUSE_USER: favs 8 8 CLICKHOUSE_PASSWORD: ${CLICKHOUSE_PASSWORD} 9 9 APP_KEY: ${APP_KEY} 10 - APP_URL: http://localhost:3333 10 + APP_URL: ${APP_URL:-http://localhost:3333} 11 11 LOG_LEVEL: info 12 12 SESSION_DRIVER: cookie 13 13 QUEUE_DRIVER: database
+1 -15
resources/views/components/layout.edge
··· 49 49 <i x-show="!dark" class="ph-fill ph-moon text-base"></i> 50 50 <i x-show="dark" class="ph-fill ph-sun text-base"></i> 51 51 </button> 52 - @if(auth.isAuthenticated) 53 - <a 54 - href="/profile/{{ auth.user.handle }}/likes" 55 - class="text-sm text-gray-600 dark:text-gray-300 hover:text-blue-600 dark:hover:text-blue-400 transition-colors duration-200" 56 - >My profile</a> 57 - <form action="/oauth/logout" method="POST" class="inline"> 58 - {{ csrfField() }} 59 - <button type="submit" class="text-sm text-gray-600 dark:text-gray-300 hover:text-blue-600 dark:hover:text-blue-400 transition-colors duration-200 cursor-pointer">Sign out</button> 60 - </form> 61 - @else 62 - <a 63 - href="/oauth/login" 64 - class="text-sm text-gray-600 dark:text-gray-300 hover:text-blue-600 dark:hover:text-blue-400 transition-colors duration-200" 65 - >Sign in</a> 66 - @endif 52 + @include('partials/auth_buttons') 67 53 </div> 68 54 </div> 69 55 @endif
+13 -10
resources/views/pages/landing.edge
··· 6 6 <i class="ph-fill ph-heart text-red-500 inline-block animate-[heart-pulse_2s_var(--ease-out-quart)_1.2s_both]"></i> 7 7 favs.blue 8 8 </h1> 9 - <button 10 - x-data="darkMode" 11 - x-on:click="toggle" 12 - class="p-1.5 rounded-md text-gray-500 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-800 cursor-pointer transition-colors duration-200" 13 - aria-label="Toggle dark mode" 14 - > 15 - <i x-show="!dark" class="ph-fill ph-moon text-base"></i> 16 - <i x-show="dark" class="ph-fill ph-sun text-base"></i> 17 - </button> 9 + <div class="flex items-center gap-2"> 10 + <button 11 + x-data="darkMode" 12 + x-on:click="toggle" 13 + class="p-1.5 rounded-md text-gray-500 dark:text-gray-400 hover:bg-gray-200 dark:hover:bg-gray-800 cursor-pointer transition-colors duration-200" 14 + aria-label="Toggle dark mode" 15 + > 16 + <i x-show="!dark" class="ph-fill ph-moon text-base"></i> 17 + <i x-show="dark" class="ph-fill ph-sun text-base"></i> 18 + </button> 19 + @include('partials/auth_buttons') 20 + </div> 18 21 </div> 19 22 <p class="font-heading text-lg text-gray-600 dark:text-gray-400 mb-10 animate-[fade-in-up_0.5s_var(--ease-out-quart)_0.08s_both]"> 20 23 See any Bluesky account's most popular posts. ··· 40 43 favs.blue indexes Bluesky posts and tracks engagement from the 41 44 <a href="https://atproto.com" target="_blank" rel="noopener" class="text-blue-600 dark:text-blue-400 hover:underline">AT Protocol</a> firehose. 42 45 Type a handle to see that account's greatest hits. 43 - No login required. 46 + Sign in with Bluesky to like and repost from here. 44 47 </p> 45 48 </div> 46 49 @endslot
+16
resources/views/partials/auth_buttons.edge
··· 1 + @eval(await auth.check()) 2 + @if(auth.isAuthenticated) 3 + <a 4 + href="/profile/{{ auth.user.handle }}/likes" 5 + class="text-sm text-gray-600 dark:text-gray-300 hover:text-blue-600 dark:hover:text-blue-400 transition-colors duration-200" 6 + >My profile</a> 7 + <form action="/oauth/logout" method="POST" class="inline"> 8 + {{ csrfField() }} 9 + <button type="submit" class="text-sm text-gray-600 dark:text-gray-300 hover:text-blue-600 dark:hover:text-blue-400 transition-colors duration-200 cursor-pointer">Sign out</button> 10 + </form> 11 + @else 12 + <a 13 + href="/oauth/login" 14 + class="text-sm text-gray-600 dark:text-gray-300 hover:text-blue-600 dark:hover:text-blue-400 transition-colors duration-200" 15 + >Sign in</a> 16 + @endif
+1
vite.config.ts
··· 20 20 ], 21 21 22 22 server: { 23 + allowedHosts: true, 23 24 watch: { 24 25 ignored: ['**/storage/**', '**/tmp/**'], 25 26 },