Make Stuffbox linking resilient to cross-origin popup isolation and stale callbacks
Hop-State: A_06FP8WHY25K99094H3X60A8 Hop-Proposal: R_06FP8WGCVDDG61H0VZ1XQJR Hop-Task: T_06FP8V5D2HSJG08NRN6VS3G Hop-Attempt: AT_06FP8V5D2KATGJNV2M2SGRR
This commit is contained in:
@@ -28,6 +28,7 @@ export async function GET() {
|
||||
provider: stuffbox ? 'stuffbox' : user.storageProvider ? 's3' : null,
|
||||
stuffboxAvailable: Boolean(configuredStuffboxUrl()),
|
||||
stuffboxBaseUrl: stuffbox?.baseUrl ?? null,
|
||||
stuffboxUpdatedAt: stuffbox?.updatedAt.toISOString() ?? null,
|
||||
s3Provider: user.storageProvider ?? null,
|
||||
});
|
||||
} catch (error) {
|
||||
|
||||
@@ -5,8 +5,8 @@ import { consumeStuffboxConnectionState } from '@/lib/stuffbox/connection-state'
|
||||
import { saveStuffboxTokens } from '@/lib/stuffbox/tokens';
|
||||
import { renderStuffboxPopupResponse } from '@/lib/stuffbox/popup-response';
|
||||
|
||||
function popupResponse(origin: string, success: boolean, message: string): NextResponse {
|
||||
return new NextResponse(renderStuffboxPopupResponse(origin, success, message), {
|
||||
function popupResponse(origin: string, success: boolean, message: string, attemptId?: string): NextResponse {
|
||||
return new NextResponse(renderStuffboxPopupResponse(origin, success, message, attemptId), {
|
||||
status: success ? 200 : 400,
|
||||
headers: { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store' },
|
||||
});
|
||||
@@ -14,16 +14,18 @@ function popupResponse(origin: string, success: boolean, message: string): NextR
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const origin = request.nextUrl.origin;
|
||||
let attemptId: string | undefined;
|
||||
try {
|
||||
const user = await requireAuth();
|
||||
const pending = await consumeStuffboxConnectionState();
|
||||
attemptId = pending?.state;
|
||||
const code = request.nextUrl.searchParams.get('code');
|
||||
const state = request.nextUrl.searchParams.get('state');
|
||||
const denied = request.nextUrl.searchParams.get('error');
|
||||
|
||||
if (denied) return popupResponse(origin, false, 'Stuffbox access was not approved.');
|
||||
if (denied) return popupResponse(origin, false, 'Stuffbox access was not approved.', attemptId);
|
||||
if (!pending || pending.userId !== user.id || !code || state !== pending.state) {
|
||||
return popupResponse(origin, false, 'The Stuffbox connection request is invalid or expired.');
|
||||
return popupResponse(origin, false, 'The Stuffbox connection request is invalid or expired.', attemptId);
|
||||
}
|
||||
|
||||
const tokens = await exchangeAuthorizationCode(pending.baseUrl, {
|
||||
@@ -32,12 +34,12 @@ export async function GET(request: NextRequest) {
|
||||
redirectUri: pending.redirectUri,
|
||||
});
|
||||
await saveStuffboxTokens(user.id, pending.baseUrl, tokens);
|
||||
return popupResponse(new URL(pending.redirectUri).origin, true, 'Stuffbox connected.');
|
||||
return popupResponse(new URL(pending.redirectUri).origin, true, 'Stuffbox connected.', attemptId);
|
||||
} catch (error) {
|
||||
if (error instanceof StuffboxApiError) {
|
||||
return popupResponse(origin, false, error.message);
|
||||
return popupResponse(origin, false, error.message, attemptId);
|
||||
}
|
||||
console.error('Stuffbox callback error:', error);
|
||||
return popupResponse(origin, false, 'Stuffbox could not be connected.');
|
||||
return popupResponse(origin, false, 'Stuffbox could not be connected.', attemptId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,7 +49,11 @@ export async function POST(request: NextRequest) {
|
||||
expiresAt: Math.min(Date.parse(connection.expiresAt) || Date.now() + 10 * 60_000, Date.now() + 10 * 60_000),
|
||||
});
|
||||
|
||||
return NextResponse.json({ authorizationUrl: connection.authorizationUrl });
|
||||
return NextResponse.json({
|
||||
authorizationUrl: connection.authorizationUrl,
|
||||
connectionStartedAt: new Date().toISOString(),
|
||||
connectionAttempt: pkce.state,
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.message === 'Authentication required') {
|
||||
return NextResponse.json({ error: 'Authentication required' }, { status: 401 });
|
||||
|
||||
Reference in New Issue
Block a user