mirror of
https://github.com/danswer-ai/danswer.git
synced 2025-10-10 05:05:34 +02:00
Cleanup user management
This commit is contained in:
@@ -10,8 +10,6 @@ from fastapi import Depends
|
|||||||
from fastapi import HTTPException
|
from fastapi import HTTPException
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from danswer.auth.invited_users import get_invited_users
|
|
||||||
from danswer.auth.invited_users import write_invited_users
|
|
||||||
from danswer.auth.users import current_admin_user
|
from danswer.auth.users import current_admin_user
|
||||||
from danswer.configs.app_configs import GENERATIVE_MODEL_ACCESS_CHECK_FREQ
|
from danswer.configs.app_configs import GENERATIVE_MODEL_ACCESS_CHECK_FREQ
|
||||||
from danswer.configs.app_configs import TOKEN_BUDGET_GLOBALLY_ENABLED
|
from danswer.configs.app_configs import TOKEN_BUDGET_GLOBALLY_ENABLED
|
||||||
@@ -28,7 +26,6 @@ from danswer.db.feedback import update_document_boost
|
|||||||
from danswer.db.feedback import update_document_hidden
|
from danswer.db.feedback import update_document_hidden
|
||||||
from danswer.db.index_attempt import cancel_indexing_attempts_for_connector
|
from danswer.db.index_attempt import cancel_indexing_attempts_for_connector
|
||||||
from danswer.db.models import User
|
from danswer.db.models import User
|
||||||
from danswer.db.users import get_user_by_email
|
|
||||||
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.factory import get_dynamic_config_store
|
||||||
@@ -40,7 +37,6 @@ from danswer.server.documents.models import ConnectorCredentialPairIdentifier
|
|||||||
from danswer.server.manage.models import BoostDoc
|
from danswer.server.manage.models import BoostDoc
|
||||||
from danswer.server.manage.models import BoostUpdateRequest
|
from danswer.server.manage.models import BoostUpdateRequest
|
||||||
from danswer.server.manage.models import HiddenUpdateRequest
|
from danswer.server.manage.models import HiddenUpdateRequest
|
||||||
from danswer.server.manage.models import UserByEmail
|
|
||||||
from danswer.utils.logger import setup_logger
|
from danswer.utils.logger import setup_logger
|
||||||
|
|
||||||
router = APIRouter(prefix="/manage")
|
router = APIRouter(prefix="/manage")
|
||||||
@@ -235,66 +231,3 @@ def update_token_budget_settings(
|
|||||||
# Store the settings in the dynamic config store
|
# Store the settings in the dynamic config store
|
||||||
get_dynamic_config_store().store(TOKEN_BUDGET_SETTINGS, settings_json)
|
get_dynamic_config_store().store(TOKEN_BUDGET_SETTINGS, settings_json)
|
||||||
return {"message": "Token budget settings updated successfully."}
|
return {"message": "Token budget settings updated successfully."}
|
||||||
|
|
||||||
|
|
||||||
@router.put("/admin/users")
|
|
||||||
def bulk_invite_users(
|
|
||||||
emails: list[str] = Body(..., embed=True),
|
|
||||||
_: User | None = Depends(current_admin_user),
|
|
||||||
) -> int:
|
|
||||||
all_emails = list(set(emails) | set(get_invited_users()))
|
|
||||||
return write_invited_users(all_emails)
|
|
||||||
|
|
||||||
|
|
||||||
@router.patch("/admin/remove-invited-user")
|
|
||||||
def remove_invited_user(
|
|
||||||
user_email: UserByEmail,
|
|
||||||
_: User | None = Depends(current_admin_user),
|
|
||||||
) -> int:
|
|
||||||
user_emails = get_invited_users()
|
|
||||||
remaining_users = [user for user in user_emails if user != user_email.user_email]
|
|
||||||
return write_invited_users(remaining_users)
|
|
||||||
|
|
||||||
|
|
||||||
@router.patch("/admin/deactivate-user")
|
|
||||||
def deactivate_user(
|
|
||||||
user_email: UserByEmail,
|
|
||||||
current_user: User = Depends(current_admin_user),
|
|
||||||
db_session: Session = Depends(get_session),
|
|
||||||
) -> None:
|
|
||||||
if current_user.email == user_email.user_email:
|
|
||||||
raise HTTPException(status_code=400, detail="You cannot deactivate yourself")
|
|
||||||
|
|
||||||
user_to_deactivate = get_user_by_email(
|
|
||||||
email=user_email.user_email, db_session=db_session
|
|
||||||
)
|
|
||||||
|
|
||||||
if not user_to_deactivate:
|
|
||||||
raise HTTPException(status_code=404, detail="User not found")
|
|
||||||
|
|
||||||
if user_to_deactivate.is_active is False:
|
|
||||||
logger.warning("{} is already deactivated".format(user_to_deactivate.email))
|
|
||||||
|
|
||||||
user_to_deactivate.is_active = False
|
|
||||||
db_session.add(user_to_deactivate)
|
|
||||||
db_session.commit()
|
|
||||||
|
|
||||||
|
|
||||||
@router.patch("/admin/activate-user")
|
|
||||||
def activate_user(
|
|
||||||
user_email: UserByEmail,
|
|
||||||
_: User | None = Depends(current_admin_user),
|
|
||||||
db_session: Session = Depends(get_session),
|
|
||||||
) -> None:
|
|
||||||
user_to_activate = get_user_by_email(
|
|
||||||
email=user_email.user_email, db_session=db_session
|
|
||||||
)
|
|
||||||
if not user_to_activate:
|
|
||||||
raise HTTPException(status_code=404, detail="User not found")
|
|
||||||
|
|
||||||
if user_to_activate.is_active is True:
|
|
||||||
logger.warning("{} is already activated".format(user_to_activate.email))
|
|
||||||
|
|
||||||
user_to_activate.is_active = True
|
|
||||||
db_session.add(user_to_activate)
|
|
||||||
db_session.commit()
|
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
import re
|
import re
|
||||||
|
|
||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
|
from fastapi import Body
|
||||||
from fastapi import Depends
|
from fastapi import Depends
|
||||||
from fastapi import HTTPException
|
from fastapi import HTTPException
|
||||||
from fastapi import status
|
from fastapi import status
|
||||||
@@ -9,6 +10,7 @@ from sqlalchemy import update
|
|||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from danswer.auth.invited_users import get_invited_users
|
from danswer.auth.invited_users import get_invited_users
|
||||||
|
from danswer.auth.invited_users import write_invited_users
|
||||||
from danswer.auth.noauth_user import fetch_no_auth_user
|
from danswer.auth.noauth_user import fetch_no_auth_user
|
||||||
from danswer.auth.noauth_user import set_no_auth_user_preferences
|
from danswer.auth.noauth_user import set_no_auth_user_preferences
|
||||||
from danswer.auth.schemas import UserRole
|
from danswer.auth.schemas import UserRole
|
||||||
@@ -17,6 +19,7 @@ from danswer.auth.users import current_admin_user
|
|||||||
from danswer.auth.users import current_user
|
from danswer.auth.users import current_user
|
||||||
from danswer.auth.users import optional_user
|
from danswer.auth.users import optional_user
|
||||||
from danswer.configs.app_configs import AUTH_TYPE
|
from danswer.configs.app_configs import AUTH_TYPE
|
||||||
|
from danswer.configs.app_configs import VALID_EMAIL_DOMAINS
|
||||||
from danswer.configs.constants import AuthType
|
from danswer.configs.constants import AuthType
|
||||||
from danswer.db.engine import get_session
|
from danswer.db.engine import get_session
|
||||||
from danswer.db.models import User
|
from danswer.db.models import User
|
||||||
@@ -30,9 +33,14 @@ from danswer.server.manage.models import UserRoleResponse
|
|||||||
from danswer.server.models import FullUserSnapshot
|
from danswer.server.models import FullUserSnapshot
|
||||||
from danswer.server.models import InvitedUserSnapshot
|
from danswer.server.models import InvitedUserSnapshot
|
||||||
from danswer.server.models import MinimalUserSnapshot
|
from danswer.server.models import MinimalUserSnapshot
|
||||||
|
from danswer.utils.logger import setup_logger
|
||||||
|
|
||||||
|
logger = setup_logger()
|
||||||
|
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
USERS_PAGE_SIZE = 10
|
USERS_PAGE_SIZE = 10
|
||||||
|
|
||||||
|
|
||||||
@@ -104,16 +112,94 @@ def list_all_users(
|
|||||||
)
|
)
|
||||||
for user in users
|
for user in users
|
||||||
][accepted_page * USERS_PAGE_SIZE : (accepted_page + 1) * USERS_PAGE_SIZE],
|
][accepted_page * USERS_PAGE_SIZE : (accepted_page + 1) * USERS_PAGE_SIZE],
|
||||||
invited=[
|
invited=[InvitedUserSnapshot(email=email) for email in invited_emails][
|
||||||
InvitedUserSnapshot(email=email)
|
invited_page * USERS_PAGE_SIZE : (invited_page + 1) * USERS_PAGE_SIZE
|
||||||
for email in invited_emails
|
],
|
||||||
if email not in accepted_emails
|
|
||||||
][invited_page * USERS_PAGE_SIZE : (invited_page + 1) * USERS_PAGE_SIZE],
|
|
||||||
accepted_pages=accepted_count // USERS_PAGE_SIZE + 1,
|
accepted_pages=accepted_count // USERS_PAGE_SIZE + 1,
|
||||||
invited_pages=invited_count // USERS_PAGE_SIZE + 1,
|
invited_pages=invited_count // USERS_PAGE_SIZE + 1,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/manage/admin/users")
|
||||||
|
def bulk_invite_users(
|
||||||
|
emails: list[str] = Body(..., embed=True),
|
||||||
|
current_user: User | None = Depends(current_admin_user),
|
||||||
|
) -> int:
|
||||||
|
if current_user is None:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400, detail="Auth is disabled, cannot invite users"
|
||||||
|
)
|
||||||
|
|
||||||
|
all_emails = list(set(emails) | set(get_invited_users()))
|
||||||
|
return write_invited_users(all_emails)
|
||||||
|
|
||||||
|
|
||||||
|
@router.patch("/manage/admin/remove-invited-user")
|
||||||
|
def remove_invited_user(
|
||||||
|
user_email: UserByEmail,
|
||||||
|
_: User | None = Depends(current_admin_user),
|
||||||
|
) -> int:
|
||||||
|
user_emails = get_invited_users()
|
||||||
|
remaining_users = [user for user in user_emails if user != user_email.user_email]
|
||||||
|
return write_invited_users(remaining_users)
|
||||||
|
|
||||||
|
|
||||||
|
@router.patch("/manage/admin/deactivate-user")
|
||||||
|
def deactivate_user(
|
||||||
|
user_email: UserByEmail,
|
||||||
|
current_user: User | None = Depends(current_admin_user),
|
||||||
|
db_session: Session = Depends(get_session),
|
||||||
|
) -> None:
|
||||||
|
if current_user is None:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400, detail="Auth is disabled, cannot deactivate user"
|
||||||
|
)
|
||||||
|
|
||||||
|
if current_user.email == user_email.user_email:
|
||||||
|
raise HTTPException(status_code=400, detail="You cannot deactivate yourself")
|
||||||
|
|
||||||
|
user_to_deactivate = get_user_by_email(
|
||||||
|
email=user_email.user_email, db_session=db_session
|
||||||
|
)
|
||||||
|
|
||||||
|
if not user_to_deactivate:
|
||||||
|
raise HTTPException(status_code=404, detail="User not found")
|
||||||
|
|
||||||
|
if user_to_deactivate.is_active is False:
|
||||||
|
logger.warning("{} is already deactivated".format(user_to_deactivate.email))
|
||||||
|
|
||||||
|
user_to_deactivate.is_active = False
|
||||||
|
db_session.add(user_to_deactivate)
|
||||||
|
db_session.commit()
|
||||||
|
|
||||||
|
|
||||||
|
@router.patch("/manage/admin/activate-user")
|
||||||
|
def activate_user(
|
||||||
|
user_email: UserByEmail,
|
||||||
|
_: User | None = Depends(current_admin_user),
|
||||||
|
db_session: Session = Depends(get_session),
|
||||||
|
) -> None:
|
||||||
|
user_to_activate = get_user_by_email(
|
||||||
|
email=user_email.user_email, db_session=db_session
|
||||||
|
)
|
||||||
|
if not user_to_activate:
|
||||||
|
raise HTTPException(status_code=404, detail="User not found")
|
||||||
|
|
||||||
|
if user_to_activate.is_active is True:
|
||||||
|
logger.warning("{} is already activated".format(user_to_activate.email))
|
||||||
|
|
||||||
|
user_to_activate.is_active = True
|
||||||
|
db_session.add(user_to_activate)
|
||||||
|
db_session.commit()
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/manage/admin/valid-domains")
|
||||||
|
def get_valid_domains(
|
||||||
|
_: User | None = Depends(current_admin_user),
|
||||||
|
) -> list[str]:
|
||||||
|
return VALID_EMAIL_DOMAINS
|
||||||
|
|
||||||
|
|
||||||
"""Endpoints for all"""
|
"""Endpoints for all"""
|
||||||
|
|
||||||
|
|
||||||
|
@@ -4,19 +4,9 @@ import SignedUpUserTable from "@/components/admin/users/SignedUpUserTable";
|
|||||||
import { SearchBar } from "@/components/search/SearchBar";
|
import { SearchBar } from "@/components/search/SearchBar";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { FiPlusSquare } from "react-icons/fi";
|
import { FiPlusSquare } from "react-icons/fi";
|
||||||
import Link from "next/link";
|
|
||||||
import { Modal } from "@/components/Modal";
|
import { Modal } from "@/components/Modal";
|
||||||
|
|
||||||
import {
|
import { Button, Text } from "@tremor/react";
|
||||||
Table,
|
|
||||||
TableHead,
|
|
||||||
TableRow,
|
|
||||||
TableHeaderCell,
|
|
||||||
TableBody,
|
|
||||||
TableCell,
|
|
||||||
Button,
|
|
||||||
Text,
|
|
||||||
} from "@tremor/react";
|
|
||||||
import { LoadingAnimation } from "@/components/Loading";
|
import { LoadingAnimation } from "@/components/Loading";
|
||||||
import { AdminPageTitle } from "@/components/admin/Title";
|
import { AdminPageTitle } from "@/components/admin/Title";
|
||||||
import { usePopup, PopupSpec } from "@/components/admin/connectors/Popup";
|
import { usePopup, PopupSpec } from "@/components/admin/connectors/Popup";
|
||||||
@@ -24,11 +14,43 @@ import { UsersIcon } from "@/components/icons/icons";
|
|||||||
import { errorHandlingFetcher } from "@/lib/fetcher";
|
import { errorHandlingFetcher } from "@/lib/fetcher";
|
||||||
import { type User, UserStatus } from "@/lib/types";
|
import { type User, UserStatus } from "@/lib/types";
|
||||||
import useSWR, { mutate } from "swr";
|
import useSWR, { mutate } from "swr";
|
||||||
import useSWRMutation from "swr/mutation";
|
|
||||||
import { ErrorCallout } from "@/components/ErrorCallout";
|
import { ErrorCallout } from "@/components/ErrorCallout";
|
||||||
import { HidableSection } from "@/app/admin/assistants/HidableSection";
|
import { HidableSection } from "@/app/admin/assistants/HidableSection";
|
||||||
import BulkAdd from "@/components/admin/users/BulkAdd";
|
import BulkAdd from "@/components/admin/users/BulkAdd";
|
||||||
|
|
||||||
|
const ValidDomainsDisplay = ({ validDomains }: { validDomains: string[] }) => {
|
||||||
|
if (!validDomains.length) {
|
||||||
|
return (
|
||||||
|
<div className="text-sm">
|
||||||
|
No invited users. Anyone can sign up with a valid email address. To
|
||||||
|
restrict access you can:
|
||||||
|
<div className="flex flex-wrap ml-2 mt-1">
|
||||||
|
(1) Invite users above. Once a user has been invited, only emails that
|
||||||
|
have explicitly been invited will be able to sign-up.
|
||||||
|
</div>
|
||||||
|
<div className="mt-1 ml-2">
|
||||||
|
(2) Set the{" "}
|
||||||
|
<b className="font-mono w-fit h-fit">VALID_EMAIL_DOMAINS</b>{" "}
|
||||||
|
environment variable to a comma separated list of email domains. This
|
||||||
|
will restrict access to users with email addresses from these domains.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="text-sm">
|
||||||
|
No invited users. Anyone with an email address with any of the following
|
||||||
|
domains can sign up: <i>{validDomains.join(", ")}</i>.
|
||||||
|
<div className="mt-2">
|
||||||
|
To further restrict access you can invite users above. Once a user has
|
||||||
|
been invited, only emails that have explicitly been invited will be able
|
||||||
|
to sign-up.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
interface UsersResponse {
|
interface UsersResponse {
|
||||||
accepted: User[];
|
accepted: User[];
|
||||||
invited: User[];
|
invited: User[];
|
||||||
@@ -46,11 +68,18 @@ const UsersTables = ({
|
|||||||
const [invitedPage, setInvitedPage] = useState(1);
|
const [invitedPage, setInvitedPage] = useState(1);
|
||||||
const [acceptedPage, setAcceptedPage] = useState(1);
|
const [acceptedPage, setAcceptedPage] = useState(1);
|
||||||
const { data, isLoading, mutate, error } = useSWR<UsersResponse>(
|
const { data, isLoading, mutate, error } = useSWR<UsersResponse>(
|
||||||
`/api/manage/users?q=${encodeURI(q)}&accepted_page=${acceptedPage - 1}&invited_page=${invitedPage - 1}`,
|
`/api/manage/users?q=${encodeURI(q)}&accepted_page=${
|
||||||
|
acceptedPage - 1
|
||||||
|
}&invited_page=${invitedPage - 1}`,
|
||||||
errorHandlingFetcher
|
errorHandlingFetcher
|
||||||
);
|
);
|
||||||
|
const {
|
||||||
|
data: validDomains,
|
||||||
|
isLoading: isLoadingDomains,
|
||||||
|
error: domainsError,
|
||||||
|
} = useSWR<string[]>("/api/manage/admin/valid-domains", errorHandlingFetcher);
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading || isLoadingDomains) {
|
||||||
return <LoadingAnimation text="Loading" />;
|
return <LoadingAnimation text="Loading" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,18 +92,45 @@ const UsersTables = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (domainsError || !validDomains) {
|
||||||
|
return (
|
||||||
|
<ErrorCallout
|
||||||
|
errorTitle="Error loading valid domains"
|
||||||
|
errorMsg={domainsError?.info?.detail}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const { accepted, invited, accepted_pages, invited_pages } = data;
|
const { accepted, invited, accepted_pages, invited_pages } = data;
|
||||||
|
|
||||||
|
// remove users that are already accepted
|
||||||
|
const finalInvited = invited.filter(
|
||||||
|
(user) => !accepted.map((u) => u.email).includes(user.email)
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<InvitedUserTable
|
<HidableSection sectionTitle="Invited Users">
|
||||||
users={invited}
|
{invited.length > 0 ? (
|
||||||
setPopup={setPopup}
|
finalInvited.length > 0 ? (
|
||||||
currentPage={invitedPage}
|
<InvitedUserTable
|
||||||
onPageChange={setInvitedPage}
|
users={finalInvited}
|
||||||
totalPages={invited_pages}
|
setPopup={setPopup}
|
||||||
mutate={mutate}
|
currentPage={invitedPage}
|
||||||
/>
|
onPageChange={setInvitedPage}
|
||||||
|
totalPages={invited_pages}
|
||||||
|
mutate={mutate}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="text-sm">
|
||||||
|
To invite additional teammates, use the <b>Invite Users</b> button
|
||||||
|
above!
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<ValidDomainsDisplay validDomains={validDomains} />
|
||||||
|
)}
|
||||||
|
</HidableSection>
|
||||||
<SignedUpUserTable
|
<SignedUpUserTable
|
||||||
users={accepted}
|
users={accepted}
|
||||||
setPopup={setPopup}
|
setPopup={setPopup}
|
||||||
@@ -129,6 +185,13 @@ const AddUserButton = ({
|
|||||||
type: "success",
|
type: "success",
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
const onFailure = async (res: Response) => {
|
||||||
|
const error = (await res.json()).detail;
|
||||||
|
setPopup({
|
||||||
|
message: `Failed to invite users - ${error}`,
|
||||||
|
type: "error",
|
||||||
|
});
|
||||||
|
};
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Button className="w-fit" onClick={() => setModal(true)}>
|
<Button className="w-fit" onClick={() => setModal(true)}>
|
||||||
@@ -143,7 +206,7 @@ const AddUserButton = ({
|
|||||||
<Text className="font-medium text-base">
|
<Text className="font-medium text-base">
|
||||||
Add the email addresses to import, separated by whitespaces.
|
Add the email addresses to import, separated by whitespaces.
|
||||||
</Text>
|
</Text>
|
||||||
<BulkAdd onSuccess={onSuccess} />
|
<BulkAdd onSuccess={onSuccess} onFailure={onFailure} />
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
)}
|
)}
|
||||||
|
@@ -1,6 +1,21 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Button } from "@tremor/react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { FiLogIn } from "react-icons/fi";
|
||||||
|
|
||||||
const Page = () => {
|
const Page = () => {
|
||||||
return (
|
return (
|
||||||
<div>Unable to login, please try again and/or contact an administrator</div>
|
<div className="flex flex-col items-center justify-center h-screen">
|
||||||
|
<div className="font-bold">
|
||||||
|
Unable to login, please try again and/or contact an administrator.
|
||||||
|
</div>
|
||||||
|
<Link href="/auth/login" className="w-fit">
|
||||||
|
<Button className="mt-4" size="xs" icon={FiLogIn}>
|
||||||
|
Back to login
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -7,8 +7,8 @@ export interface PopupSpec {
|
|||||||
|
|
||||||
export const Popup: React.FC<PopupSpec> = ({ message, type }) => (
|
export const Popup: React.FC<PopupSpec> = ({ message, type }) => (
|
||||||
<div
|
<div
|
||||||
className={`fixed bottom-4 left-4 p-4 rounded-md shadow-lg text-white z-30 ${
|
className={`fixed bottom-4 left-4 p-4 rounded-md shadow-lg text-white z-[100] ${
|
||||||
type === "success" ? "bg-green-500" : "bg-red-500"
|
type === "success" ? "bg-green-500" : "bg-error"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{message}
|
{message}
|
||||||
|
@@ -1,13 +1,8 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import useSWRMutation from "swr/mutation";
|
|
||||||
import { RobotIcon } from "@/components/icons/icons";
|
|
||||||
import { withFormik, FormikProps, FormikErrors, Form, Field } from "formik";
|
import { withFormik, FormikProps, FormikErrors, Form, Field } from "formik";
|
||||||
|
|
||||||
import { BackButton } from "@/components/BackButton";
|
import { Button } from "@tremor/react";
|
||||||
import { Card } from "@tremor/react";
|
|
||||||
import { AdminPageTitle } from "@/components/admin/Title";
|
|
||||||
import { fetchAssistantEditorInfoSS } from "@/lib/assistants/fetchPersonaEditorInfoSS";
|
|
||||||
import { Button, Text } from "@tremor/react";
|
|
||||||
|
|
||||||
const WHITESPACE_SPLIT = /\s+/;
|
const WHITESPACE_SPLIT = /\s+/;
|
||||||
const EMAIL_REGEX = /[^@]+@[^.]+\.[^.]/;
|
const EMAIL_REGEX = /[^@]+@[^.]+\.[^.]/;
|
||||||
@@ -24,6 +19,7 @@ const addUsers = async (url: string, { arg }: { arg: Array<string> }) => {
|
|||||||
|
|
||||||
interface FormProps {
|
interface FormProps {
|
||||||
onSuccess: () => void;
|
onSuccess: () => void;
|
||||||
|
onFailure: (res: Response) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FormValues {
|
interface FormValues {
|
||||||
@@ -77,13 +73,15 @@ const AddUserForm = withFormik<FormProps, FormValues>({
|
|||||||
await addUsers("/api/manage/admin/users", { arg: emails }).then((res) => {
|
await addUsers("/api/manage/admin/users", { arg: emails }).then((res) => {
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
formikBag.props.onSuccess();
|
formikBag.props.onSuccess();
|
||||||
|
} else {
|
||||||
|
formikBag.props.onFailure(res);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
})(AddUserFormRenderer);
|
})(AddUserFormRenderer);
|
||||||
|
|
||||||
const BulkAdd = ({ onSuccess }: FormProps) => {
|
const BulkAdd = ({ onSuccess, onFailure }: FormProps) => {
|
||||||
return <AddUserForm onSuccess={onSuccess} />;
|
return <AddUserForm onSuccess={onSuccess} onFailure={onFailure} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default BulkAdd;
|
export default BulkAdd;
|
||||||
|
@@ -68,43 +68,41 @@ const InvitedUserTable = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HidableSection sectionTitle="Invited Users">
|
<>
|
||||||
<>
|
<Table className="overflow-visible">
|
||||||
{totalPages > 1 ? (
|
<TableHead>
|
||||||
<CenteredPageSelector
|
<TableRow>
|
||||||
currentPage={currentPage}
|
<TableHeaderCell>Email</TableHeaderCell>
|
||||||
totalPages={totalPages}
|
<TableHeaderCell>
|
||||||
onPageChange={onPageChange}
|
<div className="flex justify-end">Actions</div>
|
||||||
/>
|
</TableHeaderCell>
|
||||||
) : null}
|
</TableRow>
|
||||||
<Table className="overflow-visible">
|
</TableHead>
|
||||||
<TableHead>
|
<TableBody>
|
||||||
<TableRow>
|
{users.map((user) => (
|
||||||
<TableHeaderCell>Email</TableHeaderCell>
|
<TableRow key={user.email}>
|
||||||
<TableHeaderCell>
|
<TableCell>{user.email}</TableCell>
|
||||||
<div className="flex justify-end">Actions</div>
|
<TableCell>
|
||||||
</TableHeaderCell>
|
<div className="flex justify-end">
|
||||||
|
<RemoveUserButton
|
||||||
|
user={user}
|
||||||
|
onSuccess={onRemovalSuccess}
|
||||||
|
onError={onRemovalError}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHead>
|
))}
|
||||||
<TableBody>
|
</TableBody>
|
||||||
{users.map((user) => (
|
</Table>
|
||||||
<TableRow key={user.email}>
|
{totalPages > 1 ? (
|
||||||
<TableCell>{user.email}</TableCell>
|
<CenteredPageSelector
|
||||||
<TableCell>
|
currentPage={currentPage}
|
||||||
<div className="flex justify-end">
|
totalPages={totalPages}
|
||||||
<RemoveUserButton
|
onPageChange={onPageChange}
|
||||||
user={user}
|
/>
|
||||||
onSuccess={onRemovalSuccess}
|
) : null}
|
||||||
onError={onRemovalError}
|
</>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
))}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
</>
|
|
||||||
</HidableSection>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -14,7 +14,6 @@ import {
|
|||||||
TableCell,
|
TableCell,
|
||||||
Button,
|
Button,
|
||||||
} from "@tremor/react";
|
} from "@tremor/react";
|
||||||
import { PageSelector } from "@/components/PageSelector";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
users: Array<User>;
|
users: Array<User>;
|
||||||
@@ -45,6 +44,7 @@ const PromoterButton = ({
|
|||||||
className="w-min"
|
className="w-min"
|
||||||
onClick={() => trigger({ user_email: user.email })}
|
onClick={() => trigger({ user_email: user.email })}
|
||||||
disabled={isMutating}
|
disabled={isMutating}
|
||||||
|
size="xs"
|
||||||
>
|
>
|
||||||
{promote ? "Promote" : "Demote"} to {promote ? "Admin" : "Basic"} User
|
{promote ? "Promote" : "Demote"} to {promote ? "Admin" : "Basic"} User
|
||||||
</Button>
|
</Button>
|
||||||
@@ -54,28 +54,38 @@ const PromoterButton = ({
|
|||||||
const DeactivaterButton = ({
|
const DeactivaterButton = ({
|
||||||
user,
|
user,
|
||||||
deactivate,
|
deactivate,
|
||||||
onSuccess,
|
setPopup,
|
||||||
onError,
|
mutate,
|
||||||
}: {
|
}: {
|
||||||
user: User;
|
user: User;
|
||||||
deactivate: boolean;
|
deactivate: boolean;
|
||||||
onSuccess: () => void;
|
setPopup: (spec: PopupSpec) => void;
|
||||||
onError: (message: string) => void;
|
mutate: () => void;
|
||||||
}) => {
|
}) => {
|
||||||
const { trigger, isMutating } = useSWRMutation(
|
const { trigger, isMutating } = useSWRMutation(
|
||||||
deactivate
|
deactivate
|
||||||
? "/api/manage/admin/deactivate-user"
|
? "/api/manage/admin/deactivate-user"
|
||||||
: "/api/manage/admin/activate-user",
|
: "/api/manage/admin/activate-user",
|
||||||
userMutationFetcher,
|
userMutationFetcher,
|
||||||
{ onSuccess, onError }
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
mutate();
|
||||||
|
setPopup({
|
||||||
|
message: `User ${deactivate ? "deactivated" : "activated"}!`,
|
||||||
|
type: "success",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onError: (errorMsg) => setPopup({ message: errorMsg, type: "error" }),
|
||||||
|
}
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
className="w-min"
|
className="w-min"
|
||||||
onClick={() => trigger({ user_email: user.email })}
|
onClick={() => trigger({ user_email: user.email })}
|
||||||
disabled={isMutating}
|
disabled={isMutating}
|
||||||
|
size="xs"
|
||||||
>
|
>
|
||||||
{deactivate ? "Deactivate" : "Activate"} Access
|
{deactivate ? "Deactivate" : "Activate"}
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -116,34 +126,8 @@ const SignedUpUserTable = ({
|
|||||||
onError(`Unable to demote admin - ${errorMsg}`);
|
onError(`Unable to demote admin - ${errorMsg}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onDeactivateSuccess = () => {
|
|
||||||
mutate();
|
|
||||||
setPopup({
|
|
||||||
message: "User deactivated!",
|
|
||||||
type: "success",
|
|
||||||
});
|
|
||||||
};
|
|
||||||
const onDeactivateError = (errorMsg: string) => {
|
|
||||||
setPopup({
|
|
||||||
message: `Unable to deactivate user - ${errorMsg}`,
|
|
||||||
type: "error",
|
|
||||||
});
|
|
||||||
};
|
|
||||||
const onActivateSuccess = () => {
|
|
||||||
mutate();
|
|
||||||
setPopup({
|
|
||||||
message: "User activate!",
|
|
||||||
type: "success",
|
|
||||||
});
|
|
||||||
};
|
|
||||||
const onActivateError = (errorMsg: string) => {
|
|
||||||
setPopup({
|
|
||||||
message: `Unable to activate user - ${errorMsg}`,
|
|
||||||
type: "error",
|
|
||||||
});
|
|
||||||
};
|
|
||||||
return (
|
return (
|
||||||
<HidableSection sectionTitle="Signed Up Users">
|
<HidableSection sectionTitle="Current Users">
|
||||||
<>
|
<>
|
||||||
{totalPages > 1 ? (
|
{totalPages > 1 ? (
|
||||||
<CenteredPageSelector
|
<CenteredPageSelector
|
||||||
@@ -186,8 +170,8 @@ const SignedUpUserTable = ({
|
|||||||
<DeactivaterButton
|
<DeactivaterButton
|
||||||
user={user}
|
user={user}
|
||||||
deactivate={user.status === UserStatus.live}
|
deactivate={user.status === UserStatus.live}
|
||||||
onSuccess={onDeactivateSuccess}
|
setPopup={setPopup}
|
||||||
onError={onDeactivateError}
|
mutate={mutate}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
@@ -12,8 +12,8 @@ const userMutationFetcher = async (
|
|||||||
}),
|
}),
|
||||||
}).then(async (res) => {
|
}).then(async (res) => {
|
||||||
if (res.ok) return res.json();
|
if (res.ok) return res.json();
|
||||||
const text = await res.text();
|
const errorDetail = (await res.json()).detail;
|
||||||
throw Error(text);
|
throw Error(errorDetail);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user