mirror of
https://github.com/danswer-ai/danswer.git
synced 2025-04-02 08:58:11 +02:00
Add API key generation in the UI + allow it to be used across all endpoints
This commit is contained in:
parent
4b44073d9a
commit
ae02a5199a
@ -52,7 +52,6 @@ from danswer.llm.llm_initialization import load_llm_providers
|
||||
from danswer.search.retrieval.search_runner import download_nltk_data
|
||||
from danswer.search.search_nlp_models import warm_up_encoders
|
||||
from danswer.server.auth_check import check_router_auth
|
||||
from danswer.server.danswer_api.ingestion import get_danswer_api_key
|
||||
from danswer.server.danswer_api.ingestion import router as danswer_api_router
|
||||
from danswer.server.documents.cc_pair import router as cc_pair_router
|
||||
from danswer.server.documents.connector import router as connector_router
|
||||
@ -154,10 +153,6 @@ async def lifespan(app: FastAPI) -> AsyncGenerator:
|
||||
# Will throw exception if an issue is found
|
||||
verify_auth()
|
||||
|
||||
# Danswer APIs key
|
||||
api_key = get_danswer_api_key()
|
||||
logger.info(f"Danswer API Key: {api_key}")
|
||||
|
||||
if OAUTH_CLIENT_ID and OAUTH_CLIENT_SECRET:
|
||||
logger.info("Both OAuth Client ID and Secret are configured.")
|
||||
|
||||
|
@ -1,9 +1,5 @@
|
||||
import secrets
|
||||
|
||||
from fastapi import APIRouter
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from fastapi import Depends
|
||||
from fastapi import Header
|
||||
from fastapi import HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from danswer.configs.constants import DocumentSource
|
||||
@ -17,55 +13,19 @@ from danswer.db.embedding_model import get_secondary_db_embedding_model
|
||||
from danswer.db.engine import get_session
|
||||
from danswer.document_index.document_index_utils import get_both_index_names
|
||||
from danswer.document_index.factory import get_default_document_index
|
||||
from danswer.dynamic_configs.factory import get_dynamic_config_store
|
||||
from danswer.dynamic_configs.interface import ConfigNotFoundError
|
||||
from danswer.indexing.embedder import DefaultIndexingEmbedder
|
||||
from danswer.indexing.indexing_pipeline import build_indexing_pipeline
|
||||
from danswer.server.danswer_api.models import DocMinimalInfo
|
||||
from danswer.server.danswer_api.models import IngestionDocument
|
||||
from danswer.server.danswer_api.models import IngestionResult
|
||||
from danswer.utils.logger import setup_logger
|
||||
from ee.danswer.auth.users import api_key_dep
|
||||
|
||||
logger = setup_logger()
|
||||
|
||||
# not using /api to avoid confusion with nginx api path routing
|
||||
router = APIRouter(prefix="/danswer-api")
|
||||
|
||||
# Assumes this gives admin privileges, basic users should not be allowed to call any Danswer apis
|
||||
_DANSWER_API_KEY = "danswer_api_key"
|
||||
|
||||
|
||||
def get_danswer_api_key(key_len: int = 30, dont_regenerate: bool = False) -> str | None:
|
||||
kv_store = get_dynamic_config_store()
|
||||
try:
|
||||
return str(kv_store.load(_DANSWER_API_KEY))
|
||||
except ConfigNotFoundError:
|
||||
if dont_regenerate:
|
||||
return None
|
||||
|
||||
logger.info("Generating Danswer API Key")
|
||||
|
||||
api_key = "dn_" + secrets.token_urlsafe(key_len)
|
||||
kv_store.store(_DANSWER_API_KEY, api_key, encrypt=True)
|
||||
|
||||
return api_key
|
||||
|
||||
|
||||
def delete_danswer_api_key() -> None:
|
||||
kv_store = get_dynamic_config_store()
|
||||
try:
|
||||
kv_store.delete(_DANSWER_API_KEY)
|
||||
except ConfigNotFoundError:
|
||||
pass
|
||||
|
||||
|
||||
def api_key_dep(authorization: str = Header(...)) -> str:
|
||||
saved_key = get_danswer_api_key(dont_regenerate=True)
|
||||
token = authorization.removeprefix("Bearer ").strip()
|
||||
if token != saved_key or not saved_key:
|
||||
raise HTTPException(status_code=401, detail="Invalid API key")
|
||||
return token
|
||||
|
||||
|
||||
@router.get("/connector-docs/{cc_pair_id}")
|
||||
def get_docs_by_connector_credential_pair(
|
||||
|
48
backend/ee/danswer/auth/api_key.py
Normal file
48
backend/ee/danswer/auth/api_key.py
Normal file
@ -0,0 +1,48 @@
|
||||
import secrets
|
||||
import uuid
|
||||
|
||||
from fastapi import Request
|
||||
from passlib.hash import sha256_crypt
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
_API_KEY_HEADER_NAME = "Authorization"
|
||||
_BEARER_PREFIX = "Bearer "
|
||||
_API_KEY_PREFIX = "dn_"
|
||||
_API_KEY_LEN = 192
|
||||
|
||||
|
||||
class ApiKeyDescriptor(BaseModel):
|
||||
api_key_id: int
|
||||
api_key_display: str
|
||||
api_key: str | None = None # only present on initial creation
|
||||
|
||||
user_id: uuid.UUID
|
||||
|
||||
|
||||
def generate_api_key() -> str:
|
||||
return _API_KEY_PREFIX + secrets.token_urlsafe(_API_KEY_LEN)
|
||||
|
||||
|
||||
def hash_api_key(api_key: str) -> str:
|
||||
# NOTE: no salt is needed, as the API key is randomly generated
|
||||
# and overlaps are impossible
|
||||
return sha256_crypt.hash(api_key, salt="")
|
||||
|
||||
|
||||
def build_displayable_api_key(api_key: str) -> str:
|
||||
if api_key.startswith(_API_KEY_PREFIX):
|
||||
api_key = api_key[len(_API_KEY_PREFIX) :]
|
||||
|
||||
return _API_KEY_PREFIX + api_key[:4] + "********" + api_key[-4:]
|
||||
|
||||
|
||||
def get_hashed_api_key_from_request(request: Request) -> str | None:
|
||||
raw_api_key_header = request.headers.get(_API_KEY_HEADER_NAME)
|
||||
if raw_api_key_header is None:
|
||||
return None
|
||||
|
||||
if raw_api_key_header.startswith(_BEARER_PREFIX):
|
||||
raw_api_key_header = raw_api_key_header[len(_BEARER_PREFIX) :].strip()
|
||||
|
||||
return hash_api_key(raw_api_key_header)
|
@ -1,3 +1,4 @@
|
||||
from fastapi import Depends
|
||||
from fastapi import HTTPException
|
||||
from fastapi import Request
|
||||
from fastapi import status
|
||||
@ -6,8 +7,11 @@ from sqlalchemy.orm import Session
|
||||
from danswer.configs.app_configs import AUTH_TYPE
|
||||
from danswer.configs.app_configs import DISABLE_AUTH
|
||||
from danswer.configs.constants import AuthType
|
||||
from danswer.db.engine import get_session
|
||||
from danswer.db.models import User
|
||||
from danswer.utils.logger import setup_logger
|
||||
from ee.danswer.auth.api_key import get_hashed_api_key_from_request
|
||||
from ee.danswer.db.api_key import fetch_user_for_api_key
|
||||
from ee.danswer.db.saml import get_saml_account
|
||||
from ee.danswer.utils.secrets import extract_hashed_cookie
|
||||
|
||||
@ -36,6 +40,12 @@ async def double_check_user(
|
||||
saml_account = get_saml_account(cookie=saved_cookie, db_session=db_session)
|
||||
user = saml_account.user if saml_account else None
|
||||
|
||||
# check if an API key is present
|
||||
if user is None:
|
||||
hashed_api_key = get_hashed_api_key_from_request(request)
|
||||
if hashed_api_key:
|
||||
user = fetch_user_for_api_key(hashed_api_key, db_session)
|
||||
|
||||
if user is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
@ -43,3 +53,17 @@ async def double_check_user(
|
||||
)
|
||||
|
||||
return user
|
||||
|
||||
|
||||
def api_key_dep(request: Request, db_session: Session = Depends(get_session)) -> User:
|
||||
hashed_api_key = get_hashed_api_key_from_request(request)
|
||||
if not hashed_api_key:
|
||||
raise HTTPException(status_code=401, detail="Missing API key")
|
||||
|
||||
if hashed_api_key:
|
||||
user = fetch_user_for_api_key(hashed_api_key, db_session)
|
||||
|
||||
if user is None:
|
||||
raise HTTPException(status_code=401, detail="Invalid API key")
|
||||
|
||||
return user
|
||||
|
108
backend/ee/danswer/db/api_key.py
Normal file
108
backend/ee/danswer/db/api_key.py
Normal file
@ -0,0 +1,108 @@
|
||||
import uuid
|
||||
|
||||
from fastapi_users.password import PasswordHelper
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from danswer.auth.schemas import UserRole
|
||||
from danswer.db.models import ApiKey
|
||||
from danswer.db.models import User
|
||||
from ee.danswer.auth.api_key import ApiKeyDescriptor
|
||||
from ee.danswer.auth.api_key import build_displayable_api_key
|
||||
from ee.danswer.auth.api_key import generate_api_key
|
||||
from ee.danswer.auth.api_key import hash_api_key
|
||||
|
||||
_DANSWER_API_KEY = "danswer_api_key"
|
||||
|
||||
|
||||
def fetch_api_keys(db_session: Session) -> list[ApiKeyDescriptor]:
|
||||
api_keys = db_session.scalars(select(ApiKey)).all()
|
||||
return [
|
||||
ApiKeyDescriptor(
|
||||
api_key_id=api_key.id,
|
||||
api_key_display=api_key.api_key_display,
|
||||
user_id=api_key.user_id,
|
||||
)
|
||||
for api_key in api_keys
|
||||
]
|
||||
|
||||
|
||||
def fetch_user_for_api_key(hashed_api_key: str, db_session: Session) -> User | None:
|
||||
api_key = db_session.scalar(
|
||||
select(ApiKey).where(ApiKey.hashed_api_key == hashed_api_key)
|
||||
)
|
||||
if api_key is None:
|
||||
return None
|
||||
|
||||
return db_session.scalar(select(User).where(User.id == api_key.user_id)) # type: ignore
|
||||
|
||||
|
||||
def insert_api_key(db_session: Session, user_id: uuid.UUID | None) -> ApiKeyDescriptor:
|
||||
std_password_helper = PasswordHelper()
|
||||
api_key = generate_api_key()
|
||||
api_key_user_id = uuid.uuid4()
|
||||
|
||||
api_key_user_row = User(
|
||||
id=api_key_user_id,
|
||||
email=f"{_DANSWER_API_KEY}__{api_key_user_id}",
|
||||
# a random password for the "user"
|
||||
hashed_password=std_password_helper.hash(std_password_helper.generate()),
|
||||
is_active=True,
|
||||
is_superuser=False,
|
||||
is_verified=True,
|
||||
role=UserRole.BASIC,
|
||||
)
|
||||
db_session.add(api_key_user_row)
|
||||
|
||||
api_key_row = ApiKey(
|
||||
hashed_api_key=hash_api_key(api_key),
|
||||
api_key_display=build_displayable_api_key(api_key),
|
||||
user_id=api_key_user_id,
|
||||
owner_id=user_id,
|
||||
)
|
||||
db_session.add(api_key_row)
|
||||
|
||||
db_session.commit()
|
||||
return ApiKeyDescriptor(
|
||||
api_key_id=api_key_row.id,
|
||||
api_key_display=api_key_row.api_key_display,
|
||||
api_key=api_key,
|
||||
user_id=api_key_user_id,
|
||||
)
|
||||
|
||||
|
||||
def regenerate_api_key(db_session: Session, api_key_id: int) -> ApiKeyDescriptor:
|
||||
"""NOTE: currently, any admin can regenerate any API key."""
|
||||
existing_api_key = db_session.scalar(select(ApiKey).where(ApiKey.id == api_key_id))
|
||||
if existing_api_key is None:
|
||||
raise ValueError(f"API key with id {api_key_id} does not exist")
|
||||
|
||||
new_api_key = generate_api_key()
|
||||
existing_api_key.hashed_api_key = hash_api_key(new_api_key)
|
||||
existing_api_key.api_key_display = build_displayable_api_key(new_api_key)
|
||||
db_session.commit()
|
||||
|
||||
return ApiKeyDescriptor(
|
||||
api_key_id=existing_api_key.id,
|
||||
api_key_display=existing_api_key.api_key_display,
|
||||
api_key=new_api_key,
|
||||
user_id=existing_api_key.user_id,
|
||||
)
|
||||
|
||||
|
||||
def remove_api_key(db_session: Session, api_key_id: int) -> None:
|
||||
existing_api_key = db_session.scalar(select(ApiKey).where(ApiKey.id == api_key_id))
|
||||
if existing_api_key is None:
|
||||
raise ValueError(f"API key with id {api_key_id} does not exist")
|
||||
|
||||
user_associated_with_key = db_session.scalar(
|
||||
select(User).where(User.id == existing_api_key.user_id) # type: ignore
|
||||
)
|
||||
if user_associated_with_key is None:
|
||||
raise ValueError(
|
||||
f"User associated with API key with id {api_key_id} does not exist. This should not happen."
|
||||
)
|
||||
|
||||
db_session.delete(existing_api_key)
|
||||
db_session.delete(user_associated_with_key)
|
||||
db_session.commit()
|
@ -17,6 +17,7 @@ from danswer.utils.logger import setup_logger
|
||||
from danswer.utils.variable_functionality import global_version
|
||||
from ee.danswer.configs.app_configs import OPENID_CONFIG_URL
|
||||
from ee.danswer.server.analytics.api import router as analytics_router
|
||||
from ee.danswer.server.api_key.api import router as api_key_router
|
||||
from ee.danswer.server.query_history.api import router as query_history_router
|
||||
from ee.danswer.server.saml import router as saml_router
|
||||
from ee.danswer.server.user_group.api import router as user_group_router
|
||||
@ -59,6 +60,8 @@ def get_ee_application() -> FastAPI:
|
||||
# analytics endpoints
|
||||
application.include_router(analytics_router)
|
||||
application.include_router(query_history_router)
|
||||
# api key management
|
||||
application.include_router(api_key_router)
|
||||
|
||||
return application
|
||||
|
||||
|
48
backend/ee/danswer/server/api_key/api.py
Normal file
48
backend/ee/danswer/server/api_key/api.py
Normal file
@ -0,0 +1,48 @@
|
||||
from fastapi import APIRouter
|
||||
from fastapi import Depends
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
import danswer.db.models as db_models
|
||||
from danswer.auth.users import current_admin_user
|
||||
from danswer.db.engine import get_session
|
||||
from ee.danswer.db.api_key import ApiKeyDescriptor
|
||||
from ee.danswer.db.api_key import fetch_api_keys
|
||||
from ee.danswer.db.api_key import insert_api_key
|
||||
from ee.danswer.db.api_key import regenerate_api_key
|
||||
from ee.danswer.db.api_key import remove_api_key
|
||||
|
||||
router = APIRouter(prefix="/admin/api-key")
|
||||
|
||||
|
||||
@router.get("")
|
||||
def list_api_keys(
|
||||
_: db_models.User | None = Depends(current_admin_user),
|
||||
db_session: Session = Depends(get_session),
|
||||
) -> list[ApiKeyDescriptor]:
|
||||
return fetch_api_keys(db_session)
|
||||
|
||||
|
||||
@router.post("")
|
||||
def create_api_key(
|
||||
user: db_models.User | None = Depends(current_admin_user),
|
||||
db_session: Session = Depends(get_session),
|
||||
) -> ApiKeyDescriptor:
|
||||
return insert_api_key(db_session, user.id if user else None)
|
||||
|
||||
|
||||
@router.patch("/{api_key_id}")
|
||||
def regenerate_existing_api_key(
|
||||
api_key_id: int,
|
||||
_: db_models.User | None = Depends(current_admin_user),
|
||||
db_session: Session = Depends(get_session),
|
||||
) -> ApiKeyDescriptor:
|
||||
return regenerate_api_key(db_session, api_key_id)
|
||||
|
||||
|
||||
@router.delete("/{api_key_id}")
|
||||
def delete_api_key(
|
||||
api_key_id: int,
|
||||
_: db_models.User | None = Depends(current_admin_user),
|
||||
db_session: Session = Depends(get_session),
|
||||
) -> None:
|
||||
remove_api_key(db_session, api_key_id)
|
@ -21,6 +21,10 @@ const nextConfig = {
|
||||
source: "/admin/groups/:path*",
|
||||
destination: "/ee/admin/groups/:path*",
|
||||
},
|
||||
{
|
||||
source: "/admin/api-key",
|
||||
destination: "/ee/admin/api-key",
|
||||
},
|
||||
// analytics / audit log pages
|
||||
{
|
||||
source: "/admin/performance/analytics",
|
||||
|
252
web/src/app/ee/admin/api-key/page.tsx
Normal file
252
web/src/app/ee/admin/api-key/page.tsx
Normal file
@ -0,0 +1,252 @@
|
||||
"use client";
|
||||
|
||||
import { ThreeDotsLoader } from "@/components/Loading";
|
||||
import { AdminPageTitle } from "@/components/admin/Title";
|
||||
import { KeyIcon } from "@/components/icons/icons";
|
||||
import { errorHandlingFetcher } from "@/lib/fetcher";
|
||||
import { ErrorCallout } from "@/components/ErrorCallout";
|
||||
import useSWR, { mutate } from "swr";
|
||||
import {
|
||||
Button,
|
||||
Divider,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeaderCell,
|
||||
TableRow,
|
||||
Text,
|
||||
Title,
|
||||
} from "@tremor/react";
|
||||
import { usePopup } from "@/components/admin/connectors/Popup";
|
||||
import { useState } from "react";
|
||||
import { Table } from "@tremor/react";
|
||||
import { DeleteButton } from "@/components/DeleteButton";
|
||||
import { FiCopy, FiRefreshCw, FiX } from "react-icons/fi";
|
||||
import { Modal } from "@/components/Modal";
|
||||
|
||||
interface APIKey {
|
||||
api_key_id: number;
|
||||
api_key_display: string;
|
||||
api_key: string | null; // only present on initial creation
|
||||
user_id: string;
|
||||
}
|
||||
|
||||
const API_KEY_TEXT = `
|
||||
API Keys allow you to access Danswer APIs programmatically. Click the button below to generate a new API Key.
|
||||
`;
|
||||
|
||||
function NewApiKeyModal({
|
||||
apiKey,
|
||||
onClose,
|
||||
}: {
|
||||
apiKey: string;
|
||||
onClose: () => void;
|
||||
}) {
|
||||
const [copyClicked, setCopyClicked] = useState(false);
|
||||
|
||||
return (
|
||||
<Modal onOutsideClick={onClose}>
|
||||
<div className="px-8 py-8">
|
||||
<div className="flex w-full border-b border-border mb-4 pb-4">
|
||||
<Title>New API Key</Title>
|
||||
<div onClick={onClose} className="ml-auto p-1 rounded hover:bg-hover">
|
||||
<FiX size={18} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="h-32">
|
||||
<Text className="mb-4">
|
||||
Make sure you copy your new API key. You won’t be able to see this
|
||||
key again.
|
||||
</Text>
|
||||
|
||||
<div className="flex mt-2">
|
||||
<b className="my-auto break-all">{apiKey}</b>
|
||||
<div
|
||||
className="ml-2 my-auto p-2 hover:bg-hover rounded cursor-pointer"
|
||||
onClick={() => {
|
||||
setCopyClicked(true);
|
||||
navigator.clipboard.writeText(apiKey);
|
||||
setTimeout(() => {
|
||||
setCopyClicked(false);
|
||||
}, 10000);
|
||||
}}
|
||||
>
|
||||
<FiCopy size="16" className="my-auto" />
|
||||
</div>
|
||||
</div>
|
||||
{copyClicked && (
|
||||
<Text className="text-success text-xs font-medium mt-1">
|
||||
API Key copied!
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
function Main() {
|
||||
const { popup, setPopup } = usePopup();
|
||||
|
||||
const {
|
||||
data: apiKeys,
|
||||
isLoading,
|
||||
error,
|
||||
} = useSWR<APIKey[]>("/api/admin/api-key", errorHandlingFetcher);
|
||||
|
||||
const [fullApiKey, setFullApiKey] = useState<string | null>(null);
|
||||
|
||||
if (isLoading) {
|
||||
return <ThreeDotsLoader />;
|
||||
}
|
||||
|
||||
if (!apiKeys || error) {
|
||||
return (
|
||||
<ErrorCallout
|
||||
errorTitle="Failed to fetch API Keys"
|
||||
errorMsg={error?.info?.detail || error.toString()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const newApiKeyButton = (
|
||||
<Button
|
||||
color="green"
|
||||
size="xs"
|
||||
className="mt-3"
|
||||
onClick={async () => {
|
||||
const response = await fetch("/api/admin/api-key", {
|
||||
method: "POST",
|
||||
});
|
||||
if (!response.ok) {
|
||||
const errorMsg = await response.text();
|
||||
setPopup({
|
||||
type: "error",
|
||||
message: `Failed to create API Key: ${errorMsg}`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
const newKey = (await response.json()) as APIKey;
|
||||
setFullApiKey(newKey.api_key);
|
||||
mutate("/api/admin/api-key");
|
||||
}}
|
||||
>
|
||||
Create API Key
|
||||
</Button>
|
||||
);
|
||||
|
||||
if (apiKeys.length === 0) {
|
||||
return (
|
||||
<div>
|
||||
{popup}
|
||||
<Text>{API_KEY_TEXT}</Text>
|
||||
{newApiKeyButton}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{fullApiKey && (
|
||||
<NewApiKeyModal
|
||||
apiKey={fullApiKey}
|
||||
onClose={() => setFullApiKey(null)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Text>{API_KEY_TEXT}</Text>
|
||||
{newApiKeyButton}
|
||||
|
||||
<Divider />
|
||||
|
||||
<Title className="mt-6">Existing API Keys</Title>
|
||||
<Table className="overflow-visible">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableHeaderCell>API Key</TableHeaderCell>
|
||||
<TableHeaderCell>Regenerate</TableHeaderCell>
|
||||
<TableHeaderCell>Delete</TableHeaderCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{apiKeys.map((apiKey) => (
|
||||
<TableRow key={apiKey.api_key_id}>
|
||||
<TableCell className="max-w-64">
|
||||
{apiKey.api_key_display}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div
|
||||
className={`
|
||||
my-auto
|
||||
flex
|
||||
mb-1
|
||||
w-fit
|
||||
hover:bg-hover cursor-pointer
|
||||
p-2
|
||||
rounded-lg
|
||||
border-border
|
||||
text-sm`}
|
||||
onClick={async () => {
|
||||
const response = await fetch(
|
||||
`/api/admin/api-key/${apiKey.api_key_id}`,
|
||||
{
|
||||
method: "PATCH",
|
||||
}
|
||||
);
|
||||
if (!response.ok) {
|
||||
const errorMsg = await response.text();
|
||||
setPopup({
|
||||
type: "error",
|
||||
message: `Failed to regenerate API Key: ${errorMsg}`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
const newKey = (await response.json()) as APIKey;
|
||||
setFullApiKey(newKey.api_key);
|
||||
mutate("/api/admin/api-key");
|
||||
}}
|
||||
>
|
||||
<FiRefreshCw className="mr-1 my-auto" />
|
||||
Refresh
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<DeleteButton
|
||||
onClick={async () => {
|
||||
const response = await fetch(
|
||||
`/api/admin/api-key/${apiKey.api_key_id}`,
|
||||
{
|
||||
method: "DELETE",
|
||||
}
|
||||
);
|
||||
if (!response.ok) {
|
||||
const errorMsg = await response.text();
|
||||
setPopup({
|
||||
type: "error",
|
||||
message: `Failed to delete API Key: ${errorMsg}`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
mutate("/api/admin/api-key");
|
||||
}}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
|
||||
return <div>{fullApiKey}</div>;
|
||||
}
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<div className="mx-auto container">
|
||||
<AdminPageTitle title="API Keys" icon={<KeyIcon size={32} />} />
|
||||
|
||||
<Main />
|
||||
</div>
|
||||
);
|
||||
}
|
@ -194,6 +194,15 @@ export async function Layout({ children }: { children: React.ReactNode }) {
|
||||
),
|
||||
link: "/admin/groups",
|
||||
},
|
||||
{
|
||||
name: (
|
||||
<div className="flex">
|
||||
<KeyIcon size={18} />
|
||||
<div className="ml-1">API Keys</div>
|
||||
</div>
|
||||
),
|
||||
link: "/admin/api-key",
|
||||
},
|
||||
]
|
||||
: []),
|
||||
],
|
||||
|
Loading…
x
Reference in New Issue
Block a user