diff --git a/app/api/auth/session/route.ts b/app/api/auth/session/route.ts index 0e054004..a21f526f 100644 --- a/app/api/auth/session/route.ts +++ b/app/api/auth/session/route.ts @@ -13,6 +13,7 @@ import { configManager } from '@/lib/admin/config-manager'; import { isPublicHttpUrl } from '@/lib/security/url-guard'; import { recordLogin } from '@/lib/telemetry/login-tracker'; import { parseJmapServers, resolveTrustedJmapUrl } from '@/lib/admin/jmap-servers'; +import { MAX_ACCOUNT_SLOTS } from '@/lib/account-utils'; const COOKIE_OPTIONS = { ...getCookieOptions(), @@ -23,7 +24,7 @@ function getSlot(request: NextRequest): number { const raw = request.nextUrl.searchParams.get('slot'); if (raw === null) return 0; const slot = parseInt(raw, 10); - if (isNaN(slot) || slot < 0 || slot > 4) return 0; + if (isNaN(slot) || slot < 0 || slot >= MAX_ACCOUNT_SLOTS) return 0; return slot; } @@ -70,7 +71,7 @@ export async function POST(request: NextRequest) { return NextResponse.json({ error: 'JMAP server not configured' }, { status: 500 }); } - const slot = typeof bodySlot === 'number' && bodySlot >= 0 && bodySlot <= 4 ? bodySlot : getSlot(request); + const slot = typeof bodySlot === 'number' && bodySlot >= 0 && bodySlot < MAX_ACCOUNT_SLOTS ? bodySlot : getSlot(request); const cookieName = sessionCookieName(slot); const authHeader = `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`; const normalizedServerUrl = await verifyJmapAuth(upstreamUrl, authHeader, { trusted: upstreamTrusted }); @@ -185,8 +186,8 @@ export async function DELETE(request: NextRequest) { const all = request.nextUrl.searchParams.get('all') === 'true'; if (all) { - // Delete all session cookies (slots 0-4) - for (let i = 0; i <= 4; i++) { + // Delete all session cookies across every slot. + for (let i = 0; i < MAX_ACCOUNT_SLOTS; i++) { cookieStore.delete(sessionCookieName(i)); clearStalwartAuthContextInStore(cookieStore, i); } diff --git a/app/api/auth/stalwart-context/route.ts b/app/api/auth/stalwart-context/route.ts index 2edb1630..7894a255 100644 --- a/app/api/auth/stalwart-context/route.ts +++ b/app/api/auth/stalwart-context/route.ts @@ -6,9 +6,10 @@ import { configManager } from '@/lib/admin/config-manager'; import { isPublicHttpUrl } from '@/lib/security/url-guard'; import { recordLogin } from '@/lib/telemetry/login-tracker'; import { parseJmapServers, resolveTrustedJmapUrl } from '@/lib/admin/jmap-servers'; +import { MAX_ACCOUNT_SLOTS } from '@/lib/account-utils'; function getSlot(request: NextRequest, bodySlot: unknown): number { - if (typeof bodySlot === 'number' && bodySlot >= 0 && bodySlot <= 4) { + if (typeof bodySlot === 'number' && bodySlot >= 0 && bodySlot < MAX_ACCOUNT_SLOTS) { return bodySlot; } @@ -16,7 +17,7 @@ function getSlot(request: NextRequest, bodySlot: unknown): number { if (raw === null) return 0; const slot = parseInt(raw, 10); - return Number.isNaN(slot) || slot < 0 || slot > 4 ? 0 : slot; + return Number.isNaN(slot) || slot < 0 || slot >= MAX_ACCOUNT_SLOTS ? 0 : slot; } export async function POST(request: NextRequest) { diff --git a/app/api/auth/token/route.ts b/app/api/auth/token/route.ts index 1fa3377b..e734ba71 100644 --- a/app/api/auth/token/route.ts +++ b/app/api/auth/token/route.ts @@ -4,12 +4,13 @@ import { logger } from '@/lib/logger'; import { refreshTokenCookieName, refreshTokenServerCookieName } from '@/lib/oauth/tokens'; import { exchangeCodeForTokens, buildOAuthParams, getMetadata, getTokenEndpoint } from '@/lib/oauth/token-exchange'; import { getCookieOptions } from '@/lib/oauth/cookie-config'; +import { MAX_ACCOUNT_SLOTS } from '@/lib/account-utils'; function getSlot(request: NextRequest): number { const raw = request.nextUrl.searchParams.get('slot'); if (raw === null) return 0; const slot = parseInt(raw, 10); - if (isNaN(slot) || slot < 0 || slot > 4) return 0; + if (isNaN(slot) || slot < 0 || slot >= MAX_ACCOUNT_SLOTS) return 0; return slot; } @@ -21,7 +22,7 @@ export async function POST(request: NextRequest) { return NextResponse.json({ error: 'Missing required parameters' }, { status: 400 }); } - const slot = typeof bodySlot === 'number' && bodySlot >= 0 && bodySlot <= 4 ? bodySlot : getSlot(request); + const slot = typeof bodySlot === 'number' && bodySlot >= 0 && bodySlot < MAX_ACCOUNT_SLOTS ? bodySlot : getSlot(request); const serverId = typeof bodyServerId === 'string' && bodyServerId ? bodyServerId : null; const tokens = await exchangeCodeForTokens(code, code_verifier, redirect_uri, serverId); @@ -112,9 +113,9 @@ export async function DELETE(request: NextRequest) { const all = request.nextUrl.searchParams.get('all') === 'true'; if (all) { - // Revoke and delete all refresh token cookies (slots 0-4) + // Revoke and delete all refresh token cookies across every slot. const cookieStore = await cookies(); - for (let i = 0; i <= 4; i++) { + for (let i = 0; i < MAX_ACCOUNT_SLOTS; i++) { const name = refreshTokenCookieName(i); const serverCookieName = refreshTokenServerCookieName(i); const token = cookieStore.get(name)?.value; diff --git a/app/api/auth/totp-token-exchange/route.ts b/app/api/auth/totp-token-exchange/route.ts index 85f2025d..4c120c40 100644 --- a/app/api/auth/totp-token-exchange/route.ts +++ b/app/api/auth/totp-token-exchange/route.ts @@ -9,6 +9,7 @@ import { configManager } from '@/lib/admin/config-manager'; import { isPublicHttpUrl } from '@/lib/security/url-guard'; import { recordLogin } from '@/lib/telemetry/login-tracker'; import { parseJmapServers, findServerByUrl, findServerById } from '@/lib/admin/jmap-servers'; +import { MAX_ACCOUNT_SLOTS } from '@/lib/account-utils'; /** * Exchange basic auth credentials (with TOTP appended) for OAuth tokens. @@ -85,7 +86,7 @@ export async function POST(request: NextRequest) { return NextResponse.json({ error: 'Missing required parameters' }, { status: 400 }); } - const slot = typeof bodySlot === 'number' && bodySlot >= 0 && bodySlot <= 4 ? bodySlot : 0; + const slot = typeof bodySlot === 'number' && bodySlot >= 0 && bodySlot < MAX_ACCOUNT_SLOTS ? bodySlot : 0; const requestedServerId = typeof bodyServerId === 'string' && bodyServerId ? bodyServerId : null; // Pin the upstream URL to a configured JMAP server. The list of allowed diff --git a/app/api/settings/route.ts b/app/api/settings/route.ts index d03a1780..a6f6da6b 100644 --- a/app/api/settings/route.ts +++ b/app/api/settings/route.ts @@ -7,6 +7,7 @@ import { readStalwartAuthContextFromStore } from '@/lib/stalwart/auth-context'; import { saveUserSettings, loadUserSettings, deleteUserSettings } from '@/lib/settings-sync'; import { configManager } from '@/lib/admin/config-manager'; import { readFileEnv } from '@/lib/read-file-env'; +import { MAX_ACCOUNT_SLOTS } from '@/lib/account-utils'; function classifyError(error: unknown): { message: string; status: number } { const code = (error as NodeJS.ErrnoException).code; @@ -59,7 +60,7 @@ function normalizeUrl(url: string): string { /** * Verify identity against session cookies across all account slots. - * With multi-account, the requesting account may be on any slot (0-4). + * With multi-account, the requesting account may be on any slot. * Checks both basic-auth session cookies and stalwart auth context cookies * (used by OAuth/SSO and TOTP-upgraded sessions). * Returns true only if a matching cookie is found. @@ -68,7 +69,7 @@ async function verifyIdentity(username: string, serverUrl: string): Promise {/* Separator + Add Account */} - {accounts.length < MAX_ACCOUNTS && ( + {accounts.length < getMaxAccounts() && (