mirror of
https://github.com/danswer-ai/danswer.git
synced 2025-09-23 12:31:30 +02:00
Add user management page
This commit is contained in:
12
backend/danswer/db/users.py
Normal file
12
backend/danswer/db/users.py
Normal file
@@ -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()
|
@@ -34,6 +34,7 @@ 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
|
||||
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(
|
||||
|
@@ -8,11 +8,8 @@ from fastapi import HTTPException
|
||||
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.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
|
||||
@@ -58,7 +52,6 @@ from danswer.db.deletion_attempt import check_deletion_attempt_is_allowed
|
||||
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 IndexAttemptSnapshot
|
||||
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 @@ _GOOGLE_DRIVE_CREDENTIAL_ID_COOKIE_NAME = "google_drive_credential_id"
|
||||
"""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),
|
||||
|
45
backend/danswer/server/users.py
Normal file
45
backend/danswer/server/users.py
Normal file
@@ -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]
|
@@ -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: (
|
||||
<div className="flex">
|
||||
<ZulipIcon size={16} />
|
||||
<div className="ml-1">Zulip</div>
|
||||
</div>
|
||||
),
|
||||
link: "/admin/connectors/zulip",
|
||||
},
|
||||
{
|
||||
name: (
|
||||
<div className="flex">
|
||||
@@ -179,15 +189,6 @@ export default async function AdminLayout({
|
||||
),
|
||||
link: "/admin/connectors/file",
|
||||
},
|
||||
{
|
||||
name: (
|
||||
<div className="flex">
|
||||
<ZulipIcon size={16} />
|
||||
<div className="ml-1">Zulip</div>
|
||||
</div>
|
||||
),
|
||||
link: "/admin/connectors/zulip",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -204,6 +205,20 @@ export default async function AdminLayout({
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "User Management",
|
||||
items: [
|
||||
{
|
||||
name: (
|
||||
<div className="flex">
|
||||
<UsersIcon size={18} />
|
||||
<div className="ml-1">Users</div>
|
||||
</div>
|
||||
),
|
||||
link: "/admin/users",
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<div className="px-12 min-h-screen bg-gray-900 text-gray-100 w-full">
|
||||
|
111
web/src/app/admin/users/page.tsx
Normal file
111
web/src/app/admin/users/page.tsx
Normal file
@@ -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;
|
@@ -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 <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,
|
||||
@@ -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>
|
||||
|
Reference in New Issue
Block a user