diff --git a/app/api/admin/plugins/[id]/config/route.ts b/app/api/admin/plugins/[id]/config/route.ts index 3b06534f..bd6e5ef2 100644 --- a/app/api/admin/plugins/[id]/config/route.ts +++ b/app/api/admin/plugins/[id]/config/route.ts @@ -1,6 +1,7 @@ import { NextRequest, NextResponse } from 'next/server'; import { getPlugin } from '@/lib/admin/plugin-registry'; import { getPluginConfig, setPluginConfig, deletePluginConfigKey } from '@/lib/admin/plugin-config'; +import { requireAdminAuth } from '@/lib/admin/session'; /** * GET /api/admin/plugins/[id]/config — Read all config for a plugin @@ -44,6 +45,9 @@ export async function PUT( { params }: { params: Promise<{ id: string }> }, ) { try { + const result = await requireAdminAuth(); + if ('error' in result) return result.error; + const { id } = await params; if (!/^[a-z0-9][a-z0-9-]*[a-z0-9]$/.test(id)) { @@ -88,6 +92,9 @@ export async function DELETE( { params }: { params: Promise<{ id: string }> }, ) { try { + const result = await requireAdminAuth(); + if ('error' in result) return result.error; + const { id } = await params; if (!/^[a-z0-9][a-z0-9-]*[a-z0-9]$/.test(id)) { diff --git a/app/api/auth/session/route.ts b/app/api/auth/session/route.ts index f1733f50..21635efd 100644 --- a/app/api/auth/session/route.ts +++ b/app/api/auth/session/route.ts @@ -59,6 +59,45 @@ export async function GET(request: NextRequest) { return NextResponse.json({ error: 'Invalid session' }, { status: 401 }); } + // Only return non-sensitive fields. Use PUT to retrieve full credentials. + const { serverUrl, username } = credentials; + return NextResponse.json( + { serverUrl, username }, + { headers: { 'Cache-Control': 'no-store, no-cache, must-revalidate' } }, + ); + } catch (error) { + logger.error('Session read error', { error: error instanceof Error ? error.message : 'Unknown error' }); + return NextResponse.json({ error: 'Internal server error' }, { status: 500 }); + } +} + +/** + * PUT — retrieve full credentials (including password) for session restoration. + * Protected by Sec-Fetch-Site to ensure only same-origin browser requests succeed. + */ +export async function PUT(request: NextRequest) { + try { + // Block non-browser and cross-origin requests + const secFetchSite = request.headers.get('sec-fetch-site'); + if (secFetchSite !== 'same-origin') { + return NextResponse.json({ error: 'Forbidden' }, { status: 403 }); + } + + const slot = getSlot(request); + const cookieName = sessionCookieName(slot); + const cookieStore = await cookies(); + const token = cookieStore.get(cookieName)?.value; + + if (!token) { + return NextResponse.json({ error: 'No session' }, { status: 401 }); + } + + const credentials = decryptSession(token); + if (!credentials) { + cookieStore.delete(cookieName); + return NextResponse.json({ error: 'Invalid session' }, { status: 401 }); + } + return NextResponse.json(credentials, { headers: { 'Cache-Control': 'no-store, no-cache, must-revalidate' }, }); diff --git a/app/api/settings/route.ts b/app/api/settings/route.ts index d0f237e7..3e0c966e 100644 --- a/app/api/settings/route.ts +++ b/app/api/settings/route.ts @@ -53,16 +53,14 @@ function isEnabled(): boolean { /** * Verify identity against session cookies across all account slots. * With multi-account, the requesting account may be on any slot (0-4). - * Returns true if any slot matches OR if no session cookies exist at all. + * Returns true only if a matching session cookie is found. */ async function verifyIdentity(username: string, serverUrl: string): Promise { const cookieStore = await cookies(); - let hasAnyCookie = false; for (let slot = 0; slot <= 4; slot++) { const token = cookieStore.get(sessionCookieName(slot))?.value; if (!token) continue; - hasAnyCookie = true; const session = decryptSession(token); if (session && session.username === username && session.serverUrl === serverUrl) { @@ -70,10 +68,7 @@ async function verifyIdentity(username: string, serverUrl: string): Promise()( scheduleRefresh(expires_in, get().refreshAccessToken, accountId); } } else if (targetAccount.authMode === 'basic' && targetAccount.rememberMe) { - const res = await fetch(`/api/auth/session?slot=${targetAccount.cookieSlot}`); + const res = await fetch(`/api/auth/session?slot=${targetAccount.cookieSlot}`, { method: 'PUT' }); if (res.ok) { const { serverUrl, username, password } = await res.json(); targetClient = new JMAPClient(serverUrl, username, password); @@ -1179,7 +1179,7 @@ export const useAuthStore = create()( throw new Error(`Token refresh failed: ${res.status}`); } } else if (account.authMode === 'basic' && account.rememberMe) { - const res = await fetch(`/api/auth/session?slot=${account.cookieSlot}`); + const res = await fetch(`/api/auth/session?slot=${account.cookieSlot}`, { method: 'PUT' }); if (res.ok) { const { serverUrl, username, password } = await res.json(); const client = new JMAPClient(serverUrl, username, password); @@ -1370,7 +1370,7 @@ export const useAuthStore = create()( if (state.authMode === 'basic') { set({ isLoading: true, isRateLimited: false, rateLimitUntil: null }); try { - const res = await fetch('/api/auth/session'); + const res = await fetch('/api/auth/session', { method: 'PUT' }); if (res.ok) { const data = await res.json(); if (!data.serverUrl || !data.username || !data.password) {