diff --git a/middleware.ts b/middleware.ts index 1b551c0..af6ab56 100644 --- a/middleware.ts +++ b/middleware.ts @@ -83,12 +83,6 @@ export function middleware(request: NextRequest) { return NextResponse.next() } - // Backward compat: accept legacy cookie during migration - const legacyCookie = request.cookies.get('mission-control-auth') - if (safeCompare(legacyCookie?.value || '', process.env.AUTH_SECRET || '')) { - return NextResponse.next() - } - return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) } @@ -97,12 +91,6 @@ export function middleware(request: NextRequest) { return NextResponse.next() } - // Backward compat: accept legacy cookie - const legacyCookie = request.cookies.get('mission-control-auth') - if (safeCompare(legacyCookie?.value || '', process.env.AUTH_SECRET || '')) { - return NextResponse.next() - } - // Redirect to login const loginUrl = request.nextUrl.clone() loginUrl.pathname = '/login' diff --git a/src/app/api/auth/login/route.ts b/src/app/api/auth/login/route.ts index f55ff35..45d656d 100644 --- a/src/app/api/auth/login/route.ts +++ b/src/app/api/auth/login/route.ts @@ -3,8 +3,27 @@ import { authenticateUser, createSession } from '@/lib/auth' import { logAuditEvent } from '@/lib/db' import { getMcSessionCookieOptions } from '@/lib/session-cookie' +// Rate limiting: 5 attempts per minute per IP +const loginAttempts = new Map() + +function checkRateLimit(ip: string): boolean { + const now = Date.now() + const entry = loginAttempts.get(ip) + if (!entry || now > entry.resetAt) { + loginAttempts.set(ip, { count: 1, resetAt: now + 60_000 }) + return true + } + entry.count++ + return entry.count <= 5 +} + export async function POST(request: Request) { try { + const ip = request.headers.get('x-forwarded-for')?.split(',')[0]?.trim() || 'unknown' + if (!checkRateLimit(ip)) { + return NextResponse.json({ error: 'Too many login attempts. Try again in a minute.' }, { status: 429 }) + } + const { username, password } = await request.json() if (!username || !password) { diff --git a/src/app/api/gateways/health/route.ts b/src/app/api/gateways/health/route.ts index 82c7876..fe3c01c 100644 --- a/src/app/api/gateways/health/route.ts +++ b/src/app/api/gateways/health/route.ts @@ -22,6 +22,20 @@ interface HealthResult { error?: string } +function isBlockedUrl(urlStr: string): boolean { + try { + const url = new URL(urlStr) + const hostname = url.hostname + // Block link-local / cloud metadata endpoints + if (hostname.startsWith('169.254.')) return true + // Block well-known cloud metadata hostnames + if (hostname === 'metadata.google.internal') return true + return false + } catch { + return true // Block malformed URLs + } +} + /** * POST /api/gateways/health - Server-side health probe for all gateways * Probes gateways from the server where loopback addresses are reachable. @@ -33,53 +47,57 @@ export async function POST(request: NextRequest) { const db = getDatabase() const gateways = db.prepare("SELECT * FROM gateways ORDER BY is_primary DESC, name ASC").all() as GatewayEntry[] - const results: HealthResult[] = await Promise.all( - gateways.map(async (gw) => { - const start = Date.now() - try { - const controller = new AbortController() - const timeout = setTimeout(() => controller.abort(), 5000) + const results: HealthResult[] = [] - const probeUrl = "http://" + gw.host + ":" + gw.port + "/" - const res = await fetch(probeUrl, { - signal: controller.signal, - }) - clearTimeout(timeout) + for (const gw of gateways) { + const probeUrl = "http://" + gw.host + ":" + gw.port + "/" - const latency = Date.now() - start - const status = res.ok ? "online" : "error" + if (isBlockedUrl(probeUrl)) { + results.push({ id: gw.id, name: gw.name, status: 'error', latency: null, agents: [], sessions_count: 0, error: 'Blocked URL' }) + continue + } - db.prepare( - "UPDATE gateways SET status = ?, latency = ?, last_seen = (unixepoch()), updated_at = (unixepoch()) WHERE id = ?" - ).run(status, latency, gw.id) + const start = Date.now() + try { + const controller = new AbortController() + const timeout = setTimeout(() => controller.abort(), 5000) - return { - id: gw.id, - name: gw.name, - status: status as "online" | "error", - latency, - agents: [], - sessions_count: 0, - } - } catch (err: any) { - const latency = Date.now() - start + const res = await fetch(probeUrl, { + signal: controller.signal, + }) + clearTimeout(timeout) - db.prepare( - "UPDATE gateways SET status = ?, latency = NULL, updated_at = (unixepoch()) WHERE id = ?" - ).run("offline", gw.id) + const latency = Date.now() - start + const status = res.ok ? "online" : "error" - return { - id: gw.id, - name: gw.name, - status: "offline" as const, - latency: null, - agents: [], - sessions_count: 0, - error: err.name === "AbortError" ? "timeout" : (err.message || "connection failed"), - } - } - }) - ) + db.prepare( + "UPDATE gateways SET status = ?, latency = ?, last_seen = (unixepoch()), updated_at = (unixepoch()) WHERE id = ?" + ).run(status, latency, gw.id) + + results.push({ + id: gw.id, + name: gw.name, + status: status as "online" | "error", + latency, + agents: [], + sessions_count: 0, + }) + } catch (err: any) { + db.prepare( + "UPDATE gateways SET status = ?, latency = NULL, updated_at = (unixepoch()) WHERE id = ?" + ).run("offline", gw.id) + + results.push({ + id: gw.id, + name: gw.name, + status: "offline" as const, + latency: null, + agents: [], + sessions_count: 0, + error: err.name === "AbortError" ? "timeout" : (err.message || "connection failed"), + }) + } + } return NextResponse.json({ results, probed_at: Date.now() }) } diff --git a/src/lib/migrations.ts b/src/lib/migrations.ts index 51f2c66..4c0c2d0 100644 --- a/src/lib/migrations.ts +++ b/src/lib/migrations.ts @@ -348,14 +348,14 @@ const migrations: Migration[] = [ String(process.env.MC_DEFAULT_OWNER_GATEWAY || process.env.MC_DEFAULT_GATEWAY_NAME || 'primary').trim() || 'primary' - db.exec(` + db.prepare(` UPDATE tenants SET owner_gateway = COALESCE( (SELECT name FROM gateways ORDER BY is_primary DESC, id ASC LIMIT 1), - '${defaultGatewayName.replace(/'/g, "''")}' + ? ) WHERE owner_gateway IS NULL OR trim(owner_gateway) = '' - `) + `).run(defaultGatewayName) db.exec(`CREATE INDEX IF NOT EXISTS idx_tenants_owner_gateway ON tenants(owner_gateway)`) }