[READ-ONLY] a fast, modern browser for the npm registry
0
fork

Configure Feed

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

fix: preserve page context via cookie after Bluesky redirect (#930)

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: Daniel Roe <daniel@roe.dev>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>

+39 -6
+3 -3
app/components/Header/AuthModal.client.vue
··· 3 3 import { authRedirect } from '~/utils/atproto/helpers' 4 4 5 5 const handleInput = shallowRef('') 6 - 6 + const route = useRoute() 7 7 const { user, logout } = useAtproto() 8 8 9 9 async function handleBlueskySignIn() { 10 - await authRedirect('https://bsky.social') 10 + await authRedirect('https://bsky.social', { redirectTo: route.fullPath }) 11 11 } 12 12 13 13 async function handleCreateAccount() { 14 - await authRedirect('https://npmx.social', true) 14 + await authRedirect('https://npmx.social', { create: true, redirectTo: route.fullPath }) 15 15 } 16 16 17 17 async function handleLogin() {
+10 -2
app/utils/atproto/helpers.ts
··· 1 1 import type { FetchError } from 'ofetch' 2 2 import type { LocationQueryRaw } from 'vue-router' 3 3 4 + interface AuthRedirectOptions { 5 + create?: boolean 6 + redirectTo?: string 7 + } 8 + 4 9 /** 5 10 * Redirect user to ATProto authentication 6 11 */ 7 - export async function authRedirect(identifier: string, create: boolean = false) { 12 + export async function authRedirect(identifier: string, options: AuthRedirectOptions = {}) { 8 13 let query: LocationQueryRaw = { handle: identifier } 9 - if (create) { 14 + if (options.create) { 10 15 query = { ...query, create: 'true' } 16 + } 17 + if (options.redirectTo) { 18 + query = { ...query, returnTo: options.redirectTo } 11 19 } 12 20 await navigateTo( 13 21 {
+26 -1
server/api/auth/atproto.get.ts
··· 10 10 import { Client } from '@atproto/lex' 11 11 import * as app from '#shared/types/lexicons/app' 12 12 import { ensureValidAtIdentifier } from '@atproto/syntax' 13 + // @ts-expect-error virtual file from oauth module 14 + import { clientUri } from '#oauth/config' 13 15 14 16 /** 15 17 * Fetch the user's profile record to get their avatar blob reference ··· 66 68 }) 67 69 68 70 if (!query.code) { 71 + // Validate returnTo is a safe relative path (prevent open redirect) 72 + // Only set cookie on initial auth request, not the callback 73 + let redirectPath = '/' 74 + try { 75 + const clientOrigin = new URL(clientUri).origin 76 + const returnToUrl = new URL(query.returnTo?.toString() || '/', clientUri) 77 + if (returnToUrl.origin === clientOrigin) { 78 + redirectPath = returnToUrl.pathname + returnToUrl.search + returnToUrl.hash 79 + } 80 + } catch { 81 + // Invalid URL, fall back to root 82 + } 83 + 84 + setCookie(event, 'auth_return_to', redirectPath, { 85 + maxAge: 60 * 5, 86 + httpOnly: true, 87 + // secure only if NOT in dev mode 88 + secure: !import.meta.dev, 89 + }) 69 90 try { 70 91 const handle = query.handle?.toString() 71 92 const create = query.create?.toString() ··· 126 147 }, 127 148 }) 128 149 } 129 - return sendRedirect(event, '/') 150 + 151 + const returnToURL = getCookie(event, 'auth_return_to') || '/' 152 + deleteCookie(event, 'auth_return_to') 153 + 154 + return sendRedirect(event, returnToURL) 130 155 })