Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion src/app/api/cloud/auth/route.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
import { NextResponse } from "next/server";
import { validateApiKey, getProviderConnections, getModelAliases } from "@/models";
import { requireAuth, unauthorizedResponse } from "@/lib/apiAuth.js";

// Verify API key and return provider credentials
export async function POST(request) {
// Require authentication (validates API key)
const auth = await requireAuth(request);
if (!auth.authenticated) {
return unauthorizedResponse("Invalid API key");
}
try {
const authHeader = request.headers.get("Authorization");
if (!authHeader?.startsWith("Bearer ")) {
Expand All @@ -11,7 +17,7 @@ export async function POST(request) {

const apiKey = authHeader.slice(7);

// Validate API key
// API key already validated by requireAuth, get connections
const isValid = await validateApiKey(apiKey);
if (!isValid) {
return NextResponse.json({ error: "Invalid API key" }, { status: 401 });
Expand Down
8 changes: 7 additions & 1 deletion src/app/api/cloud/credentials/update/route.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
import { NextResponse } from "next/server";
import { validateApiKey, getProviderConnections, updateProviderConnection } from "@/models";
import { requireAuth, unauthorizedResponse } from "@/lib/apiAuth.js";

// Update provider credentials (for cloud token refresh)
export async function PUT(request) {
// Require authentication (validates API key)
const auth = await requireAuth(request);
if (!auth.authenticated) {
return unauthorizedResponse("Invalid API key");
}
try {
const authHeader = request.headers.get("Authorization");
if (!authHeader?.startsWith("Bearer ")) {
Expand All @@ -17,7 +23,7 @@ export async function PUT(request) {
return NextResponse.json({ error: "Provider and credentials required" }, { status: 400 });
}

// Validate API key
// API key already validated by requireAuth
const isValid = await validateApiKey(apiKey);
if (!isValid) {
return NextResponse.json({ error: "Invalid API key" }, { status: 401 });
Expand Down
20 changes: 19 additions & 1 deletion src/app/api/keys/[id]/route.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,23 @@
import { NextResponse } from "next/server";
import { deleteApiKey, getApiKeyById, updateApiKey } from "@/lib/localDb";
import { requireAuth, unauthorizedResponse } from "@/lib/apiAuth.js";
import { sanitizeApiKeyData } from "@/lib/sanitize.js";

// GET /api/keys/[id] - Get single key
export async function GET(request, { params }) {
// Require authentication
const auth = await requireAuth(request);
if (!auth.authenticated) {
return unauthorizedResponse();
}
try {
const { id } = await params;
const key = await getApiKeyById(id);
if (!key) {
return NextResponse.json({ error: "Key not found" }, { status: 404 });
}
return NextResponse.json({ key });
const sanitized = sanitizeApiKeyData(key);
return NextResponse.json({ key: sanitized });
} catch (error) {
console.log("Error fetching key:", error);
return NextResponse.json({ error: "Failed to fetch key" }, { status: 500 });
Expand All @@ -18,6 +26,11 @@ export async function GET(request, { params }) {

// PUT /api/keys/[id] - Update key
export async function PUT(request, { params }) {
// Require authentication
const auth = await requireAuth(request);
if (!auth.authenticated) {
return unauthorizedResponse();
}
try {
const { id } = await params;
const body = await request.json();
Expand All @@ -42,6 +55,11 @@ export async function PUT(request, { params }) {

// DELETE /api/keys/[id] - Delete API key
export async function DELETE(request, { params }) {
// Require authentication
const auth = await requireAuth(request);
if (!auth.authenticated) {
return unauthorizedResponse();
}
try {
const { id } = await params;

Expand Down
19 changes: 17 additions & 2 deletions src/app/api/keys/route.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,23 @@
import { NextResponse } from "next/server";
import { getApiKeys, createApiKey } from "@/lib/localDb";
import { getConsistentMachineId } from "@/shared/utils/machineId";
import { requireAuth, unauthorizedResponse } from "@/lib/apiAuth.js";
import { sanitizeApiKeyData } from "@/lib/sanitize.js";

export const dynamic = "force-dynamic";

// GET /api/keys - List API keys
export async function GET() {
export async function GET(request) {
// Require authentication
const auth = await requireAuth(request);
if (!auth.authenticated) {
return unauthorizedResponse();
}

try {
const keys = await getApiKeys();
return NextResponse.json({ keys });
const sanitized = sanitizeApiKeyData(keys);
return NextResponse.json({ keys: sanitized });
} catch (error) {
console.log("Error fetching keys:", error);
return NextResponse.json({ error: "Failed to fetch keys" }, { status: 500 });
Expand All @@ -17,6 +26,12 @@ export async function GET() {

// POST /api/keys - Create new API key
export async function POST(request) {
// Require authentication
const auth = await requireAuth(request);
if (!auth.authenticated) {
return unauthorizedResponse();
}

try {
const body = await request.json();
const { name } = body;
Expand Down
9 changes: 8 additions & 1 deletion src/app/api/shutdown/route.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import { NextResponse } from "next/server";
import { requireAuth, unauthorizedResponse } from "@/lib/apiAuth.js";

export async function POST(request) {
// Require authentication
const auth = await requireAuth(request);
if (!auth.authenticated) {
return unauthorizedResponse();
}

export async function POST() {
const response = NextResponse.json({ success: true, message: "Shutting down..." });

setTimeout(() => {
Expand Down
8 changes: 6 additions & 2 deletions src/app/api/usage/[connectionId]/route.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
// Ensure proxyFetch is loaded to patch globalThis.fetch
import "open-sse/index.js";

import { getProviderConnectionById, updateProviderConnection } from "@/lib/localDb";
import { requireAuth, unauthorizedResponse } from "@/lib/apiAuth.js";
import { getUsageForProvider } from "open-sse/services/usage.js";
import { getExecutor } from "open-sse/executors/index.js";
/**
Expand Down Expand Up @@ -91,6 +90,11 @@ async function refreshAndUpdateCredentials(connection) {
* GET /api/usage/[connectionId] - Get usage data for a specific connection
*/
export async function GET(request, { params }) {
// Require authentication
const auth = await requireAuth(request);
if (!auth.authenticated) {
return unauthorizedResponse();
}
try {
const { connectionId } = await params;

Expand Down
6 changes: 6 additions & 0 deletions src/app/api/usage/chart/route.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
import { NextResponse } from "next/server";
import { getChartData } from "@/lib/usageDb";
import { requireAuth, unauthorizedResponse } from "@/lib/apiAuth.js";

const VALID_PERIODS = new Set(["24h", "7d", "30d", "60d"]);

export async function GET(request) {
// Require authentication
const auth = await requireAuth(request);
if (!auth.authenticated) {
return unauthorizedResponse();
}
try {
const { searchParams } = new URL(request.url);
const period = searchParams.get("period") || "7d";
Expand Down
13 changes: 11 additions & 2 deletions src/app/api/usage/history/route.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
import { NextResponse } from "next/server";
import { getUsageStats } from "@/lib/usageDb";
import { requireAuth, unauthorizedResponse } from "@/lib/apiAuth.js";
import { sanitizeUsageStats } from "@/lib/sanitize.js";

export async function GET(request) {
// Require authentication
const auth = await requireAuth(request);
if (!auth.authenticated) {
return unauthorizedResponse();
}

export async function GET() {
try {
const stats = await getUsageStats();
return NextResponse.json(stats);
const sanitized = sanitizeUsageStats(stats);
return NextResponse.json(sanitized);
} catch (error) {
console.error("Error fetching usage stats:", error);
return NextResponse.json({ error: "Failed to fetch usage stats" }, { status: 500 });
Expand Down
8 changes: 7 additions & 1 deletion src/app/api/usage/logs/route.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import { NextResponse } from "next/server";
import { requireAuth, unauthorizedResponse } from "@/lib/apiAuth.js";
import { getRecentLogs } from "@/lib/usageDb";

export async function GET() {
export async function GET(request) {
// Require authentication
const auth = await requireAuth(request);
if (!auth.authenticated) {
return unauthorizedResponse();
}
try {
const logs = await getRecentLogs(200);
return NextResponse.json(logs);
Expand Down
8 changes: 7 additions & 1 deletion src/app/api/usage/providers/route.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,18 @@ import { NextResponse } from "next/server";
import { getRequestDetailsDb } from "@/lib/requestDetailsDb";
import { getProviderNodes } from "@/lib/localDb";
import { AI_PROVIDERS, getProviderByAlias } from "@/shared/constants/providers";
import { requireAuth, unauthorizedResponse } from "@/lib/apiAuth.js";

/**
* GET /api/usage/providers
* Returns list of unique providers from request details
*/
export async function GET() {
export async function GET(request) {
// Require authentication
const auth = await requireAuth(request);
if (!auth.authenticated) {
return unauthorizedResponse();
}
try {
const db = await getRequestDetailsDb();

Expand Down
6 changes: 6 additions & 0 deletions src/app/api/usage/request-details/route.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
import { NextResponse } from "next/server";
import { getRequestDetails } from "@/lib/usageDb";
import { requireAuth, unauthorizedResponse } from "@/lib/apiAuth.js";

/**
* GET /api/usage/request-details
* Query parameters: page, pageSize (1-100), provider, model, connectionId, status, startDate, endDate
*/
export async function GET(request) {
// Require authentication
const auth = await requireAuth(request);
if (!auth.authenticated) {
return unauthorizedResponse();
}
try {
const { searchParams } = new URL(request.url);

Expand Down
8 changes: 7 additions & 1 deletion src/app/api/usage/request-logs/route.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import { NextResponse } from "next/server";
import { requireAuth, unauthorizedResponse } from "@/lib/apiAuth.js";
import { getRecentLogs } from "@/lib/usageDb";

export async function GET() {
export async function GET(request) {
// Require authentication
const auth = await requireAuth(request);
if (!auth.authenticated) {
return unauthorizedResponse();
}
try {
const logs = await getRecentLogs(200);
return NextResponse.json(logs);
Expand Down
11 changes: 10 additions & 1 deletion src/app/api/usage/stats/route.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
import { NextResponse } from "next/server";
import { getUsageStats } from "@/lib/usageDb";
import { requireAuth, unauthorizedResponse } from "@/lib/apiAuth.js";
import { sanitizeUsageStats } from "@/lib/sanitize.js";

const VALID_PERIODS = new Set(["24h", "7d", "30d", "60d", "all"]);

export const dynamic = "force-dynamic";

export async function GET(request) {
// Require authentication
const auth = await requireAuth(request);
if (!auth.authenticated) {
return unauthorizedResponse();
}

try {
const { searchParams } = new URL(request.url);
const period = searchParams.get("period") || "7d";
Expand All @@ -15,7 +23,8 @@ export async function GET(request) {
}

const stats = await getUsageStats(period);
return NextResponse.json(stats);
const sanitized = sanitizeUsageStats(stats);
return NextResponse.json(sanitized);
} catch (error) {
console.error("[API] Failed to get usage stats:", error);
return NextResponse.json({ error: "Failed to fetch usage stats" }, { status: 500 });
Expand Down
15 changes: 11 additions & 4 deletions src/app/api/usage/stream/route.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
import { getUsageStats, statsEmitter, getActiveRequests } from "@/lib/usageDb";

import { requireAuth, unauthorizedResponse } from "@/lib/apiAuth.js";
import { sanitizeUsageStats } from "@/lib/sanitize.js";
export const dynamic = "force-dynamic";

export async function GET() {
export async function GET(request) {
// Require authentication
const auth = await requireAuth(request);
if (!auth.authenticated) {
return unauthorizedResponse();
}
const encoder = new TextEncoder();
const state = { closed: false, keepalive: null, send: null, sendPending: null, cachedStats: null };

Expand All @@ -20,8 +26,9 @@ export async function GET() {
}
// Then do full recalc and update cache
const stats = await getUsageStats();
state.cachedStats = stats;
controller.enqueue(encoder.encode(`data: ${JSON.stringify(stats)}\n\n`));
const sanitized = sanitizeUsageStats(stats);
state.cachedStats = sanitized;
controller.enqueue(encoder.encode(`data: ${JSON.stringify(sanitized)}\n\n`));
} catch {
state.closed = true;
statsEmitter.off("update", state.send);
Expand Down
59 changes: 59 additions & 0 deletions src/lib/apiAuth.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { jwtVerify } from "jose";
import { getApiKeys } from "@/lib/localDb.js";

const SECRET = new TextEncoder().encode(
process.env.JWT_SECRET || "9router-default-secret-change-me"
);

/**
* Authentication middleware for API routes
* Supports both JWT cookie (for dashboard users) and Bearer API key
*
* @param {Request} request - Next.js request object
* @returns {Promise<{authenticated: boolean, user?: object, apiKey?: string, error?: string}>}
*/
export async function requireAuth(request) {
// Try JWT cookie first (dashboard users)
const token = request.cookies.get("auth_token")?.value;

if (token) {
try {
const { payload } = await jwtVerify(token, SECRET);
return { authenticated: true, user: payload };
} catch (err) {
// JWT invalid, continue to try API key
}
}

// Try Bearer API key
const authHeader = request.headers.get("authorization");
if (authHeader?.startsWith("Bearer ")) {
const apiKey = authHeader.substring(7);

try {
const apiKeys = await getApiKeys();
const validKey = apiKeys.find(k => k.key === apiKey);

if (validKey) {
return { authenticated: true, apiKey: validKey.key, keyName: validKey.name };
}
} catch (err) {
console.error("[apiAuth] Error validating API key:", err);
}
}

return { authenticated: false, error: "Authentication required" };
}

/**
* Helper to return 401 Unauthorized response
*/
export function unauthorizedResponse(message = "Authentication required") {
return new Response(
JSON.stringify({ error: message }),
{
status: 401,
headers: { "Content-Type": "application/json" }
}
);
}
Loading