Skip to content

Commit

Permalink
Add user management page
Browse files Browse the repository at this point in the history
  • Loading branch information
Weves committed Aug 25, 2023
1 parent 8cda11c commit b27107c
Show file tree
Hide file tree
Showing 7 changed files with 201 additions and 33 deletions.
12 changes: 12 additions & 0 deletions backend/danswer/db/users.py
Original file line number Diff line number Diff line change
@@ -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()
2 changes: 2 additions & 0 deletions backend/danswer/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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(
Expand Down
25 changes: 0 additions & 25 deletions backend/danswer/server/manage.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,15 @@
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
from danswer.configs.app_configs import GENERATIVE_MODEL_ACCESS_CHECK_FREQ
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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand All @@ -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),
Expand Down
45 changes: 45 additions & 0 deletions backend/danswer/server/users.py
Original file line number Diff line number Diff line change
@@ -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]
29 changes: 22 additions & 7 deletions web/src/app/admin/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
ZulipIcon,
ProductboardIcon,
LinearIcon,
UsersIcon,
} from "@/components/icons/icons";
import { DISABLE_AUTH } from "@/lib/constants";
import { getCurrentUserSS } from "@/lib/userSS";
Expand Down Expand Up @@ -161,6 +162,15 @@ export default async function AdminLayout({
),
link: "/admin/connectors/bookstack",
},
{
name: (
<div className="flex">
<ZulipIcon size={16} />
<div className="ml-1">Zulip</div>
</div>
),
link: "/admin/connectors/zulip",
},
{
name: (
<div className="flex">
Expand All @@ -179,28 +189,33 @@ export default async function AdminLayout({
),
link: "/admin/connectors/file",
},
],
},
{
name: "Keys",
items: [
{
name: (
<div className="flex">
<ZulipIcon size={16} />
<div className="ml-1">Zulip</div>
<KeyIcon size={18} />
<div className="ml-1">OpenAI</div>
</div>
),
link: "/admin/connectors/zulip",
link: "/admin/keys/openai",
},
],
},
{
name: "Keys",
name: "User Management",
items: [
{
name: (
<div className="flex">
<KeyIcon size={18} />
<div className="ml-1">OpenAI</div>
<UsersIcon size={18} />
<div className="ml-1">Users</div>
</div>
),
link: "/admin/keys/openai",
link: "/admin/users",
},
],
},
Expand Down
111 changes: 111 additions & 0 deletions web/src/app/admin/users/page.tsx
Original file line number Diff line number Diff line change
@@ -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<User[]>(
"/api/manage/users",
fetcher
);

if (isLoading) {
return <LoadingAnimation text="Loading" />;
}

if (error || !data) {
return <div className="text-red-600">Error loading users</div>;
}

return (
<div>
{popup}
<BasicTable
columns={columns}
data={data.map((user) => {
return {
email: user.email,
role: <i>{user.role === "admin" ? "Admin" : "User"}</i>,
promote:
user.role !== "admin" ? (
<Button
onClick={async () => {
const res = await fetch(
"/api/manage/promote-user-to-admin",
{
method: "PATCH",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
user_email: user.email,
}),
}
);
if (!res.ok) {
const errorMsg = await res.text();
setPopup({
message: `Unable to promote user - ${errorMsg}`,
type: "error",
});
} else {
mutate("/api/manage/users");
setPopup({
message: "User promoted to admin!",
type: "success",
});
}
}}
>
Promote to Admin!
</Button>
) : (
""
),
};
})}
/>
</div>
);
};

const Page = () => {
return (
<div>
<div className="border-solid border-gray-600 border-b pb-2 mb-4 flex">
<UsersIcon size={32} />
<h1 className="text-3xl font-bold pl-2">Manage Users</h1>
</div>

<UsersTable />
</div>
);
};

export default Page;
10 changes: 9 additions & 1 deletion web/src/components/icons/icons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -51,6 +52,13 @@ export const KeyIcon = ({
return <Key size={size} className={className} />;
};

export const UsersIcon = ({
size = 16,
className = defaultTailwindCSS,
}: IconProps) => {
return <Users size={size} className={className} />;
};

export const TrashIcon = ({
size = 16,
className = defaultTailwindCSS,
Expand Down Expand Up @@ -217,7 +225,7 @@ export const ZulipIcon = ({
return (
<div
style={{ width: `${size}px`, height: `${size}px` }}
className={`w-[${size}px] h-[${size}px] -m-0.5 ` + className}
className={`w-[${size}px] h-[${size}px] ` + className}
>
<Image src={zulipIcon} alt="Logo" width="96" height="96" />
</div>
Expand Down

1 comment on commit b27107c

@vercel
Copy link

@vercel vercel bot commented on b27107c Aug 25, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.