fix: update the permission for add provider resource

This commit is contained in:
2026-07-11 16:21:12 +07:00
parent 4ac342c5a2
commit 3b354166eb
29 changed files with 396 additions and 89 deletions
@@ -47,7 +47,7 @@ export default function AddApiKeyModal({ isOpen, provider, providerName, isCompa
const [mode, setMode] = useState("single"); // "single" | "bulk"
const [bulkText, setBulkText] = useState("");
const [bulkResult, setBulkResult] = useState(null); // { success, failed }
const [bulkResult, setBulkResult] = useState(null); // { success, failed, duplicateLines }
const buildProviderSpecificData = () => {
if (isOllamaLocal && formData.ollamaHostUrl.trim()) {
@@ -137,6 +137,7 @@ export default function AddApiKeyModal({ isOpen, provider, providerName, isCompa
setBulkResult(null);
let success = 0;
let failed = 0;
const duplicateLines = [];
for (let i = 0; i < lines.length; i++) {
const parts = lines[i].split("|");
const baseName = parts.length >= 2 ? parts[0].trim() : "Key";
@@ -165,14 +166,20 @@ export default function AddApiKeyModal({ isOpen, provider, providerName, isCompa
...(providerSpecificData ? { providerSpecificData } : {}),
}),
});
if (res.ok) success++;
else failed++;
if (res.ok) {
success++;
} else {
failed++;
const data = await res.json().catch(() => null);
if (res.status === 409) duplicateLines.push(i + 1);
if (data?.error) console.log(`Bulk API key add failed on line ${i + 1}:`, data.error);
}
} catch {
failed++;
}
}
setSaving(false);
setBulkResult({ success, failed });
setBulkResult({ success, failed, duplicateLines });
if (success > 0 && onBulkDone) onBulkDone();
};
@@ -203,7 +210,14 @@ export default function AddApiKeyModal({ isOpen, provider, providerName, isCompa
/>
{bulkResult && (
<div className={`text-sm font-medium ${bulkResult.failed > 0 ? "text-yellow-400" : "text-green-400"}`}>
{bulkResult.success} added{bulkResult.failed > 0 ? `, ✗ ${bulkResult.failed} failed` : ""}
<p>
{bulkResult.success} added{bulkResult.failed > 0 ? `, ✗ ${bulkResult.failed} failed` : ""}
</p>
{bulkResult.duplicateLines.length > 0 && (
<p className="mt-1 text-red-500">
Account/API key already exists in the system (line{bulkResult.duplicateLines.length > 1 ? "s" : ""} {bulkResult.duplicateLines.join(", ")}).
</p>
)}
</div>
)}
<div className="flex gap-2">
@@ -22,6 +22,7 @@ import Link from "next/link";
import { getErrorCode, getRelativeTime } from "@/shared/utils";
import { useNotificationStore } from "@/store/notificationStore";
import { useHeaderSearchStore } from "@/store/headerSearchStore";
import useUserStore from "@/store/userStore";
import ModelAvailabilityBadge from "./components/ModelAvailabilityBadge";
import AddCompatibleModal from "./components/AddCompatibleModal";
@@ -105,6 +106,7 @@ export default function ProvidersPage() {
const [testingMode, setTestingMode] = useState(null);
const [testResults, setTestResults] = useState(null);
const notify = useNotificationStore();
const user = useUserStore((state) => state.user);
const searchQuery = useHeaderSearchStore((s) => s.query);
const registerSearch = useHeaderSearchStore((s) => s.register);
const unregisterSearch = useHeaderSearchStore((s) => s.unregister);
@@ -281,12 +283,18 @@ export default function ProvidersPage() {
"oauth",
);
const freeEntries = Object.entries(FREE_PROVIDERS)
.filter(([, info]) => !info.hidden && matchSearch(info.name))
.filter(
([, info]) =>
!info.hidden &&
(user?.role === "admin" || !info.noAuth) &&
matchSearch(info.name),
)
.sort(([, a], [, b]) => (b.noAuth ? 1 : 0) - (a.noAuth ? 1 : 0));
const freeTierEntries = sortByPriority(
Object.entries(FREE_TIER_PROVIDERS).filter(
([, info]) =>
!info.hidden &&
(user?.role === "admin" || !info.noAuth) &&
matchSearch(info.name) &&
(info.serviceKinds ?? ["llm"]).includes("llm"),
),
+22 -5
View File
@@ -7,6 +7,7 @@ import {
pollForToken
} from "@/lib/oauth/providers";
import { createProviderConnection } from "@/models";
import { getProviderConnectionAccess } from "@/lib/providers/connectionAccess";
import {
startCodexProxy,
stopCodexProxy,
@@ -20,7 +21,7 @@ import {
clearXaiSession,
} from "@/lib/oauth/utils/server";
async function completeXaiManualCode(code, state) {
async function completeXaiManualCode(code, state, ownerId) {
const session = state ? getXaiSessionStatus(state) : null;
if (!session) {
throw new Error("xAI OAuth session not found; restart the login flow and paste the code again");
@@ -38,6 +39,7 @@ async function completeXaiManualCode(code, state) {
const connection = await createProviderConnection({
provider: "xai",
authType: "oauth",
ownerId,
...tokenData,
expiresAt: tokenData.expiresIn
? new Date(Date.now() + tokenData.expiresIn * 1000).toISOString()
@@ -68,6 +70,7 @@ async function completeXaiManualCode(code, state) {
// GET /api/oauth/[provider]/device-code - Request device code (for device_code flow)
export async function GET(request, { params }) {
try {
const { user } = await getProviderConnectionAccess();
const { provider, action } = await params;
const { searchParams } = new URL(request.url);
@@ -98,8 +101,8 @@ export async function GET(request, { params }) {
let serverSide = false;
if (result.success && state && codeVerifier && redirectUri) {
serverSide = provider === "xai"
? registerXaiSession({ state, codeVerifier, redirectUri })
: registerCodexSession({ state, codeVerifier, redirectUri });
? registerXaiSession({ state, codeVerifier, redirectUri, ownerId: user.id })
: registerCodexSession({ state, codeVerifier, redirectUri, ownerId: user.id });
}
return NextResponse.json({ ...result, serverSide });
}
@@ -178,6 +181,9 @@ export async function GET(request, { params }) {
return NextResponse.json({ error: "Unknown action" }, { status: 400 });
} catch (error) {
if (error.message === "Unauthorized") {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
console.log("OAuth GET error:", error);
return NextResponse.json({ error: error.message }, { status: 500 });
}
@@ -187,6 +193,7 @@ export async function GET(request, { params }) {
// POST /api/oauth/[provider]/poll - Poll for token (device_code flow)
export async function POST(request, { params }) {
try {
const { user } = await getProviderConnectionAccess();
const { provider, action } = await params;
let body;
try {
@@ -223,6 +230,7 @@ export async function POST(request, { params }) {
const connection = await createProviderConnection({
provider,
authType: "access_token",
ownerId: user.id,
accessToken: code,
email: email || null,
providerSpecificData,
@@ -253,6 +261,7 @@ export async function POST(request, { params }) {
const connection = await createProviderConnection({
provider,
authType: "oauth",
ownerId: user.id,
...tokenData,
expiresAt: tokenData.expiresIn
? new Date(Date.now() + tokenData.expiresIn * 1000).toISOString()
@@ -307,6 +316,7 @@ export async function POST(request, { params }) {
const connection = await createProviderConnection({
provider,
authType: "oauth",
ownerId: user.id,
...result.tokens,
expiresAt: result.tokens.expiresIn
? new Date(Date.now() + result.tokens.expiresIn * 1000).toISOString()
@@ -339,13 +349,20 @@ export async function POST(request, { params }) {
return NextResponse.json({ error: "Manual code only supported for xai" }, { status: 400 });
}
const { code, state } = body;
const connection = await completeXaiManualCode(String(code || "").trim(), String(state || "").trim());
const connection = await completeXaiManualCode(
String(code || "").trim(),
String(state || "").trim(),
user.id,
);
return NextResponse.json({ success: true, connection });
}
return NextResponse.json({ error: "Unknown action" }, { status: 400 });
} catch (error) {
if (error.message === "Unauthorized") {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
console.log("OAuth POST error:", error);
return NextResponse.json({ error: error.message }, { status: 500 });
return NextResponse.json({ error: error.message }, { status: error.status || 500 });
}
}
+14 -1
View File
@@ -1,6 +1,7 @@
import { NextResponse } from "next/server";
import { createProviderConnection } from "@/models";
import { extractCodexAccountInfo } from "@/lib/oauth/providers";
import { requireCurrentDashboardUser } from "@/lib/auth/currentUser";
/**
* POST /api/oauth/codex/bulk-import
@@ -17,6 +18,16 @@ import { extractCodexAccountInfo } from "@/lib/oauth/providers";
* Tokens are NEVER echoed back in the response.
*/
export async function POST(request) {
let user;
try {
user = await requireCurrentDashboardUser();
} catch (error) {
if (error.message === "Unauthorized") {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
return NextResponse.json({ error: "Failed to authenticate user" }, { status: 500 });
}
let body;
try {
body = await request.json();
@@ -64,6 +75,7 @@ export async function POST(request) {
id: _id,
provider: _provider,
authType: _authType,
ownerId: _ownerId,
createdAt: _createdAt,
updatedAt: _updatedAt,
...item
@@ -106,13 +118,14 @@ export async function POST(request) {
const created = await createProviderConnection({
provider: "codex",
authType: "oauth",
ownerId: user.id,
...item,
});
results.push({ index: i, ok: true, id: created.id });
success++;
} catch (e) {
results.push({ index: i, ok: false, error: e.message || "Unknown error" });
results.push({ index: i, ok: false, error: e.message || "Unknown error", status: e.status || null });
failed++;
}
}
@@ -1,6 +1,7 @@
import { NextResponse } from "next/server";
import { createProviderConnection } from "@/models";
import { extractCodexAccountInfo } from "@/lib/oauth/providers";
import { getProviderConnectionAccess } from "@/lib/providers/connectionAccess";
/**
* POST /api/oauth/codex/import-token
@@ -11,6 +12,7 @@ import { extractCodexAccountInfo } from "@/lib/oauth/providers";
*/
export async function POST(request) {
try {
const { user } = await getProviderConnectionAccess();
const { accessToken, name } = await request.json();
if (!accessToken || typeof accessToken !== "string") {
@@ -71,6 +73,7 @@ export async function POST(request) {
const connection = await createProviderConnection({
provider: "codex",
authType: "access_token",
ownerId: user.id,
accessToken: token,
name: connectionName,
email: email,
@@ -90,7 +93,10 @@ export async function POST(request) {
},
});
} catch (error) {
if (error.message === "Unauthorized") {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
console.log("Codex access token import error:", error);
return NextResponse.json({ error: error.message }, { status: 500 });
return NextResponse.json({ error: error.message }, { status: error.status || 500 });
}
}
+7 -1
View File
@@ -1,6 +1,7 @@
import { NextResponse } from "next/server";
import { CursorService } from "@/lib/oauth/services/cursor";
import { createProviderConnection } from "@/models";
import { getProviderConnectionAccess } from "@/lib/providers/connectionAccess";
/**
* POST /api/oauth/cursor/import
@@ -12,6 +13,7 @@ import { createProviderConnection } from "@/models";
*/
export async function POST(request) {
try {
const { user } = await getProviderConnectionAccess();
const { accessToken, machineId } = await request.json();
if (!accessToken || typeof accessToken !== "string") {
@@ -43,6 +45,7 @@ export async function POST(request) {
const connection = await createProviderConnection({
provider: "cursor",
authType: "oauth",
ownerId: user.id,
accessToken: tokenData.accessToken,
refreshToken: null, // Cursor doesn't have public refresh endpoint
expiresAt: new Date(Date.now() + tokenData.expiresIn * 1000).toISOString(),
@@ -65,8 +68,11 @@ export async function POST(request) {
},
});
} catch (error) {
if (error.message === "Unauthorized") {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
console.log("Cursor import token error:", error);
return NextResponse.json({ error: error.message }, { status: 500 });
return NextResponse.json({ error: error.message }, { status: error.status || 500 });
}
}
+7 -1
View File
@@ -1,5 +1,6 @@
import { NextResponse } from "next/server";
import { createProviderConnection } from "@/models";
import { getProviderConnectionAccess } from "@/lib/providers/connectionAccess";
const GITLAB_DEFAULT_BASE = "https://gitlab.com";
@@ -9,6 +10,7 @@ const GITLAB_DEFAULT_BASE = "https://gitlab.com";
*/
export async function POST(request) {
try {
const { user: dashboardUser } = await getProviderConnectionAccess();
let body;
try {
body = await request.json();
@@ -39,6 +41,7 @@ export async function POST(request) {
await createProviderConnection({
provider: "gitlab",
authType: "oauth",
ownerId: dashboardUser.id,
accessToken: token.trim(),
refreshToken: null,
expiresAt: null,
@@ -56,7 +59,10 @@ export async function POST(request) {
return NextResponse.json({ success: true });
} catch (error) {
if (error.message === "Unauthorized") {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
console.error("GitLab PAT auth error:", error);
return NextResponse.json({ error: error.message }, { status: 500 });
return NextResponse.json({ error: error.message }, { status: error.status || 500 });
}
}
+7 -1
View File
@@ -1,5 +1,6 @@
import { NextResponse } from "next/server";
import { createProviderConnection } from "@/models";
import { getProviderConnectionAccess } from "@/lib/providers/connectionAccess";
/**
* iFlow Cookie-Based Authentication
@@ -8,6 +9,7 @@ import { createProviderConnection } from "@/models";
*/
export async function POST(request) {
try {
const { user } = await getProviderConnectionAccess();
const { cookie } = await request.json();
if (!cookie || typeof cookie !== "string") {
@@ -109,6 +111,7 @@ export async function POST(request) {
const connection = await createProviderConnection({
provider: "iflow",
authType: "cookie",
ownerId: user.id,
name: refreshedKey.name || keyData.name,
email: refreshedKey.name || keyData.name,
apiKey: refreshedKey.apiKey,
@@ -131,7 +134,10 @@ export async function POST(request) {
},
});
} catch (error) {
if (error.message === "Unauthorized") {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
console.error("iFlow cookie auth error:", error);
return NextResponse.json({ error: error.message }, { status: 500 });
return NextResponse.json({ error: error.message }, { status: error.status || 500 });
}
}
+9
View File
@@ -1,6 +1,7 @@
import { NextResponse } from "next/server";
import { KiroService } from "@/lib/oauth/services/kiro";
import { createProviderConnection } from "@/models";
import { getProviderConnectionAccess } from "@/lib/providers/connectionAccess";
/**
* POST /api/oauth/kiro/api-key
@@ -10,6 +11,7 @@ import { createProviderConnection } from "@/models";
*/
export async function POST(request) {
try {
const { user } = await getProviderConnectionAccess();
const { apiKey, region } = await request.json();
if (!apiKey || typeof apiKey !== "string" || !apiKey.trim()) {
@@ -35,6 +37,7 @@ export async function POST(request) {
const connection = await createProviderConnection({
provider: "kiro",
authType: "api_key",
ownerId: user.id,
accessToken: credential.accessToken,
refreshToken: null,
expiresAt: new Date(Date.now() + 365 * 24 * 60 * 60 * 1000).toISOString(),
@@ -57,7 +60,13 @@ export async function POST(request) {
},
});
} catch (error) {
if (error.message === "Unauthorized") {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
console.log("Kiro API key import error:", error);
if (error.status === 409) {
return NextResponse.json({ error: error.message }, { status: 409 });
}
// Do not reflect upstream response body to the client (SSRF hardening)
return NextResponse.json(
{ error: "API key validation failed" },
@@ -1,6 +1,7 @@
import { NextResponse } from "next/server";
import { createProviderConnection } from "@/models";
import { normalizeKiroExternalIdpAuth } from "@/lib/oauth/kiroExternalIdp";
import { getProviderConnectionAccess } from "@/lib/providers/connectionAccess";
/**
* POST /api/oauth/kiro/import-cli-proxy
@@ -8,6 +9,7 @@ import { normalizeKiroExternalIdpAuth } from "@/lib/oauth/kiroExternalIdp";
*/
export async function POST(request) {
try {
const { user } = await getProviderConnectionAccess();
const body = await request.json();
const rawAuth = body?.cliProxyAuth ?? body?.auth ?? body?.json ?? body;
const tokenData = normalizeKiroExternalIdpAuth(rawAuth);
@@ -15,6 +17,7 @@ export async function POST(request) {
const connection = await createProviderConnection({
provider: "kiro",
authType: "oauth",
ownerId: user.id,
accessToken: tokenData.accessToken,
refreshToken: tokenData.refreshToken,
expiresAt: tokenData.expiresAt,
@@ -32,9 +35,12 @@ export async function POST(request) {
},
});
} catch (error) {
if (error.message === "Unauthorized") {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
return NextResponse.json(
{ error: error?.message || "CLIProxyAPI import failed" },
{ status: 400 }
{ status: error?.status || 400 }
);
}
}
+7 -1
View File
@@ -1,6 +1,7 @@
import { NextResponse } from "next/server";
import { KiroService } from "@/lib/oauth/services/kiro";
import { createProviderConnection } from "@/models";
import { getProviderConnectionAccess } from "@/lib/providers/connectionAccess";
/**
* POST /api/oauth/kiro/import
@@ -10,6 +11,7 @@ import { createProviderConnection } from "@/models";
*/
export async function POST(request) {
try {
const { user } = await getProviderConnectionAccess();
const { refreshToken, clientId, clientSecret, region, authMethod, profileArn } = await request.json();
if (!refreshToken || typeof refreshToken !== "string") {
@@ -38,6 +40,7 @@ export async function POST(request) {
const connection = await createProviderConnection({
provider: "kiro",
authType: "oauth",
ownerId: user.id,
accessToken: tokenData.accessToken,
refreshToken: tokenData.refreshToken || refreshToken.trim(),
expiresAt: new Date(Date.now() + (tokenData.expiresIn || 3600) * 1000).toISOString(),
@@ -60,7 +63,10 @@ export async function POST(request) {
},
});
} catch (error) {
if (error.message === "Unauthorized") {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
console.log("Kiro import token error:", error);
return NextResponse.json({ error: error.message }, { status: 500 });
return NextResponse.json({ error: error.message }, { status: error.status || 500 });
}
}
@@ -1,6 +1,7 @@
import { NextResponse } from "next/server";
import { KiroService } from "@/lib/oauth/services/kiro";
import { createProviderConnection } from "@/models";
import { getProviderConnectionAccess } from "@/lib/providers/connectionAccess";
/**
* POST /api/oauth/kiro/social-exchange
@@ -9,6 +10,7 @@ import { createProviderConnection } from "@/models";
*/
export async function POST(request) {
try {
const { user } = await getProviderConnectionAccess();
const { code, codeVerifier, provider } = await request.json();
if (!code || !codeVerifier) {
@@ -40,6 +42,7 @@ export async function POST(request) {
const connection = await createProviderConnection({
provider: "kiro",
authType: "oauth",
ownerId: user.id,
accessToken: tokenData.accessToken,
refreshToken: tokenData.refreshToken,
expiresAt: new Date(Date.now() + tokenData.expiresIn * 1000).toISOString(),
@@ -61,7 +64,10 @@ export async function POST(request) {
},
});
} catch (error) {
if (error.message === "Unauthorized") {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
console.log("Kiro social exchange error:", error);
return NextResponse.json({ error: error.message }, { status: 500 });
return NextResponse.json({ error: error.message }, { status: error.status || 500 });
}
}
+6 -1
View File
@@ -1,5 +1,6 @@
import { NextResponse } from "next/server";
import { getProviderConnectionById } from "@/models";
import { getProviderConnectionAccess } from "@/lib/providers/connectionAccess";
import { isOpenAICompatibleProvider, isAnthropicCompatibleProvider } from "@/shared/constants/providers";
import { GEMINI_CONFIG } from "@/lib/oauth/constants/oauth";
import { refreshGoogleToken, updateProviderCredentials } from "@/sse/services/tokenRefresh";
@@ -393,7 +394,8 @@ const PROVIDER_MODELS_CONFIG = {
export async function GET(request, { params }) {
try {
const { id } = await params;
const connection = await getProviderConnectionById(id);
const { ownerId } = await getProviderConnectionAccess();
const connection = await getProviderConnectionById(id, ownerId);
if (!connection) {
return NextResponse.json({ error: "Connection not found" }, { status: 404 });
@@ -546,6 +548,9 @@ export async function GET(request, { params }) {
models
});
} catch (error) {
if (error.message === "Unauthorized") {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
console.log("Error fetching provider models:", error);
return NextResponse.json({ error: "Failed to fetch models" }, { status: 500 });
}
+20 -2
View File
@@ -5,6 +5,7 @@ import {
updateProviderConnection,
deleteProviderConnection,
} from "@/models";
import { getProviderConnectionAccess } from "@/lib/providers/connectionAccess";
function normalizeProxyConfig(body = {}) {
const hasAnyProxyField =
@@ -63,7 +64,8 @@ function shouldMergeProviderSpecificData(existing, incoming, hasLegacyProxy, has
export async function GET(request, { params }) {
try {
const { id } = await params;
const connection = await getProviderConnectionById(id);
const { ownerId } = await getProviderConnectionAccess();
const connection = await getProviderConnectionById(id, ownerId);
if (!connection) {
return NextResponse.json({ error: "Connection not found" }, { status: 404 });
@@ -78,6 +80,9 @@ export async function GET(request, { params }) {
return NextResponse.json({ connection: result });
} catch (error) {
if (error.message === "Unauthorized") {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
console.log("Error fetching connection:", error);
return NextResponse.json({ error: "Failed to fetch connection" }, { status: 500 });
}
@@ -87,6 +92,7 @@ export async function GET(request, { params }) {
export async function PUT(request, { params }) {
try {
const { id } = await params;
const { ownerId } = await getProviderConnectionAccess();
const body = await request.json();
const {
name,
@@ -101,7 +107,7 @@ export async function PUT(request, { params }) {
providerSpecificData
} = body;
const existing = await getProviderConnectionById(id);
const existing = await getProviderConnectionById(id, ownerId);
if (!existing) {
return NextResponse.json({ error: "Connection not found" }, { status: 404 });
}
@@ -166,6 +172,9 @@ export async function PUT(request, { params }) {
return NextResponse.json({ connection: result });
} catch (error) {
if (error.message === "Unauthorized") {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
console.log("Error updating connection:", error);
return NextResponse.json({ error: "Failed to update connection" }, { status: 500 });
}
@@ -175,6 +184,12 @@ export async function PUT(request, { params }) {
export async function DELETE(request, { params }) {
try {
const { id } = await params;
const { ownerId } = await getProviderConnectionAccess();
const existing = await getProviderConnectionById(id, ownerId);
if (!existing) {
return NextResponse.json({ error: "Connection not found" }, { status: 404 });
}
const deleted = await deleteProviderConnection(id);
if (!deleted) {
@@ -183,6 +198,9 @@ export async function DELETE(request, { params }) {
return NextResponse.json({ message: "Connection deleted successfully" });
} catch (error) {
if (error.message === "Unauthorized") {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
console.log("Error deleting connection:", error);
return NextResponse.json({ error: "Failed to delete connection" }, { status: 500 });
}
@@ -4,6 +4,7 @@ import { getProviderModels, PROVIDER_ID_TO_ALIAS } from "open-sse/config/provide
import { isOpenAICompatibleProvider, isAnthropicCompatibleProvider } from "@/shared/constants/providers";
import { UPDATER_CONFIG } from "@/shared/constants/config";
import { pingModelByKind } from "@/app/api/models/test/ping";
import { getProviderConnectionAccess } from "@/lib/providers/connectionAccess";
/**
* POST /api/providers/[id]/test-models
@@ -13,7 +14,8 @@ import { pingModelByKind } from "@/app/api/models/test/ping";
export async function POST(request, { params }) {
try {
const { id } = await params;
const connection = await getProviderConnectionById(id);
const { ownerId } = await getProviderConnectionAccess();
const connection = await getProviderConnectionById(id, ownerId);
if (!connection) {
return NextResponse.json({ error: "Connection not found" }, { status: 404 });
}
@@ -60,6 +62,9 @@ export async function POST(request, { params }) {
return NextResponse.json({ provider: providerId, connectionId: id, results });
} catch (error) {
if (error.message === "Unauthorized") {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
console.log("Error testing models:", error);
return NextResponse.json({ error: "Test failed" }, { status: 500 });
}
+10
View File
@@ -1,10 +1,17 @@
import { NextResponse } from "next/server";
import { testSingleConnection } from "./testUtils.js";
import { getProviderConnectionById } from "@/lib/localDb";
import { getProviderConnectionAccess } from "@/lib/providers/connectionAccess";
// POST /api/providers/[id]/test - Test connection
export async function POST(request, { params }) {
try {
const { id } = await params;
const { ownerId } = await getProviderConnectionAccess();
const connection = await getProviderConnectionById(id, ownerId);
if (!connection) {
return NextResponse.json({ error: "Connection not found" }, { status: 404 });
}
const result = await testSingleConnection(id);
if (result.error === "Connection not found") {
@@ -17,6 +24,9 @@ export async function POST(request, { params }) {
refreshed: result.refreshed || false,
});
} catch (error) {
if (error.message === "Unauthorized") {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
console.log("Error testing connection:", error);
return NextResponse.json({ error: "Test failed" }, { status: 500 });
}
+6 -1
View File
@@ -2,6 +2,7 @@ import { NextResponse } from "next/server";
import { getProviderConnections } from "@/lib/localDb";
import { backfillCodexEmails } from "@/lib/oauth/providers";
import { USAGE_APIKEY_PROVIDERS, USAGE_SUPPORTED_PROVIDERS } from "@/shared/constants/providers";
import { getProviderConnectionAccess } from "@/lib/providers/connectionAccess";
const SAFE_FIELDS = [
"id", "provider", "authType", "name", "email", "displayName",
@@ -76,6 +77,7 @@ function sortConnections(connections, sort) {
export async function GET(request) {
try {
const { ownerId } = await getProviderConnectionAccess();
await backfillCodexEmails();
const { searchParams } = new URL(request.url);
@@ -85,7 +87,7 @@ export async function GET(request) {
const page = parsePositiveInt(searchParams.get("page"), 1);
const pageSize = Math.min(parsePositiveInt(searchParams.get("pageSize"), DEFAULT_PAGE_SIZE), MAX_PAGE_SIZE);
const allConnections = await getProviderConnections();
const allConnections = await getProviderConnections(ownerId ? { ownerId } : {});
const eligibleConnections = allConnections.filter(isUsageEligible);
const providerOptions = Array.from(new Set(eligibleConnections.map((conn) => conn.provider))).sort();
@@ -121,6 +123,9 @@ export async function GET(request) {
},
});
} catch (error) {
if (error.message === "Unauthorized") {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
console.log("Error fetching providers for client:", error);
return NextResponse.json({ error: "Failed to fetch providers" }, { status: 500 });
}
+15 -2
View File
@@ -9,6 +9,7 @@ import {
import { APIKEY_PROVIDERS } from "@/shared/constants/config";
import { AI_PROVIDERS, FREE_TIER_PROVIDERS, WEB_COOKIE_PROVIDERS, isOpenAICompatibleProvider, isAnthropicCompatibleProvider, isCustomEmbeddingProvider } from "@/shared/constants/providers";
import { normalizeProviderId, normalizeProviderSpecificData } from "@/lib/providerNormalization";
import { getProviderConnectionAccess } from "@/lib/providers/connectionAccess";
export const dynamic = "force-dynamic";
@@ -49,7 +50,8 @@ async function normalizeProxyPoolId(proxyPoolId) {
// GET /api/providers - List all connections
export async function GET() {
try {
const connections = await getProviderConnections();
const { ownerId } = await getProviderConnectionAccess();
const connections = await getProviderConnections(ownerId ? { ownerId } : {});
// Build nodeNameMap for compatible providers (id → name)
let nodeNameMap = {};
@@ -78,6 +80,9 @@ export async function GET() {
return NextResponse.json({ connections: safeConnections });
} catch (error) {
if (error.message === "Unauthorized") {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
console.log("Error fetching providers:", error);
return NextResponse.json({ error: "Failed to fetch providers" }, { status: 500 });
}
@@ -86,6 +91,7 @@ export async function GET() {
// POST /api/providers - Create new connection (API Key only, OAuth via separate flow)
export async function POST(request) {
try {
const { user } = await getProviderConnectionAccess();
const body = await request.json();
const provider = normalizeProviderId(body.provider);
const { apiKey, name, displayName, priority, globalPriority, defaultModel, testStatus } = body;
@@ -175,6 +181,7 @@ export async function POST(request) {
const newConnection = await createProviderConnection({
provider,
authType: isWebCookieProvider ? "cookie" : "apikey",
ownerId: user.id,
name: connectionName,
apiKey: apiKey || "",
priority: priority || 1,
@@ -191,7 +198,13 @@ export async function POST(request) {
return NextResponse.json({ connection: result }, { status: 201 });
} catch (error) {
if (error.message === "Unauthorized") {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
console.log("Error creating provider:", error);
return NextResponse.json({ error: "Failed to create provider" }, { status: 500 });
return NextResponse.json(
{ error: error.status === 409 ? error.message : "Failed to create provider" },
{ status: error.status || 500 },
);
}
}
+9 -1
View File
@@ -8,6 +8,7 @@ import {
ANTHROPIC_COMPATIBLE_PREFIX,
} from "@/shared/constants/providers";
import { testSingleConnection } from "../[id]/test/testUtils.js";
import { getProviderConnectionAccess } from "@/lib/providers/connectionAccess";
function getAuthGroup(providerId, connection = null) {
// Prioritize authType from connection if available
@@ -42,6 +43,7 @@ function isCompatibleProvider(providerId) {
// POST /api/providers/test-batch - Test multiple connections by group
export async function POST(request) {
try {
const { ownerId } = await getProviderConnectionAccess();
const body = await request.json();
const { mode, providerId } = body;
@@ -49,7 +51,10 @@ export async function POST(request) {
return NextResponse.json({ error: "mode is required" }, { status: 400 });
}
const allConnections = await getProviderConnections({ isActive: true });
const allConnections = await getProviderConnections({
isActive: true,
...(ownerId ? { ownerId } : {}),
});
let connectionsToTest = [];
if (mode === "provider" && providerId) {
@@ -125,6 +130,9 @@ export async function POST(request) {
},
});
} catch (error) {
if (error.message === "Unauthorized") {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
console.log("Error in batch test:", error);
return NextResponse.json({ error: "Batch test failed" }, { status: 500 });
}
+5 -4
View File
@@ -82,7 +82,7 @@ export async function exportDb() {
const out = {
settings: await exportSettings(),
users: db.all(`SELECT id, username, password, role, isActive, createdAt, updatedAt FROM users`).map((r) => ({ ...r, isActive: r.isActive === 1 || r.isActive === true })),
providerConnections: db.all(`SELECT * FROM providerConnections`).map((r) => ({ ...parseJson(r.data, {}), id: r.id, provider: r.provider, authType: r.authType, name: r.name, email: r.email, priority: r.priority, isActive: r.isActive === 1, createdAt: r.createdAt, updatedAt: r.updatedAt })),
providerConnections: db.all(`SELECT * FROM providerConnections`).map((r) => ({ ...parseJson(r.data, {}), id: r.id, provider: r.provider, authType: r.authType, name: r.name, email: r.email, ownerId: r.ownerId, priority: r.priority, isActive: r.isActive === 1, createdAt: r.createdAt, updatedAt: r.updatedAt })),
providerNodes: db.all(`SELECT * FROM providerNodes`).map((r) => ({ ...parseJson(r.data, {}), id: r.id, type: r.type, name: r.name, createdAt: r.createdAt, updatedAt: r.updatedAt })),
proxyPools: db.all(`SELECT * FROM proxyPools`).map((r) => ({ ...parseJson(r.data, {}), id: r.id, isActive: r.isActive === 1, testStatus: r.testStatus, createdAt: r.createdAt, updatedAt: r.updatedAt })),
apiKeys: db.all(`SELECT * FROM apiKeys`).map((r) => ({ id: r.id, key: r.key, name: r.name, machineId: r.machineId, ownerId: r.ownerId, isActive: r.isActive === 1, createdAt: r.createdAt })),
@@ -144,11 +144,12 @@ export async function importDb(payload) {
}
}
const fallbackOwnerId = db.get(`SELECT id FROM users WHERE role = 'admin' ORDER BY createdAt ASC LIMIT 1`)?.id || null;
for (const c of payload.providerConnections || []) {
const { id, provider, authType, name, email, priority, isActive, createdAt, updatedAt, ...rest } = c;
const { id, provider, authType, name, email, ownerId, priority, isActive, createdAt, updatedAt, ...rest } = c;
db.run(
`INSERT OR REPLACE INTO providerConnections(id, provider, authType, name, email, priority, isActive, data, createdAt, updatedAt) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[id, provider, authType || "oauth", name || null, email || null, priority || null, isActive === false ? 0 : 1, stringifyJson(rest), createdAt || new Date().toISOString(), updatedAt || new Date().toISOString()]
`INSERT OR REPLACE INTO providerConnections(id, provider, authType, name, email, ownerId, priority, isActive, data, createdAt, updatedAt) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[id, provider, authType || "oauth", name || null, email || null, ownerId || fallbackOwnerId, priority || null, isActive === false ? 0 : 1, stringifyJson(rest), createdAt || new Date().toISOString(), updatedAt || new Date().toISOString()]
);
}
for (const n of payload.providerNodes || []) {
@@ -0,0 +1,20 @@
// Provider connections belong to the dashboard account that created them.
// Existing installations retain their connections under the first admin.
export default {
version: 4,
name: "provider-connection-owners",
up(db) {
const columns = db.all(`PRAGMA table_info(providerConnections)`);
if (!columns.some((column) => column.name === "ownerId")) {
db.exec(`ALTER TABLE providerConnections ADD COLUMN ownerId TEXT`);
}
const admin = db.get(`SELECT id FROM users WHERE role = 'admin' ORDER BY createdAt ASC LIMIT 1`);
if (admin) {
db.run(
`UPDATE providerConnections SET ownerId = ? WHERE ownerId IS NULL OR ownerId = ''`,
[admin.id],
);
}
},
};
+2 -1
View File
@@ -4,8 +4,9 @@
import m001 from "./001-initial.js";
import m002 from "./002-users-table.js";
import m003 from "./003-api-key-owners.js";
import m004 from "./004-provider-connection-owners.js";
export const MIGRATIONS = [m001, m002, m003].sort((a, b) => a.version - b.version);
export const MIGRATIONS = [m001, m002, m003, m004].sort((a, b) => a.version - b.version);
export function latestVersion() {
return MIGRATIONS.length ? MIGRATIONS[MIGRATIONS.length - 1].version : 0;
+69 -52
View File
@@ -20,6 +20,7 @@ function rowToConn(row) {
authType: row.authType,
name: row.name,
email: row.email,
ownerId: row.ownerId,
priority: row.priority,
isActive: row.isActive === 1 || row.isActive === true,
createdAt: row.createdAt,
@@ -28,13 +29,14 @@ function rowToConn(row) {
}
function connToRow(c) {
const { id, provider, authType, name, email, priority, isActive, createdAt, updatedAt, ...rest } = c;
const { id, provider, authType, name, email, ownerId, priority, isActive, createdAt, updatedAt, ...rest } = c;
return {
id,
provider,
authType,
name: name ?? null,
email: email ?? null,
ownerId: ownerId ?? null,
priority: priority ?? null,
isActive: isActive === false ? 0 : 1,
data: stringifyJson(rest),
@@ -46,13 +48,13 @@ function connToRow(c) {
function upsert(db, c) {
const r = connToRow(c);
db.run(
`INSERT INTO providerConnections(id, provider, authType, name, email, priority, isActive, data, createdAt, updatedAt)
VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`INSERT INTO providerConnections(id, provider, authType, name, email, ownerId, priority, isActive, data, createdAt, updatedAt)
VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(id) DO UPDATE SET
provider=excluded.provider, authType=excluded.authType, name=excluded.name,
email=excluded.email, priority=excluded.priority, isActive=excluded.isActive,
email=excluded.email, ownerId=excluded.ownerId, priority=excluded.priority, isActive=excluded.isActive,
data=excluded.data, updatedAt=excluded.updatedAt`,
[r.id, r.provider, r.authType, r.name, r.email, r.priority, r.isActive, r.data, r.createdAt, r.updatedAt]
[r.id, r.provider, r.authType, r.name, r.email, r.ownerId, r.priority, r.isActive, r.data, r.createdAt, r.updatedAt]
);
}
@@ -67,11 +69,54 @@ function deriveConnectionName(data, fallbackName) {
return fallbackName;
}
function findDuplicateAccount(connections, data) {
const incomingCredential = data.accessToken || data.apiKey || data.refreshToken;
if (incomingCredential) {
const credentialMatch = connections.find((connection) => (
connection.accessToken === incomingCredential ||
connection.apiKey === incomingCredential ||
connection.refreshToken === incomingCredential
));
if (credentialMatch) return credentialMatch;
}
if (!data.email) return null;
const incomingUsername = data.providerSpecificData?.username;
const incomingWorkspace = data.providerSpecificData?.chatgptAccountId;
return connections.find((connection) => {
if (connection.email !== data.email) return false;
const existingWorkspace = connection.providerSpecificData?.chatgptAccountId;
if (incomingWorkspace && existingWorkspace) {
return incomingWorkspace === existingWorkspace;
}
if (incomingWorkspace || existingWorkspace) return false;
const existingUsername = connection.providerSpecificData?.username;
if (incomingUsername && existingUsername) {
return incomingUsername === existingUsername;
}
if (incomingUsername || existingUsername) return false;
return true;
});
}
function duplicateAccountError() {
const error = new Error("Account already exists in the system");
error.code = "PROVIDER_ACCOUNT_EXISTS";
error.status = 409;
return error;
}
export async function getProviderConnections(filter = {}) {
const db = await getAdapter();
const where = [];
const params = [];
if (filter.provider) { where.push("provider = ?"); params.push(filter.provider); }
if (filter.ownerId) { where.push("ownerId = ?"); params.push(filter.ownerId); }
if (filter.isActive !== undefined) { where.push("isActive = ?"); params.push(filter.isActive ? 1 : 0); }
const sql = `SELECT * FROM providerConnections${where.length ? ` WHERE ${where.join(" AND ")}` : ""}`;
const rows = db.all(sql, params);
@@ -80,9 +125,15 @@ export async function getProviderConnections(filter = {}) {
return list;
}
export async function getProviderConnectionById(id) {
export async function getProviderConnectionById(id, ownerId = null) {
const db = await getAdapter();
const row = db.get(`SELECT * FROM providerConnections WHERE id = ?`, [id]);
const where = ["id = ?"];
const params = [id];
if (ownerId) {
where.push("ownerId = ?");
params.push(ownerId);
}
const row = db.get(`SELECT * FROM providerConnections WHERE ${where.join(" AND ")}`, params);
return rowToConn(row);
}
@@ -105,52 +156,17 @@ export async function createProviderConnection(data) {
let result;
db.transaction(() => {
const all = db.all(`SELECT * FROM providerConnections WHERE provider = ?`, [data.provider]).map(rowToConn);
const allProviderConnections = db.all(`SELECT * FROM providerConnections WHERE provider = ?`, [data.provider]).map(rowToConn);
const all = data.ownerId
? allProviderConnections.filter((connection) => connection.ownerId === data.ownerId)
: allProviderConnections;
let existing = null;
if (data.authType === "oauth" && data.email) {
const incomingUsername = data.providerSpecificData?.username;
const incomingWs = data.providerSpecificData?.chatgptAccountId;
existing = all.find(c => {
if (c.authType !== "oauth" || c.email !== data.email) return false;
// Codex/OpenAI can issue multiple OAuth grants for the same email.
// Refresh tokens are rotated single-use; collapsing a new login onto an
// existing bare-email row overwrites the first account's token pair and
// makes it look "invalid" after adding a second account. Only update an
// existing Codex row when both rows expose the same ChatGPT account ID.
if (data.provider === "codex") {
const existingWs = c.providerSpecificData?.chatgptAccountId;
return !!incomingWs && !!existingWs && incomingWs === existingWs;
}
// Workspace providers use workspace ID when both sides have it
const existingWs = c.providerSpecificData?.chatgptAccountId;
if (incomingWs && existingWs) return incomingWs === existingWs;
if (incomingWs && !existingWs) return false;
if (!incomingWs && existingWs) return false;
// Non-workspace providers: match on (email + username) so cross-IdP
// accounts don't overwrite each other. Require username on both sides
// — if only one side has it, treat as a distinct identity rather than
// collapsing onto the bare-email fallback (which would re-introduce
// the cross-IdP overwrite).
const existingUsername = c.providerSpecificData?.username;
if (incomingUsername && existingUsername) {
return incomingUsername === existingUsername;
}
if (incomingUsername || existingUsername) return false;
return true;
});
} else if (data.authType === "apikey" && data.name) {
existing = all.find(c => c.authType === "apikey" && c.name === data.name);
}
// access_token: never dedup — user manages duplicates manually
if (existing) {
const merged = { ...existing, ...data, updatedAt: now };
upsert(db, merged);
result = merged;
return;
// Account identities are global: two dashboard users must not register the
// same provider account. API-key connections without an account identity
// or matching credential remain private and may use the same display name.
const existingAccount = findDuplicateAccount(allProviderConnections, data);
if (existingAccount) {
throw duplicateAccountError();
}
let connectionName = data.name || null;
@@ -167,6 +183,7 @@ export async function createProviderConnection(data) {
provider: data.provider,
authType: data.authType || "oauth",
name: connectionName,
ownerId: data.ownerId || null,
priority: connectionPriority,
isActive: data.isActive !== undefined ? data.isActive : true,
createdAt: now,
+3 -1
View File
@@ -3,7 +3,7 @@
// pre-change safety backup in migrate.js: when the stored version is lower,
// one lightweight DB backup is taken before applying schema changes. Forgetting
// to bump only skips that backup — it does NOT break the additive auto-sync.
export const SCHEMA_VERSION = 3;
export const SCHEMA_VERSION = 4;
export const PRAGMA_SQL = `
PRAGMA journal_mode = WAL;
@@ -53,6 +53,7 @@ export const TABLES = {
authType: "TEXT NOT NULL",
name: "TEXT",
email: "TEXT",
ownerId: "TEXT",
priority: "INTEGER",
isActive: "INTEGER DEFAULT 1",
data: "TEXT NOT NULL",
@@ -61,6 +62,7 @@ export const TABLES = {
},
indexes: [
"CREATE INDEX IF NOT EXISTS idx_pc_provider ON providerConnections(provider)",
"CREATE INDEX IF NOT EXISTS idx_pc_owner ON providerConnections(ownerId)",
"CREATE INDEX IF NOT EXISTS idx_pc_provider_active ON providerConnections(provider, isActive)",
"CREATE INDEX IF NOT EXISTS idx_pc_priority ON providerConnections(provider, priority)",
],
+8 -4
View File
@@ -129,11 +129,12 @@ const pendingExchanges = new Map();
* Register a pending exchange session for server-side mode.
* Modal client calls this before opening popup.
*/
export function registerCodexSession({ state, codeVerifier, redirectUri }) {
if (!state || !codeVerifier || !redirectUri) return false;
export function registerCodexSession({ state, codeVerifier, redirectUri, ownerId }) {
if (!state || !codeVerifier || !redirectUri || !ownerId) return false;
pendingExchanges.set(state, {
codeVerifier,
redirectUri,
ownerId,
status: "pending",
createdAt: Date.now(),
});
@@ -224,6 +225,7 @@ export function startCodexProxy(appPort) {
const connection = await createProviderConnection({
provider: "codex",
authType: "oauth",
ownerId: session.ownerId,
...tokenData,
expiresAt: tokenData.expiresIn
? new Date(Date.now() + tokenData.expiresIn * 1000).toISOString()
@@ -297,11 +299,12 @@ const XAI_PROXY_TIMEOUT_MS = 300000; // 5 minutes
const XAI_PROXY_PORT = 56121;
const xaiPendingExchanges = new Map();
export function registerXaiSession({ state, codeVerifier, redirectUri }) {
if (!state || !codeVerifier || !redirectUri) return false;
export function registerXaiSession({ state, codeVerifier, redirectUri, ownerId }) {
if (!state || !codeVerifier || !redirectUri || !ownerId) return false;
xaiPendingExchanges.set(state, {
codeVerifier,
redirectUri,
ownerId,
status: "pending",
createdAt: Date.now(),
});
@@ -366,6 +369,7 @@ export function startXaiProxy(appPort) {
const connection = await createProviderConnection({
provider: "xai",
authType: "oauth",
ownerId: session.ownerId,
...tokenData,
expiresAt: tokenData.expiresIn
? new Date(Date.now() + tokenData.expiresIn * 1000).toISOString()
+9
View File
@@ -0,0 +1,9 @@
import { requireCurrentDashboardUser } from "@/lib/auth/currentUser";
export async function getProviderConnectionAccess() {
const user = await requireCurrentDashboardUser();
return {
user,
ownerId: user.role === "admin" ? null : user.id,
};
}
+4
View File
@@ -130,6 +130,10 @@ export default function OAuthModal({ isOpen, provider, providerInfo, onSuccess,
return;
}
if (!res.ok) {
throw new Error(data.error || "OAuth connection failed");
}
if (data.error === "expired_token" || data.error === "access_denied") {
throw new Error(data.errorDescription || data.error);
}
+2
View File
@@ -37,6 +37,8 @@ describe("Schema migrations", () => {
"_meta", "settings", "providerConnections", "providerNodes",
"proxyPools", "apiKeys", "combos", "kv", "usageHistory", "usageDaily", "requestDetails",
]));
expect(db.all(`PRAGMA table_info(providerConnections)`).map((column) => column.name)).toContain("ownerId");
expect(db.all(`PRAGMA index_list(providerConnections)`).map((index) => index.name)).toContain("idx_pc_owner");
});
it("existing DB at older schemaVersion → re-applies pending migrations on restart", async () => {
+80
View File
@@ -113,6 +113,86 @@ describe("DB SQLite layer — public API parity", () => {
expect(back.providerSpecificData).toEqual({ foo: "bar" });
});
it("providerConnections: scopes retrieval and lookup to connection owner", async () => {
const ownerOne = await sqliteDb.createUser({ username: "connection-owner-one", password: "password", role: "user" });
const ownerTwo = await sqliteDb.createUser({ username: "connection-owner-two", password: "password", role: "user" });
const firstConnection = await sqliteDb.createProviderConnection({
provider: "owner-test-one",
authType: "apikey",
name: "owner-one-connection",
apiKey: "key-one",
ownerId: ownerOne.id,
});
const secondConnection = await sqliteDb.createProviderConnection({
provider: "owner-test-two",
authType: "apikey",
name: "owner-two-connection",
apiKey: "key-two",
ownerId: ownerTwo.id,
});
const account = await sqliteDb.createProviderConnection({
provider: "owner-test-account",
authType: "oauth",
email: "shared@example.com",
accessToken: "token-one",
ownerId: ownerOne.id,
});
const ownerOneConnections = await sqliteDb.getProviderConnections({ ownerId: ownerOne.id });
expect(ownerOneConnections.map((connection) => connection.id)).toContain(firstConnection.id);
expect(ownerOneConnections.map((connection) => connection.id)).not.toContain(secondConnection.id);
await expect(sqliteDb.createProviderConnection({
provider: "owner-test-account",
authType: "oauth",
email: "shared@example.com",
accessToken: "token-two",
ownerId: ownerTwo.id,
})).rejects.toMatchObject({
code: "PROVIDER_ACCOUNT_EXISTS",
status: 409,
});
expect(account.ownerId).toBe(ownerOne.id);
await sqliteDb.createProviderConnection({
provider: "owner-test-token",
authType: "access_token",
accessToken: "shared-token",
ownerId: ownerOne.id,
});
await expect(sqliteDb.createProviderConnection({
provider: "owner-test-token",
authType: "access_token",
accessToken: "shared-token",
ownerId: ownerTwo.id,
})).rejects.toMatchObject({
code: "PROVIDER_ACCOUNT_EXISTS",
status: 409,
});
await sqliteDb.createProviderConnection({
provider: "owner-test-api-key",
authType: "apikey",
name: "first-key-name",
apiKey: "shared-api-key",
ownerId: ownerOne.id,
});
await expect(sqliteDb.createProviderConnection({
provider: "owner-test-api-key",
authType: "apikey",
name: "different-key-name",
apiKey: "shared-api-key",
ownerId: ownerTwo.id,
})).rejects.toMatchObject({
code: "PROVIDER_ACCOUNT_EXISTS",
status: 409,
});
expect(await sqliteDb.getProviderConnectionById(firstConnection.id, ownerTwo.id)).toBeNull();
expect(await sqliteDb.getProviderConnectionById(firstConnection.id, ownerOne.id)).toMatchObject({
id: firstConnection.id,
ownerId: ownerOne.id,
});
});
it("providerConnections: GitHub OAuth uses account identity as fallback name", async () => {
const c = await sqliteDb.createProviderConnection({
provider: "github",