mirror of
https://github.com/danswer-ai/danswer.git
synced 2025-06-23 14:30:57 +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.retrieval.search_runner import download_nltk_data
|
||||||
from danswer.search.search_nlp_models import warm_up_encoders
|
from danswer.search.search_nlp_models import warm_up_encoders
|
||||||
from danswer.server.auth_check import check_router_auth
|
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.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.cc_pair import router as cc_pair_router
|
||||||
from danswer.server.documents.connector import router as connector_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
|
# Will throw exception if an issue is found
|
||||||
verify_auth()
|
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:
|
if OAUTH_CLIENT_ID and OAUTH_CLIENT_SECRET:
|
||||||
logger.info("Both OAuth Client ID and Secret are configured.")
|
logger.info("Both OAuth Client ID and Secret are configured.")
|
||||||
|
|
||||||
|
@ -1,9 +1,5 @@
|
|||||||
import secrets
|
from fastapi import APIRouter, HTTPException
|
||||||
|
|
||||||
from fastapi import APIRouter
|
|
||||||
from fastapi import Depends
|
from fastapi import Depends
|
||||||
from fastapi import Header
|
|
||||||
from fastapi import HTTPException
|
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from danswer.configs.constants import DocumentSource
|
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.db.engine import get_session
|
||||||
from danswer.document_index.document_index_utils import get_both_index_names
|
from danswer.document_index.document_index_utils import get_both_index_names
|
||||||
from danswer.document_index.factory import get_default_document_index
|
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.embedder import DefaultIndexingEmbedder
|
||||||
from danswer.indexing.indexing_pipeline import build_indexing_pipeline
|
from danswer.indexing.indexing_pipeline import build_indexing_pipeline
|
||||||
from danswer.server.danswer_api.models import DocMinimalInfo
|
from danswer.server.danswer_api.models import DocMinimalInfo
|
||||||
from danswer.server.danswer_api.models import IngestionDocument
|
from danswer.server.danswer_api.models import IngestionDocument
|
||||||
from danswer.server.danswer_api.models import IngestionResult
|
from danswer.server.danswer_api.models import IngestionResult
|
||||||
from danswer.utils.logger import setup_logger
|
from danswer.utils.logger import setup_logger
|
||||||
|
from ee.danswer.auth.users import api_key_dep
|
||||||
|
|
||||||
logger = setup_logger()
|
logger = setup_logger()
|
||||||
|
|
||||||
# not using /api to avoid confusion with nginx api path routing
|
# not using /api to avoid confusion with nginx api path routing
|
||||||
router = APIRouter(prefix="/danswer-api")
|
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}")
|
@router.get("/connector-docs/{cc_pair_id}")
|
||||||
def get_docs_by_connector_credential_pair(
|
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 HTTPException
|
||||||
from fastapi import Request
|
from fastapi import Request
|
||||||
from fastapi import status
|
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 AUTH_TYPE
|
||||||
from danswer.configs.app_configs import DISABLE_AUTH
|
from danswer.configs.app_configs import DISABLE_AUTH
|
||||||
from danswer.configs.constants import AuthType
|
from danswer.configs.constants import AuthType
|
||||||
|
from danswer.db.engine import get_session
|
||||||
from danswer.db.models import User
|
from danswer.db.models import User
|
||||||
from danswer.utils.logger import setup_logger
|
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.db.saml import get_saml_account
|
||||||
from ee.danswer.utils.secrets import extract_hashed_cookie
|
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)
|
saml_account = get_saml_account(cookie=saved_cookie, db_session=db_session)
|
||||||
user = saml_account.user if saml_account else None
|
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:
|
if user is None:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
@ -43,3 +53,17 @@ async def double_check_user(
|
|||||||
)
|
)
|
||||||
|
|
||||||
return 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 danswer.utils.variable_functionality import global_version
|
||||||
from ee.danswer.configs.app_configs import OPENID_CONFIG_URL
|
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.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.query_history.api import router as query_history_router
|
||||||
from ee.danswer.server.saml import router as saml_router
|
from ee.danswer.server.saml import router as saml_router
|
||||||
from ee.danswer.server.user_group.api import router as user_group_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
|
# analytics endpoints
|
||||||
application.include_router(analytics_router)
|
application.include_router(analytics_router)
|
||||||
application.include_router(query_history_router)
|
application.include_router(query_history_router)
|
||||||
|
# api key management
|
||||||
|
application.include_router(api_key_router)
|
||||||
|
|
||||||
return application
|
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*",
|
source: "/admin/groups/:path*",
|
||||||
destination: "/ee/admin/groups/:path*",
|
destination: "/ee/admin/groups/:path*",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
source: "/admin/api-key",
|
||||||
|
destination: "/ee/admin/api-key",
|
||||||
|
},
|
||||||
// analytics / audit log pages
|
// analytics / audit log pages
|
||||||
{
|
{
|
||||||
source: "/admin/performance/analytics",
|
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",
|
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