From b27107c18418cfd9c378ebc0a6005f1c2d072950 Mon Sep 17 00:00:00 2001 From: Weves Date: Fri, 25 Aug 2023 12:15:38 -0700 Subject: [PATCH] Add user management page --- backend/danswer/db/users.py | 12 ++++ backend/danswer/main.py | 2 + backend/danswer/server/manage.py | 25 ------- backend/danswer/server/users.py | 45 ++++++++++++ web/src/app/admin/layout.tsx | 29 ++++++-- web/src/app/admin/users/page.tsx | 111 +++++++++++++++++++++++++++++ web/src/components/icons/icons.tsx | 10 ++- 7 files changed, 201 insertions(+), 33 deletions(-) create mode 100644 backend/danswer/db/users.py create mode 100644 backend/danswer/server/users.py create mode 100644 web/src/app/admin/users/page.tsx diff --git a/backend/danswer/db/users.py b/backend/danswer/db/users.py new file mode 100644 index 00000000000..c5f5a5c7309 --- /dev/null +++ b/backend/danswer/db/users.py @@ -0,0 +1,12 @@ +from collections.abc import Sequence + +from sqlalchemy import select +from sqlalchemy.orm import Session + +from danswer.db.models import User + + +def list_users(db_session: Session) -> Sequence[User]: + """List all users. No pagination as of now, as the # of users + is assumed to be relatively small (<< 1 million)""" + return db_session.scalars(select(User)).unique().all() diff --git a/backend/danswer/main.py b/backend/danswer/main.py index 72962ef26b2..fcd4f18dcd5 100644 --- a/backend/danswer/main.py +++ b/backend/danswer/main.py @@ -34,6 +34,7 @@ from danswer.server.health import router as health_router from danswer.server.manage import router as admin_router from danswer.server.search_backend import router as backend_router +from danswer.server.users import router as user_router from danswer.utils.logger import setup_logger @@ -66,6 +67,7 @@ def get_application() -> FastAPI: application.include_router(backend_router) application.include_router(event_processing_router) application.include_router(admin_router) + application.include_router(user_router) application.include_router(health_router) application.include_router( diff --git a/backend/danswer/server/manage.py b/backend/danswer/server/manage.py index 4041d683111..01a67ca33e6 100644 --- a/backend/danswer/server/manage.py +++ b/backend/danswer/server/manage.py @@ -8,11 +8,8 @@ from fastapi import Request from fastapi import Response from fastapi import UploadFile -from fastapi_users.db import SQLAlchemyUserDatabase -from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import Session -from danswer.auth.schemas import UserRole from danswer.auth.users import current_admin_user from danswer.auth.users import current_user from danswer.configs.app_configs import DISABLE_GENERATIVE_AI @@ -20,9 +17,6 @@ from danswer.configs.constants import GEN_AI_API_KEY_STORAGE_KEY from danswer.connectors.file.utils import write_temp_files from danswer.connectors.google_drive.connector_auth import build_service_account_creds -from danswer.connectors.google_drive.connector_auth import ( - DB_CREDENTIALS_DICT_SERVICE_ACCOUNT_KEY, -) from danswer.connectors.google_drive.connector_auth import DB_CREDENTIALS_DICT_TOKEN_KEY from danswer.connectors.google_drive.connector_auth import delete_google_app_cred from danswer.connectors.google_drive.connector_auth import delete_service_account_key @@ -58,7 +52,6 @@ from danswer.db.deletion_attempt import create_deletion_attempt from danswer.db.deletion_attempt import get_deletion_attempts from danswer.db.engine import get_session -from danswer.db.engine import get_sqlalchemy_async_engine from danswer.db.index_attempt import create_index_attempt from danswer.db.index_attempt import get_latest_index_attempts from danswer.db.models import DeletionAttempt @@ -87,7 +80,6 @@ from danswer.server.models import ObjectCreationIdResponse from danswer.server.models import RunConnectorRequest from danswer.server.models import StatusResponse -from danswer.server.models import UserByEmail from danswer.server.models import UserRoleResponse from danswer.utils.logger import setup_logger @@ -101,23 +93,6 @@ """Admin only API endpoints""" -@router.patch("/promote-user-to-admin", response_model=None) -async def promote_admin( - user_email: UserByEmail, user: User = Depends(current_admin_user) -) -> None: - if user.role != UserRole.ADMIN: - raise HTTPException(status_code=401, detail="Unauthorized") - async with AsyncSession(get_sqlalchemy_async_engine()) as asession: - user_db = SQLAlchemyUserDatabase(asession, User) # type: ignore - user_to_promote = await user_db.get_by_email(user_email.user_email) - if not user_to_promote: - raise HTTPException(status_code=404, detail="User not found") - user_to_promote.role = UserRole.ADMIN - asession.add(user_to_promote) - await asession.commit() - return - - @router.get("/admin/connector/google-drive/app-credential") def check_google_app_credentials_exist( _: User = Depends(current_admin_user), diff --git a/backend/danswer/server/users.py b/backend/danswer/server/users.py new file mode 100644 index 00000000000..b9e3f7e6dde --- /dev/null +++ b/backend/danswer/server/users.py @@ -0,0 +1,45 @@ +from fastapi import APIRouter +from fastapi import Depends +from fastapi import HTTPException +from fastapi_users.db import SQLAlchemyUserDatabase +from fastapi_users_db_sqlalchemy import UUID_ID +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.orm import Session + +from danswer.auth.schemas import UserRead +from danswer.auth.schemas import UserRole +from danswer.auth.users import current_admin_user +from danswer.db.engine import get_session +from danswer.db.engine import get_sqlalchemy_async_engine +from danswer.db.models import User +from danswer.db.users import list_users +from danswer.server.models import UserByEmail + + +router = APIRouter(prefix="/manage") + + +@router.patch("/promote-user-to-admin") +async def promote_admin( + user_email: UserByEmail, user: User = Depends(current_admin_user) +) -> None: + if user.role != UserRole.ADMIN: + raise HTTPException(status_code=401, detail="Unauthorized") + async with AsyncSession(get_sqlalchemy_async_engine()) as asession: + user_db = SQLAlchemyUserDatabase[User, UUID_ID](asession, User) + user_to_promote = await user_db.get_by_email(user_email.user_email) + if not user_to_promote: + raise HTTPException(status_code=404, detail="User not found") + user_to_promote.role = UserRole.ADMIN + asession.add(user_to_promote) + await asession.commit() + return + + +@router.get("/users") +def list_all_users( + _: User | None = Depends(current_admin_user), + db_session: Session = Depends(get_session), +) -> list[UserRead]: + users = list_users(db_session) + return [UserRead.from_orm(user) for user in users] diff --git a/web/src/app/admin/layout.tsx b/web/src/app/admin/layout.tsx index 1fc8fe132f9..761b3dd04f4 100644 --- a/web/src/app/admin/layout.tsx +++ b/web/src/app/admin/layout.tsx @@ -17,6 +17,7 @@ import { ZulipIcon, ProductboardIcon, LinearIcon, + UsersIcon, } from "@/components/icons/icons"; import { DISABLE_AUTH } from "@/lib/constants"; import { getCurrentUserSS } from "@/lib/userSS"; @@ -161,6 +162,15 @@ export default async function AdminLayout({ ), link: "/admin/connectors/bookstack", }, + { + name: ( +
+ +
Zulip
+
+ ), + link: "/admin/connectors/zulip", + }, { name: (
@@ -179,28 +189,33 @@ export default async function AdminLayout({ ), link: "/admin/connectors/file", }, + ], + }, + { + name: "Keys", + items: [ { name: (
- -
Zulip
+ +
OpenAI
), - link: "/admin/connectors/zulip", + link: "/admin/keys/openai", }, ], }, { - name: "Keys", + name: "User Management", items: [ { name: (
- -
OpenAI
+ +
Users
), - link: "/admin/keys/openai", + link: "/admin/users", }, ], }, diff --git a/web/src/app/admin/users/page.tsx b/web/src/app/admin/users/page.tsx new file mode 100644 index 00000000000..a7a388061f1 --- /dev/null +++ b/web/src/app/admin/users/page.tsx @@ -0,0 +1,111 @@ +"use client"; + +import { Button } from "@/components/Button"; +import { LoadingAnimation } from "@/components/Loading"; +import { BasicTable } from "@/components/admin/connectors/BasicTable"; +import { Popup, usePopup } from "@/components/admin/connectors/Popup"; +import { KeyIcon, TrashIcon, UsersIcon } from "@/components/icons/icons"; +import { ApiKeyForm } from "@/components/openai/ApiKeyForm"; +import { GEN_AI_API_KEY_URL } from "@/components/openai/constants"; +import { fetcher } from "@/lib/fetcher"; +import { User } from "@/lib/types"; +import { useState } from "react"; +import useSWR, { mutate } from "swr"; + +const columns = [ + { + header: "Email", + key: "email", + }, + { + header: "Role", + key: "role", + }, + { + header: "Promote", + key: "promote", + }, +]; + +const UsersTable = () => { + const { popup, setPopup } = usePopup(); + + const { data, isLoading, error } = useSWR( + "/api/manage/users", + fetcher + ); + + if (isLoading) { + return ; + } + + if (error || !data) { + return
Error loading users
; + } + + return ( +
+ {popup} + { + return { + email: user.email, + role: {user.role === "admin" ? "Admin" : "User"}, + promote: + user.role !== "admin" ? ( + + ) : ( + "" + ), + }; + })} + /> +
+ ); +}; + +const Page = () => { + return ( +
+
+ +

Manage Users

+
+ + +
+ ); +}; + +export default Page; diff --git a/web/src/components/icons/icons.tsx b/web/src/components/icons/icons.tsx index f34bc8b5b22..60ff4eeb9ff 100644 --- a/web/src/components/icons/icons.tsx +++ b/web/src/components/icons/icons.tsx @@ -13,6 +13,7 @@ import { PencilSimple, X, Question, + Users, } from "@phosphor-icons/react"; import { SiBookstack } from "react-icons/si"; import { FaFile, FaGlobe } from "react-icons/fa"; @@ -51,6 +52,13 @@ export const KeyIcon = ({ return ; }; +export const UsersIcon = ({ + size = 16, + className = defaultTailwindCSS, +}: IconProps) => { + return ; +}; + export const TrashIcon = ({ size = 16, className = defaultTailwindCSS, @@ -217,7 +225,7 @@ export const ZulipIcon = ({ return (
Logo