diff --git a/backend/danswer/db/credentials.py b/backend/danswer/db/credentials.py
index 18e900dbd19..9e0374902dd 100644
--- a/backend/danswer/db/credentials.py
+++ b/backend/danswer/db/credentials.py
@@ -20,14 +20,17 @@
def fetch_credentials(
- user: User | None,
db_session: Session,
+ user: User | None = None,
+ public_only: bool | None = None,
) -> list[Credential]:
stmt = select(Credential)
if user:
stmt = stmt.where(
or_(Credential.user_id == user.id, Credential.user_id.is_(None))
)
+ if public_only is not None:
+ stmt = stmt.where(Credential.public_doc == public_only)
results = db_session.scalars(stmt)
return list(results.all())
@@ -106,7 +109,7 @@ def backend_update_credential_json(
def delete_credential(
credential_id: int,
- user: User,
+ user: User | None,
db_session: Session,
) -> None:
credential = fetch_credential_by_id(credential_id, user, db_session)
@@ -146,7 +149,7 @@ def create_initial_public_credential() -> None:
def delete_google_drive_service_account_credentials(
user: User | None, db_session: Session
) -> None:
- credentials = fetch_credentials(user, db_session)
+ credentials = fetch_credentials(db_session=db_session, user=user)
for credential in credentials:
if credential.credential_json.get(DB_CREDENTIALS_DICT_SERVICE_ACCOUNT_KEY):
db_session.delete(credential)
diff --git a/backend/danswer/main.py b/backend/danswer/main.py
index fcd4f18dcd5..197bec11473 100644
--- a/backend/danswer/main.py
+++ b/backend/danswer/main.py
@@ -30,6 +30,7 @@
from danswer.datastores.document_index import get_default_document_index
from danswer.db.credentials import create_initial_public_credential
from danswer.direct_qa.llm_utils import get_default_llm
+from danswer.server.credential import router as credential_router
from danswer.server.event_loading import router as event_processing_router
from danswer.server.health import router as health_router
from danswer.server.manage import router as admin_router
@@ -68,6 +69,7 @@ def get_application() -> FastAPI:
application.include_router(event_processing_router)
application.include_router(admin_router)
application.include_router(user_router)
+ application.include_router(credential_router)
application.include_router(health_router)
application.include_router(
diff --git a/backend/danswer/server/credential.py b/backend/danswer/server/credential.py
new file mode 100644
index 00000000000..591ff388270
--- /dev/null
+++ b/backend/danswer/server/credential.py
@@ -0,0 +1,128 @@
+from fastapi import APIRouter
+from fastapi import Depends
+from fastapi import HTTPException
+from sqlalchemy.orm import Session
+
+from danswer.auth.users import current_admin_user
+from danswer.auth.users import current_user
+from danswer.db.credentials import create_credential
+from danswer.db.credentials import delete_credential
+from danswer.db.credentials import fetch_credential_by_id
+from danswer.db.credentials import fetch_credentials
+from danswer.db.credentials import update_credential
+from danswer.db.engine import get_session
+from danswer.db.models import User
+from danswer.server.models import CredentialBase
+from danswer.server.models import CredentialSnapshot
+from danswer.server.models import ObjectCreationIdResponse
+from danswer.server.models import StatusResponse
+
+
+router = APIRouter(prefix="/manage")
+
+
+"""Admin-only endpoints"""
+
+
+@router.get("/admin/credential")
+def list_credentials_admin(
+ _: User = Depends(current_admin_user),
+ db_session: Session = Depends(get_session),
+) -> list[CredentialSnapshot]:
+ """Lists all public credentials"""
+ credentials = fetch_credentials(db_session=db_session, public_only=True)
+ return [
+ CredentialSnapshot.from_credential_db_model(credential)
+ for credential in credentials
+ ]
+
+
+@router.delete("/admin/credential/{credential_id}")
+def delete_credential_by_id_admin(
+ credential_id: int,
+ _: User = Depends(current_admin_user),
+ db_session: Session = Depends(get_session),
+) -> StatusResponse:
+ """Same as the user endpoint, but can delete any credential (not just the user's own)"""
+ delete_credential(db_session=db_session, credential_id=credential_id, user=None)
+ return StatusResponse(
+ success=True, message="Credential deleted successfully", data=credential_id
+ )
+
+
+"""Endpoints for all"""
+
+
+@router.get("/credential")
+def list_credentials(
+ user: User | None = Depends(current_user),
+ db_session: Session = Depends(get_session),
+) -> list[CredentialSnapshot]:
+ credentials = fetch_credentials(db_session=db_session, user=user)
+ return [
+ CredentialSnapshot.from_credential_db_model(credential)
+ for credential in credentials
+ ]
+
+
+@router.get("/credential/{credential_id}")
+def get_credential_by_id(
+ credential_id: int,
+ user: User = Depends(current_user),
+ db_session: Session = Depends(get_session),
+) -> CredentialSnapshot | StatusResponse[int]:
+ credential = fetch_credential_by_id(credential_id, user, db_session)
+ if credential is None:
+ raise HTTPException(
+ status_code=401,
+ detail=f"Credential {credential_id} does not exist or does not belong to user",
+ )
+
+ return CredentialSnapshot.from_credential_db_model(credential)
+
+
+@router.post("/credential")
+def create_credential_from_model(
+ connector_info: CredentialBase,
+ user: User = Depends(current_user),
+ db_session: Session = Depends(get_session),
+) -> ObjectCreationIdResponse:
+ return create_credential(connector_info, user, db_session)
+
+
+@router.patch("/credential/{credential_id}")
+def update_credential_from_model(
+ credential_id: int,
+ credential_data: CredentialBase,
+ user: User = Depends(current_user),
+ db_session: Session = Depends(get_session),
+) -> CredentialSnapshot | StatusResponse[int]:
+ updated_credential = update_credential(
+ credential_id, credential_data, user, db_session
+ )
+ if updated_credential is None:
+ raise HTTPException(
+ status_code=401,
+ detail=f"Credential {credential_id} does not exist or does not belong to user",
+ )
+
+ return CredentialSnapshot(
+ id=updated_credential.id,
+ credential_json=updated_credential.credential_json,
+ user_id=updated_credential.user_id,
+ public_doc=updated_credential.public_doc,
+ time_created=updated_credential.time_created,
+ time_updated=updated_credential.time_updated,
+ )
+
+
+@router.delete("/credential/{credential_id}")
+def delete_credential_by_id(
+ credential_id: int,
+ user: User = Depends(current_user),
+ db_session: Session = Depends(get_session),
+) -> StatusResponse:
+ delete_credential(credential_id, user, db_session)
+ return StatusResponse(
+ success=True, message="Credential deleted successfully", data=credential_id
+ )
diff --git a/backend/danswer/server/manage.py b/backend/danswer/server/manage.py
index 01a67ca33e6..6dbcea67a06 100644
--- a/backend/danswer/server/manage.py
+++ b/backend/danswer/server/manage.py
@@ -641,81 +641,6 @@ def get_connector_by_id(
)
-@router.get("/credential")
-def get_credentials(
- user: User = Depends(current_user),
- db_session: Session = Depends(get_session),
-) -> list[CredentialSnapshot]:
- credentials = fetch_credentials(user, db_session)
- return [
- CredentialSnapshot.from_credential_db_model(credential)
- for credential in credentials
- ]
-
-
-@router.get("/credential/{credential_id}")
-def get_credential_by_id(
- credential_id: int,
- user: User = Depends(current_user),
- db_session: Session = Depends(get_session),
-) -> CredentialSnapshot | StatusResponse[int]:
- credential = fetch_credential_by_id(credential_id, user, db_session)
- if credential is None:
- raise HTTPException(
- status_code=401,
- detail=f"Credential {credential_id} does not exist or does not belong to user",
- )
-
- return CredentialSnapshot.from_credential_db_model(credential)
-
-
-@router.post("/credential")
-def create_credential_from_model(
- connector_info: CredentialBase,
- user: User = Depends(current_user),
- db_session: Session = Depends(get_session),
-) -> ObjectCreationIdResponse:
- return create_credential(connector_info, user, db_session)
-
-
-@router.patch("/credential/{credential_id}")
-def update_credential_from_model(
- credential_id: int,
- credential_data: CredentialBase,
- user: User = Depends(current_user),
- db_session: Session = Depends(get_session),
-) -> CredentialSnapshot | StatusResponse[int]:
- updated_credential = update_credential(
- credential_id, credential_data, user, db_session
- )
- if updated_credential is None:
- raise HTTPException(
- status_code=401,
- detail=f"Credential {credential_id} does not exist or does not belong to user",
- )
-
- return CredentialSnapshot(
- id=updated_credential.id,
- credential_json=updated_credential.credential_json,
- user_id=updated_credential.user_id,
- public_doc=updated_credential.public_doc,
- time_created=updated_credential.time_created,
- time_updated=updated_credential.time_updated,
- )
-
-
-@router.delete("/credential/{credential_id}")
-def delete_credential_by_id(
- credential_id: int,
- user: User = Depends(current_user),
- db_session: Session = Depends(get_session),
-) -> StatusResponse:
- delete_credential(credential_id, user, db_session)
- return StatusResponse(
- success=True, message="Credential deleted successfully", data=credential_id
- )
-
-
@router.put("/connector/{connector_id}/credential/{credential_id}")
def associate_credential_to_connector(
connector_id: int,
diff --git a/web/src/app/admin/connectors/bookstack/page.tsx b/web/src/app/admin/connectors/bookstack/page.tsx
index bc92d9dacee..d32985db7e5 100644
--- a/web/src/app/admin/connectors/bookstack/page.tsx
+++ b/web/src/app/admin/connectors/bookstack/page.tsx
@@ -8,16 +8,16 @@ import { CredentialForm } from "@/components/admin/connectors/CredentialForm";
import {
BookstackCredentialJson,
BookstackConfig,
- Credential,
ConnectorIndexingStatus,
} from "@/lib/types";
import useSWR, { useSWRConfig } from "swr";
import { fetcher } from "@/lib/fetcher";
import { LoadingAnimation } from "@/components/Loading";
-import { deleteCredential, linkCredential } from "@/lib/credential";
+import { adminDeleteCredential, linkCredential } from "@/lib/credential";
import { ConnectorForm } from "@/components/admin/connectors/ConnectorForm";
import { ConnectorsTable } from "@/components/admin/connectors/table/ConnectorsTable";
import { usePopup } from "@/components/admin/connectors/Popup";
+import { usePublicCredentials } from "@/lib/hooks";
const Main = () => {
const { popup, setPopup } = usePopup();
@@ -35,10 +35,8 @@ const Main = () => {
data: credentialsData,
isLoading: isCredentialsLoading,
error: isCredentialsError,
- } = useSWR[]>(
- "/api/manage/credential",
- fetcher
- );
+ refreshCredentials,
+ } = usePublicCredentials();
if (
(!connectorIndexingStatuses && isConnectorIndexingStatusesLoading) ||
@@ -91,8 +89,8 @@ const Main = () => {
});
return;
}
- await deleteCredential(bookstackCredential.id);
- mutate("/api/manage/credential");
+ await adminDeleteCredential(bookstackCredential.id);
+ refreshCredentials();
}}
>
@@ -146,7 +144,7 @@ const Main = () => {
}}
onSubmit={(isSuccess) => {
if (isSuccess) {
- mutate("/api/manage/credential");
+ refreshCredentials();
mutate("/api/manage/admin/connector/indexing-status");
}
}}
diff --git a/web/src/app/admin/connectors/confluence/page.tsx b/web/src/app/admin/connectors/confluence/page.tsx
index 509a2f70021..a526923e62f 100644
--- a/web/src/app/admin/connectors/confluence/page.tsx
+++ b/web/src/app/admin/connectors/confluence/page.tsx
@@ -8,16 +8,16 @@ import { CredentialForm } from "@/components/admin/connectors/CredentialForm";
import {
ConfluenceCredentialJson,
ConfluenceConfig,
- Credential,
ConnectorIndexingStatus,
} from "@/lib/types";
import useSWR, { useSWRConfig } from "swr";
import { fetcher } from "@/lib/fetcher";
import { LoadingAnimation } from "@/components/Loading";
-import { deleteCredential, linkCredential } from "@/lib/credential";
+import { adminDeleteCredential, linkCredential } from "@/lib/credential";
import { ConnectorForm } from "@/components/admin/connectors/ConnectorForm";
import { ConnectorsTable } from "@/components/admin/connectors/table/ConnectorsTable";
import { usePopup } from "@/components/admin/connectors/Popup";
+import { usePublicCredentials } from "@/lib/hooks";
const Main = () => {
const { popup, setPopup } = usePopup();
@@ -35,10 +35,8 @@ const Main = () => {
data: credentialsData,
isLoading: isCredentialsLoading,
error: isCredentialsError,
- } = useSWR[]>(
- "/api/manage/credential",
- fetcher
- );
+ refreshCredentials,
+ } = usePublicCredentials();
if (
(!connectorIndexingStatuses && isConnectorIndexingStatusesLoading) ||
@@ -97,8 +95,8 @@ const Main = () => {
});
return;
}
- await deleteCredential(confluenceCredential.id);
- mutate("/api/manage/credential");
+ await adminDeleteCredential(confluenceCredential.id);
+ refreshCredentials();
}}
>
@@ -143,7 +141,7 @@ const Main = () => {
}}
onSubmit={(isSuccess) => {
if (isSuccess) {
- mutate("/api/manage/credential");
+ refreshCredentials();
}
}}
/>
diff --git a/web/src/app/admin/connectors/github/page.tsx b/web/src/app/admin/connectors/github/page.tsx
index 83e690f5a51..ac30dca23ee 100644
--- a/web/src/app/admin/connectors/github/page.tsx
+++ b/web/src/app/admin/connectors/github/page.tsx
@@ -15,8 +15,9 @@ import {
import { ConnectorForm } from "@/components/admin/connectors/ConnectorForm";
import { LoadingAnimation } from "@/components/Loading";
import { CredentialForm } from "@/components/admin/connectors/CredentialForm";
-import { deleteCredential, linkCredential } from "@/lib/credential";
+import { adminDeleteCredential, linkCredential } from "@/lib/credential";
import { ConnectorsTable } from "@/components/admin/connectors/table/ConnectorsTable";
+import { usePublicCredentials } from "@/lib/hooks";
const Main = () => {
const { mutate } = useSWRConfig();
@@ -33,10 +34,8 @@ const Main = () => {
data: credentialsData,
isLoading: isCredentialsLoading,
error: isCredentialsError,
- } = useSWR[]>(
- "/api/manage/credential",
- fetcher
- );
+ refreshCredentials,
+ } = usePublicCredentials();
if (
(!connectorIndexingStatuses && isConnectorIndexingStatusesLoading) ||
@@ -60,9 +59,10 @@ const Main = () => {
(connectorIndexingStatus) =>
connectorIndexingStatus.connector.source === "github"
);
- const githubCredential = credentialsData.filter(
- (credential) => credential.credential_json?.github_access_token
- )[0];
+ const githubCredential: Credential =
+ credentialsData.filter(
+ (credential) => credential.credential_json?.github_access_token
+ )[0];
return (
<>
@@ -80,8 +80,8 @@ const Main = () => {